Android Handler詳解、使用(倒計時、驗證碼)
一、爲什麼要使用Handler
當出現耗時操作,並需要根據耗時操作返回結果時:
當Android的一個程序開啓的時候,他會開啓一個主線程,也就是常說的UI線程,但是大家都知道不能在主線程中進行耗時操作,就是各種下載、IO操作、等等,如果時間過長那麼會出現一個ANR無響應的對話框,提示等待或者關閉。所以我們把這些耗時的操作放入子線程中去執行。
我們將耗時操作放入子線程中執行的話又會出現一個問題,就是當我們執行耗時操作的時候又想對UI更新操作怎麼辦?
子線程沒有辦法對UI界面上的內容進行操作,如果操作,將拋出異常:CalledFromWrongThreadException
子線程沒有辦法對UI界面上的內容進行操作,如果操作,將拋出異常:CalledFromWrongThreadException
也就是說我們在Android中處理多線程要保證一下兩點:
1.不要阻塞UI線程
2.不要在UI線程之外訪問Android UI工具包(TextView.setText()這樣的操作)
所以這時就需要一種機制:主線程可以發送“命令/任務”給子線程執行,然後子線程反饋執行結果;
那麼爲了實現子線程中操作UI界面,Android中引入了Handler消息傳遞機制,目的是打破對主線程的依賴性。
那麼爲了實現子線程中操作UI界面,Android中引入了Handler消息傳遞機制,目的是打破對主線程的依賴性。
二、那麼什麼是Handler呢?
一個Handler允許你發送和處理消息(Message)以及與一個線程的消息隊列相關的Runnable對象。每個Handler實例都和單個線程以及該線程的消息隊列有關。當你創建了一個新Handler,它就會和創建它的線程/消息隊列綁定,在那以後,它就會傳遞消息以及runnable對象給消息隊列,然後執行它們。
需要使用Handler有兩大主要的原因:
(1)在將來的某個時間點調度處理消息和runnable對象;
(2)將需要執行的操作放到其他線程之中,而不是自己的;
(1)在將來的某個時間點調度處理消息和runnable對象;
(2)將需要執行的操作放到其他線程之中,而不是自己的;
調度處理消息是通過調用post(Runnable), postAtTime(Runnable, long),postDelayed(Runnable, long), sendEmptyMessage(int),
sendMessage(Message),sendMessageAtTime(Message, long)和sendMessageDelayed(Message,long)等方法完成的。其中的post版本的方法可以讓你將Runnable對象放進消息隊列;sendMessage版本的方法可以讓你將一個包含有bundle對象的消息對象放進消息隊列,然後交由handleMessage(Message)方法處理。(這個需要你複寫Handler的handleMessage方法)
你必需要知道的:
若在主線程中實例化一個Handler對象,例如: Handler mHandler = newHandler();
此時它並沒有新派生一個線程來執行此Handler,而是將此Handler附加在主線程上,故此時若你在Handler中執行耗時操作的話,還是會彈出ANR對話框!
三、一個簡單的示例,驗證碼倒計時的實現
先上一張圖吧
1.創建一個Handler對象並實現其中的handleMessage方法
2.在點擊方法中開啓線程循環發送消息
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
break;
}
}
};
//send方式發送驗證碼
public void sendMessageClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 59;i>=0;i--){
Message msg = handler.obtainMessage();
msg.arg1 = i;
handler.sendMessage(msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
3.對handleMessage方法進行完善
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
if(msg.arg1==0){
button_send.setText("點擊發送驗證碼");
button_send.setClickable(true);
}else{
button_send.setText("請在("+msg.arg1+"秒)後再次點擊按鈕");
button_send.setClickable(false);
}
break;
}
}
};
但是!上述代碼並不算是一個很好的代碼,因爲沒有考慮到Handler的內存泄露問題,那麼這個內存泄露是如何產生的呢?又應該如何去解決。
因爲Handler會持有一個外部類的引用,例如在Mainactivity中創建,那麼會獲取到Mainactivity的引用,因爲Handler是他的內部類,內部類的對象要依賴於外部類。當你使用的時候,如果退出了Mainactivity那麼Handler對象應該被銷燬,但是當你退出的時候Handler還在工作的話,那麼這個Mainactivity並不能被退出。也就是Handler執行後纔會退出,依然佔用內存,也就造成了內存泄露。
解決辦法:
1.定義一個內部類時會默認擁有外部類對象的引用,所以建議使用內部類時最好定義爲一個static靜態內部類。(因爲靜態內部類相當於外部類,不依賴外部類對象)
2.創建一個自定義的Handler類並集成Handler
3.定義一個弱引用 定義當前對象
private static class MyHandler extends Handler{
private final WeakReference<MainActivity> weakReference;
public MyHandler(MainActivity activity) {
weakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = weakReference.get();
if(activity!=null){
switch (msg.what) {
case 0:
if(msg.arg1==0){
button_send.setText("點擊發送驗證碼");
button_send.setClickable(true);
}else{
button_send.setText("請在("+msg.arg1+"秒)後再次點擊按鈕");
button_send.setClickable(false);
}
break;
}
}
}
}
4.創建一個自定義Handler對象private MyHandler myHandler = new MyHandler(this);
四、上述是使用send,那麼如何使用Post呢?
1.最開始還是一樣的方式
2.將Runnable對象發送到消息隊列中,按照隊列的機制按順序執行不同的Runnable對象中的run方法。
//post方式發送驗證碼
public void sendMessagePostClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 59;i>=0;i--){
Message msg = myHandler.obtainMessage();
msg.arg1 = i;
final int finalI = i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myHandler.post(new Runnable() {
@Override
public void run() {
if(finalI ==0){
button_post.setText("點擊發送驗證碼");
button_post.setClickable(true);
}else{
button_post.setText("請在("+ finalI +"秒)後再次點擊按鈕");
button_post.setClickable(false);
}
}
});
}
}
}).start();
}
五、如果在消息處理的時候有延時操作怎麼辦?
1.post
//開啓子線程的線程名字
private HandlerThread handlerThread = new HandlerThread("myHandlerThread");
private Handler mHandler = new Handler();
public void downloadPostClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第一種方式,在handler綁定在主線程中,其中不能做耗時操作
mHandler.post(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("ceshi", "post線程id"+Thread.currentThread().getId());
}
}) ;
/* //開啓了子線程,可以耗時操作了
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
mHandler.post(new Runnable() {
@Override
public void run() {
Log.e("ceshi", "post線程id"+Thread.currentThread().getName());
}
});*/
}
}).start();
}
如果不開啓子線程的話,就是在主線程中執行耗時操作了,所以開啓一個HandlerThread在這個新建的線程中執行耗時操作。2.send方式的話那麼在處理的時候使用新的線程就好
六、原理
1.首先是基本的概念:
a.Message:消息對象,其中包含了消息ID,消息處理對象以及處理的數據等,由MessageQueue統一列隊,終由Handler處理。用於重複利用,避免大量創建消息對象,造成內存浪費
b.Handler:消息處理者,負責Message的發送及處理。使用Handler時,需要實handleMessage(Message msg)方法來對特定的Message進行處理,例如更新UI等。Handler類的主要作用:(有兩個主要作用)1)、在工作線程中發送消息;2)、在主線程中獲取、並處理消息。
c.MessageQueue:消息隊列,用來存放Handler發送過來的消息,並按照FIFO規則執行。當然,存放Message並非實際意義的保存,而是將Message串聯起來的,等待Looper的抽取。
d.Looper:消息泵(消息對象的處理者),不斷地從MessageQueue中抽取Message執行。因此,一個MessageQueue需要一個Looper。
e.Thread:線程,負責調度整個消息循環,即消息循環的執行場所。
首先Handler創建一個Message對象,存放如消息隊列中,然後消息泵從隊列中取出Message對象(FIFO原則)
,當取出對象後交送給Handler的消息處理者handleMessage(Message msg)去處理消息。其中Message Pool是消息池,當消息創建後如果在執行那麼則創建新的,如果不在執行那麼從消息池中直接調用就好。
附上驗證碼倒計時的源碼:源碼