您好,登錄后才能下訂單哦!
前言
做 Android 開發肯定離不開跟 Handler 打交道,它通常被我們用來做主線程與子線程之間的通信工具,而 Handler 作為 Android 中消息機制的重要一員也確實給我們的開發帶來了極大的便利。
可以說只要有異步線程與主線程通信的地方就一定會有 Handler。
那么;
本文將持續為你揭曉
handler 整個流程中,主要有四個對象,handler,Message
,MessageQueue,Looper
。 當應用創建的時候,就會在主線程中創建 handler 對象,
Android 中主線程是不能進行耗時操作的,子線程是不能進行更新 UI 的。所以就有了 handler, 它的作用就是實現線程之間的通信。
當應用創 建的時候,就會在主線程中創建 handler對象, 我們通過要傳送的消息保存到 Message
中,handler 通過調用 sendMessage
方法 將 Message
發送到 MessageQueue
中,Looper 對象就會不斷的調用 loop()
方法
不斷的從 MessageQueue
中取出 Message
交給 handler
進行處理。從而實現線程 之間的通信。
1) Handler 線程的消息通訊的橋梁,主要用來發送消息及處理消息。
2) Thread 普通線程,如果需要有自己的消息隊列,需要調用 Looper.prepare()
創建 Looper
實例,調用 loop()去循環消息。
3) HandlerThread 是一個帶有 Looper 的線程,在 HandleThread 的 run()
方法中調用了 Looper.prepare()
創建了 Looper
實例,并調用 Looper.loop()
開啟了 Loop 循環,循環從消息隊 列中獲取消息并交由 Handler 處理。利用該線程的 Looper 創建 Handler 實例,此 Handler 的 handleMessage()
方法是運行在子線程中的。即 Handler 利用哪個線程的 Looper 創建的實例, 它就和相應的線程綁定到一起,處理該線程上的消息,它的 handleMessage()
方法就是在那 個線程中運行的,無參構造默認是主線程。
HandlerThread
提供了 quit()/quitSafely()
方法退出 HandlerThread
的消息循環,它們分別調用 Looper
的 quit
和 quitSafely
方法,quit 會將消息 隊列中的所有消息移除,而 quitSafely
會將消息隊列所有延遲消息移除,非延遲消息派發出 去讓 Handler 去處理。
HandlerThread
適合處理本地 IO 讀寫操作(讀寫數據庫或文件),因為本地 IO 操作耗 時不長,對于單線程+異步隊列不會產生較大阻塞,而網絡操作相對比較耗時,容易阻塞后 面的請求,因此HandlerThread
不適合加入網絡操作
負責跨線程通信,這是因為在主線程不能做耗時操作,而子線程不能更新 UI,所以當子線程中進行耗時操作后需要更新 UI時,通過 Handler 將有關 UI 的操作切換到主線程中執行。Handler 作用 ?有哪些要素 ?流程是怎樣的 ?
具體分為四大要素:
①Message(消息): 需要被傳遞的消息,消息分為硬件產生的消息(如按鈕、觸摸)和軟件生成的消息。
②MessageQueue(消息隊列): 負責消息的存儲與管理,負責管理由 Handler 發送過來的 Message。讀取會自動刪除消息,單鏈表維護,插入和刪除上有優勢。在其 next()方法中會無限循環,不斷判斷是否有消息,有就返回這條消息并移除
③Handler(消息處理器): 負責 Message 的發送及處理。主要向消息池發送各種消息事件(Handler.sendMessage())和處理相應消息事件(Handler.handleMessage()),按照先進先出執行,內部使用的是單鏈表的結構。
④Looper(消息池): 負責關聯線程以及消息的分發,在該線程下從 MessageQueue 獲取 Message,分發給Handler,Looper 創建的時候會創建一個MessageQueue,調用 loop()方法的時候消息循環開始,其中會不斷調用 messageQueue 的 next()方法,當有消息就處理,否則阻塞在 messageQueue 的next()方法中。當 Looper 的 quit()被調用的時候會調用messageQueue
的 quit(),此時 next()會返回 null,然后 loop()方法也就跟著退出。
泄露原因:
Handler 允許我們發送延時消息,如果在延時期間用戶關閉了 Activity,那么該 Activity會泄露。 這個泄露是因為 Message會持有 Handler,而又因為 Java 的特性,內部類會持有外部類,使得 Activity 會被Handler 持有,這樣最終就導致 Activity 泄露。
解決方案:
將 Handler 定義成靜態的內部類,在內部持有
Activity 的弱引用,并在 Acitivity 的 onDestroy()中調用 handler.removeCallbacksAndMessages(null)
及時移除所有消息
如果隊列中只有這個消息,那么消息不會被發送,而是計算到時喚醒的時間,先將 Looper 阻塞,到時間就喚醒它。但如果此時要加入新消息,該消息隊列的對頭跟 delay 時間相比更長,則插入到頭部,按照觸發時間進行排序,隊頭的時間最小、隊尾的時間最大
不可以,因為在主線程中,Activity 內部包含一個 Looper 對象,它會自動管理 Looper,處理子線程中發送過來的消息。而對于子線程而言,沒有任何對象幫助我們維護 Looper 對象,所以需要我們自己手動維護。所以要在子線程開啟 Handler 要先創建 Looper,并開啟 Looper 循環
//示例代碼
new Thread(new Runnable(){
@Override
public void run() {
looper.prepare();
new Handler() {
@Override
piblic void handlerMessage(Message msg) {
super,handleMessage(msg);
}
}
looper.loop();
}
}).start();
該問題很難被考到,但是如果一旦問到,100%會回答 不上來。開發者很難注意到一個主線程的四循環居然沒有阻塞住主 線程。
應該從 主線程的消息循環機制 與 Linux 的循環異步等 待作用講起。最后將 handle 引起的內存泄漏,內存泄漏一定是一 個加分項
這里有簡單的幾個問題拋出來:
1.Looper 死循環為什么不會導致應用卡死,會消耗大量資源嗎?
2.主線程的消息循環機制是什么(死循環如何處理其它事務)?
3.ActivityThread 的動力是什么?(ActivityThread 執行 Looper 的線程是什么)
4.Handler 是如何能夠線程切換,發送 Message 的?(線程間通訊)
5.子線程有哪些更新 UI 的方法。
6.子線程中 Toast,showDialog,的方法。(和子線程不能更新 UI 有關嗎)
7.如何處理 Handler 使用不當導致的內存泄露?
1.Looper 死循環為什么不會導致應用卡死?
線程默認沒有 Looper 的,如果需要使用 Handler 就必須為 線程創建 Looper。我們經常提到的主線程,也叫 UI 線程, 它就是 ActivityThread,ActivityThread 被創建時就會初 始化 Looper,這也是在主線程中默認可以使用 Handler 的 原因
我們先來看一段代碼:
new Thread(new Runnable() {
@Override
public void run() {
Log.e("qdx", "step 0 ");
Looper.prepare();
Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();
Log.e("qdx", "step 1 ");
Looper.loop();
Log.e("qdx", "step 2 ");
}
}).start();
我們知道Looper.loop();里面維護了一個死循環方法,所以按照理論,上述代碼執行的應該是 step 0 –>step 1 也就是說循環在Looper.prepare();與Looper.loop();之間
在子線程中,如果手動為其創建了Looper,那么在所有的事情完成以后應該調用quit方法來終止消息循環,否則這個子線程就會一直處于等待(阻塞)狀態,而如果退出Looper以后,這個線程就會立刻(執行所有方法并)終止,因此建議不需要的時候終止Looper
執行結果也正如我們所說,這時候如果了解ActivityThread,并且在main方法中我們會看到主線程也是通過Looper方式來維持一個消息循環。
public static void main(String[] args) {
Looper.prepareMainLooper();//創建Looper和MessageQueue對象,用于處理主線程的消息
ActivityThread thread = new ActivityThread();
thread.attach(false);//建立Binder通道 (創建新線程)
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
//如果能執行下面方法,說明應用崩潰或者是退出了...
throw new RuntimeException("Main thread loop unexpectedly exited");
}
那么回到我們的問題上,這個死循環會不會導致應用卡死,即使不會的話,它會慢慢的消耗事實上,會在進入死循環之前便創建了新binder線程,在代碼ActivityThread.main()中:越來越多的資源嗎?
對于線程即是一段可執行的代碼,當可執行代碼執行完成后,線程生命周期便該終止了,線程退出。而對于主線程,我們是絕不希望會被運行一段時間,自己就退出,那么如何保證能一直存活呢?簡單做法就是可執行代碼是能一直執行下去的,死循環便能保證不會被退出,例如,binder線程也是采用死循環的方法,通過循環方式不同與Binder驅動進行讀寫操作,當然并非簡單地死循環,無消息時會休眠。但這里可能又引發了另一個問題,既然是死循環又如何去處理其他事務呢?通過創建新線程的方式。真正會卡死主線程的操作是在回調方法onCreate/onStart/onResume等操作時間過長,會導致掉幀,甚至發生ANR,looper.loop本身不會導致應用卡死。
主線程的死循環一直運行是不是特別消耗CPU資源呢? 其實不然,這里就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這里采用的epoll機制,是一種IO多路復用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質同步I/O,即讀寫是阻塞的。 所以說,主線程大多數時候都是處于休眠狀態,并不會消耗大量CPU資源
2.主線程的消息循環機制是什么?
事實上,會在進入死循環之前便創建了新binder線程,在代碼ActivityThread.main()中:
public static void main(String[] args) {
//創建Looper和MessageQueue對象,用于處理主線程的消息
Looper.prepareMainLooper();
//創建ActivityThread對象
ActivityThread thread = new ActivityThread();
//建立Binder通道 (創建新線程)
thread.attach(false);
Looper.loop(); //消息循環運行
throw new RuntimeException("Main thread loop unexpectedly exited");
}
Activity的生命周期都是依靠主線程的 Looper.loop,當收到不同Message時則采用相應措施:一旦退出消息循環,那么你的程序也就可以退出了。 從消息隊列中取消息可能會阻塞,取到消息會做出相應的處理。如果某個消息處理時間過長,就可能會影響UI線程的刷新速率,造成卡頓的現象。
thread.attach(false)方法函數中便會創建一個Binder線程(具體是指ApplicationThread,Binder的服務端,用于接收系統服務AMS發送來的事件),該Binder線程通過Handler將Message發送給主線程。「Activity 啟動過程」
比如收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會通過反射機制,創建Activity實例,然后再執行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。
主線程的消息又是哪來的呢?當然是App進程中的其他線程通過Handler發送給主線程進程
3.ActivityThread 的動力是什么?
進程 每個app運行時前首先創建一個進程,該進程是由Zygote fork出來的,用于承載App上運行的各種Activity/Service等組件。進程對于上層應用來說是完全透明的,這也是google有意為之,讓App程序都是運行在Android Runtime。大多數情況一個App就運行在一個進程中,除非在AndroidManifest.xml中配置Android:process屬性,或通過native代碼fork進程
線程 線程對應用來說非常常見,比如每次new Thread().start都會創建一個新的線程。該線程與App所在進程之間資源共享,從Linux角度來說進程與線程除了是否共享資源外,并沒有本質的區別,都是一個task_struct結構體,在CPU看來進程或線程無非就是一段可執行的代碼,CPU采用CFS調度算法,保證每個task都盡可能公平的享有CPU時間片。
其實承載ActivityThread的主線程就是由Zygote fork而創建的進程。
4.Handler 是如何能夠線程切換
其實看完上面我們大致也清楚線程間是共享資源的。所以Handler處理不同線程問題就只要注意異步情況即可。
這里再引申出Handler的一些小知識點。 Handler創建的時候會采用當前線程的Looper來構造消息循環系統,Looper在哪個線程創建,就跟哪個線程綁定,并且Handler是在他關聯的Looper對應的線程中處理消息的。(敲黑板)
那么Handler內部如何獲取到當前線程的Looper呢—–ThreadLocal。ThreadLocal可以在不同的線程中互不干擾的存儲并提供數據,通過ThreadLocal可以輕松獲取每個線程的Looper。
當然需要注意的是:
①線程是默認沒有Looper的,如果需要使用Handler,就必須為線程創建Looper。我們經常提到的主線程,也叫UI線程,它就是ActivityThread,
②ActivityThread被創建時就會初始化Looper,這也是在主線程中默認可以使用Handler的原因。
系統為什么不允許在子線程中訪問UI?(摘自《Android開發藝術探索》)
這是因為Android的UI控件不是線程安全的,如果在多線程中并發訪問可能會導致UI控件處于不可預期的狀態,那么為什么系統不對UI控件的訪問加上鎖機制呢?
缺點有兩個:
①首先加上鎖機制會讓UI訪問的邏輯變得復雜
②鎖機制會降低UI訪問的效率,因為鎖機制會阻塞某些線程的執行。 所以最簡單且高效的方法就是采用單線程模型來處理UI操作
5.子線程有哪些更新UI的方法
主線程中定義Handler,子線程通過mHandler發送消息,主線程Handler的handleMessage更新UI。 用Activity對象的runOnUiThread方法。 創建Handler,傳入getMainLooper。 View.post(Runnabler) 。
runOnUiThread 第一種咱們就不分析了,我們來看看第二種比較常用的寫法。
先重新溫習一下上面說的
Looper在哪個線程創建,就跟哪個線程綁定,并且Handler是在他關聯的Looper對應的線程中處理消息的。(敲黑板)
new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
//DO UI method
}
});
}
}).start();
final Handler mHandler = new Handler();
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);//子線程(非UI線程)
} else {
action.run();
}
}
進入Activity類里面,可以看到如果是在子線程中,通過mHandler發送的更新UI消息。 而這個Handler是在Activity中創建的,也就是說在主線程中創建,所以便和我們在主線程中使用Handler更新UI沒有差別。 因為這個Looper,就是ActivityThread中創建的Looper(Looper.prepareMainLooper())。
創建Handler,傳入getMainLooper 那么同理,我們在子線程中,是否也可以創建一個Handler,并獲取MainLooper,從而在子線程中更新UI呢? 首先我們看到,在Looper類中有靜態對象sMainLooper,并且這個sMainLooper就是在ActivityThread中創建的MainLooper
private static Looper sMainLooper; // guarded by Looper.class
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
所以不用多說,我們就可以通過這個sMainLooper來進行更新UI操作
new Thread(new Runnable() {
@Override
public void run() {
Log.e("qdx", "step 1 "+Thread.currentThread().getName());
Handler handler=new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
//Do Ui method
Log.e("qdx", "step 2 "+Thread.currentThread().getName());
}
});
}
}).start();
View.post(Runnabler)老樣子,我們點入源碼
//View
/**
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
*
* @param action The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
* message queue. Returns false on failure, usually because the
* looper processing the message queue is exiting.
*
*/
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action); //一般情況走這里
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
/**
* A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
* handler can be used to pump events in the UI events queue.
*/
final Handler mHandler;
居然也是Handler從中作祟,根據Handler的注釋,也可以清楚該Handler可以處理UI事件,也就是說它的Looper也是主線程的sMainLooper。這就是說我們常用的更新UI都是通過Handler實現的。
另外更新UI 也可以通過AsyncTask來實現,難道這個AsyncTask的線程切換也是通過 Handler 嗎? 沒錯,也是通過Handler……
6.子線程中Toast,showDialog,的方法
可能有些人看到這個問題,就會想: 子線程本來就不可以更新UI的啊 而且上面也說了更新UI的方法.兄臺且慢,且聽我把話寫完
new Thread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩潰無疑
}
}).start();
看到這個崩潰日志,是否有些疑惑,因為一般如果子線程不能更新UI控件是會報如下錯誤的(子線程不能更新UI)
所以子線程不能更新Toast的原因就和Handler有關了,據我們了解,每一個Handler都要有對應的Looper對象,那么。 滿足你
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
這樣便能在子線程中Toast,不是說子線程…? 老樣子,我們追根到底看一下Toast內部執行方式
//Toast
/**
* Show the view for the specified duration.
*/
public void show() {
......
INotificationManager service = getService();//從SMgr中獲取名為notification的服務
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);//enqueue? 難不成和Handler的隊列有關?
} catch (RemoteException e) {
// Empty
}
在show方法中,我們看到Toast的show方法和普通UI 控件不太一樣,并且也是通過Binder進程間通訊方法執行Toast繪制。這其中的過程就不在多討論了,有興趣的可以在NotificationManagerService類中分析。
現在把目光放在TN 這個類上(難道越重要的類命名就越簡潔,如H類),通過TN 類,可以了解到它是Binder的本地類。在Toast的show方法中,將這個TN對象傳給NotificationManagerService就是為了通訊!并且我們也在TN中發現了它的show方法。
private static class TN extends ITransientNotification.Stub {//Binder服務端的具體實現類
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
}
看完上面代碼,就知道子線程中Toast報錯的原因,因為在TN中使用Handler,所以需要創建Looper對象。 那么既然用Handler來發送消息,就可以在handleMessage中找到更新Toast的方法。 在handleMessage看到由handleShow處理。
//Toast的TN類
public void handleShow(IBinder windowToken) {
......
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, mParams);//使用WindowManager的addView方法
trySendAccessibilityEvent();
}
}
看到這里就可以總結一下:
Toast本質是通過window顯示和繪制的(操作的是window),而主線程不能更新UI 是因為ViewRootImpl的checkThread方法在Activity維護的View樹的行為。 Toast中TN類使用Handler是為了用隊列和時間控制排隊顯示Toast,所以為了防止在創建TN時拋出異常,需要在子線程中使用Looper.prepare();和Looper.loop();(但是不建議這么做,因為它會使線程無法執行結束,導致內存泄露)
Dialog亦是如此。同時我們又多了一個知識點要去研究:Android 中Window是什么,它內部有什么機制?
7.如何處理Handler 使用不當導致的內存泄露? 首先上文在子線程中為了節目效果,使用如下方式創建Looper
Looper.prepare();
.......
Looper.loop();
實際上這是非常危險的一種做法
在子線程中,如果手動為其創建Looper,那么在所有的事情完成以后應該調用quit方法來終止消息循環,否則這個子線程就會一直處于等待的狀態,而如果退出Looper以后,這個線程就會立刻終止,因此建議不需要的時候終止Looper。(【 Looper.myLooper().quit();】)
那么,如果在Handler的handleMessage方法中(或者是run方法)處理消息,如果這個是一個延時消息,會一直保存在主線程的消息隊列里,并且會影響系統對Activity的回收,造成內存泄露。
具體可以參考Handler內存泄漏分析及解決
總結一下,解決Handler內存泄露主要2點
1 有延時消息,要在Activity銷毀的時候移除Messages
2 匿名內部類導致的泄露改為匿名靜態內部類,并且對上下文或者Activity使用弱引用。
總結
想不到Handler居然可以騰出這么多浪花,與此同時感謝前輩的摸索。
另外Handler還有許多不為人知的秘密,等待大家探索,下面我再簡單的介紹兩分鐘
HandlerThread
IdleHandler
HandlerThread
HandlerThread繼承Thread,它是一種可以使用Handler的Thread,它的實現也很簡單,在run方法中也是通過Looper.prepare()來創建消息隊列,并通過Looper.loop()來開啟消息循環(與我們手動創建方法基本一致),這樣在實際的使用中就允許在HandlerThread中創建Handler了
由于HandlerThread的run方法是一個無限循環,因此當不需要使用的時候通過quit或者quitSafely方法來終止線程的執行
HandlerThread的本質也是線程,所以切記關聯的Handler中處理消息的handleMessage為子線程。
IdleHandler
/**
* Callback interface for discovering when a thread is going to block
* waiting for more messages.
*/
public static interface IdleHandler {
/**
* Called when the message queue has run out of messages and will now
* wait for more. Return true to keep your idle handler active, false
* to have it removed. This may be called if there are still messages
* pending in the queue, but they are all scheduled to be dispatched
* after the current time.
*/
boolean queueIdle();
}
根據注釋可以了解到,這個接口方法是在消息隊列全部處理完成后或者是在阻塞的過程中等待更多的消息的時候調用的,返回值false表示只回調一次,true表示可以接收多次回調。
具體使用如下代碼
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
return false;
}
});
另外提供一個小技巧:在HandlerThread中獲取Looper的MessageQueue方法之反射。
因為Looper.myQueue()如果在主線程調用就會使用主線程looper 使用handlerThread.getLooper().getQueue()最低版本需要23 //HandlerThread中獲取MessageQueue
Field field = Looper.class.getDeclaredField("mQueue");
field.setAccessible(true);
MessageQueue queue = (MessageQueue) field.get(handlerThread.getLooper());
那么Android的消息循環機制是通過Handler,是否可以通過IdleHandler來判斷Activity的加載和繪制情況(measure,layout,draw等)呢?并且IdleHandler是否也隱藏著不為人知的特殊功能?
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。