一、前言
其實,這篇文章本來應該叫做"Handler
原始碼解析"的,但是想想寫Handler
原始碼的文章太多了,還是緩一緩,先寫些不一樣的,今天,我們從原始碼的角度來總結一下應用Handler
的一些場景的原理。
二、Handler 的應用
2.1 執行緒間的通訊
在平時的開發當中,Handler
最常見的用法就是用於執行緒之間的通訊,特別是當我們在子執行緒中去處理耗時的任務,當任務完成之後,我們希望將結果傳送到主執行緒中進行處理,那麼就會使用到Handler
,基本的思想如下圖所示:
Handler
放入在主執行緒中的迴圈佇列時,那麼主執行緒就會收到訊息,之後,我們就可以在主執行緒中進行後續的處理,例如下面這樣:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d("Handler", "HandleMessageThreadId=" + Thread.currentThread().getId());
}
};
private void startThread() {
new Thread() {
@Override
public void run() {
super.run();
Log.d("Handler", "ThreadId=" + Thread.currentThread().getId());
mHandler.sendEmptyMessage(0);
}
}.start();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("Handler", "MainThreadId=" + Thread.currentThread().getId());
startThread();
}
}
複製程式碼
列印出上面的執行緒Id
,可以看到最後handleMessage
收到訊息的時候是在主執行緒當中:
2.2 實現延時操作
除了執行緒間的通訊之外,我們還可以通過Handler
提供的sendXXXDelay
方法,實現延時操作,延時操作有兩種目的:
- 一種是希望讓不那麼緊急的任務延後執行,例如在應用啟動過程中,我們在
onCreate
方法中的任務不是那個緊急,那麼可以通過Handler
傳送一個延時訊息出去,讓它不佔用主執行緒去渲染布局的資源,從而提高應用的啟動速度。 - 另一種就是防抖動操作,我們收到一個命令,並不是馬上執行它,而是通過
Handler
的sendxxxDelay
方法延時,如果在這段事件內又有一個相同的命令來到了,那麼就把之前的訊息移除,再放入一個新的延時訊息。 例如下面的例子,當我們點選一個按鈕之後,我們不立刻執行任務,而是一段時間之後仍然沒有收到第二次點選事件採取執行任務:
private Handler mDelayHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d("Handler", "handleMessage");
}
};
private void performClickDelay() {
Log.d("Handler", "performClickDelay");
mDelayHandler.removeMessages(1);
mDelayHandler.sendEmptyMessageDelayed(1, 500);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTv = (TextView) findViewById(R.id.tv);
mTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performClickDelay();
}
});
}
複製程式碼
我們連續多次點選按鈕之後,只收到了一次訊息:
2.3 使用HandlerThread
在非同步執行緒執行耗時操作
由於Looper
其實是執行緒的一個私有變數(ThreadLocal
),主執行緒可以有Looper
,子執行緒同樣也可以有Looper
,只不過從子執行緒中的Looper
中的MessageQueue
中取出訊息之後,是在子執行緒當中處理的,那麼我們就可以通過它來執行非同步操作,其基本思想如下圖所示:
HandlerThread
就是基於這一思想來實現的,關於HandlerThread
的內部實現,可以參考之前的這篇文章 Android 非同步任務知識梳理(2) - HandlerThread 原始碼解析,它的本質就是當非同步執行緒啟動之後,會初始化這個執行緒私有的Looper
,因此,當我們通過HandlerThread
中的getLooper()
方法獲得這個Looper
之後,在通過這個Looper
來建立一個Handler
,那麼這個Handler
的handleMessage
回撥時所在的執行緒就是這個非同步執行緒。
下面是一個簡單的事例:
private void useHandlerThread() {
final HandlerThread handlerThread = new HandlerThread("handlerThread");
System.out.println("MainThreadId=" + Thread.currentThread().getId() + ",handlerThreadId=" + handlerThread.getId());
handlerThread.start();
MyHandler handler = new MyHandler(handlerThread.getLooper());
handler.sendEmptyMessage(0);
}
private class MyHandler extends Handler {
MyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
System.out.println("handleMessageThreadId=" + Thread.currentThread().getId());
}
}
複製程式碼
我們列印出handleMessage
的執行時的執行緒ID
,可以看到它就是HandlerThread
的執行緒ID
,因此我們就可以通過傳送訊息的方式,在非同步執行緒執行一些耗時的操作:
2.4 使用AsyncQueryHandler
對ContentProvider
進行操作
如果你的本地資料使用ContentProvider
封裝的,那麼對這些資料的增刪改查操作,為了不影響主執行緒,應該在子執行緒中進行操作,而AsyncQueryHandler
就是系統為我們提供的很方便的工具類,它通過Handler + HandlerThread
的方式,實現了非同步的增刪改查。
它是在HandlerThread
的基礎之上擴充套件出來的,其基本思想如下圖所示,這一框架就保證了發起命令和接收回撥是在同一個執行緒,而任務的執行則是在另一個執行緒:
當然AsyncQueryHandler
限制了只能操作通過ContentProvider
封裝的資料,我們可以參考它的思想,進行擴充套件,實現對於資料庫的增刪改查。
2.5 使用Handler
機制檢測應用中的卡頓問題
對於每個應用程式來說,它的入口函式為ActivityThread
中的main()
方法:
public static void main(String[] args) {
//...
Looper.prepareMainLooper();
//...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
複製程式碼
而在main()
函式的最後,則構建一個在主執行緒中的迴圈佇列,之後應用程式收到的事件之後,還主執行緒就會被喚醒,進行事件的處理,假如這一處理的事件過長,那麼就會發生ANR
,因此,我們就可以通過計算主執行緒中對於單次訊息的處理時間,從而間隔地判斷是否存在卡頓問題。
那麼要怎麼知道主執行緒中單次訊息的處理時間呢,我們檢視Looper
的原始碼:
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
複製程式碼
對於訊息的處理對應這句:
msg.target.dispatchMessage(msg);
複製程式碼
而在這句話的前後,則呼叫了Printer
類列印,那麼我們就可以通過這兩次列印的之間的時長來獲得單次訊息的處理時間。
public class MainLoopMonitor {
private static final String MSG_START = ">>>>> Dispatching";
private static final String MSG_END = "<<<<< Finished";
private static final int TIME = 1000;
private Handler mExecuteHandler;
private Runnable mExecuteRunnable;
private static class Holder {
private static final MainLoopMonitor INSTANCE = new MainLoopMonitor();
}
public static MainLoopMonitor getInstance() {
return Holder.INSTANCE;
}
private MainLoopMonitor() {
HandlerThread monitorThread = new HandlerThread("LooperMonitor");
monitorThread.start();
mExecuteHandler = new Handler(monitorThread.getLooper());
mExecuteRunnable = new ExecutorRunnable();
}
public void startMonitor() {
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
if (x.startsWith(MSG_START)) {
mExecuteHandler.postDelayed(mExecuteRunnable, TIME);
} else if (x.startsWith(MSG_END)) {
mExecuteHandler.removeCallbacks(mExecuteRunnable);
}
}
});
}
private class ExecutorRunnable implements Runnable {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s).append("\n");
}
System.out.println("MainLooperMonitor:" + sb.toString());
}
}
}
複製程式碼
假如我們像下面這樣,在主執行緒中進行了耗時的操作:
public void anrButton(View view) {
for (int i = 0; i < (1 << 30); i++) {
int b = 6;
}
}
複製程式碼
那麼就會列印出下面的堆疊資訊:
三、Handler 使用注意事項
在介紹完Handler
的這些用法,我們再來總結一下使用Handler
是比較容易犯的一些錯誤。
3.1 記憶體洩漏
一個比較常見的錯誤就是將定義Handler
的子類時將它作為Activity
的內部類,而由於內部類會預設持有外部類的引用,因此,如果這個內部類的例項在Activity
試圖被回收的時候,沒有被銷燬掉,那麼就會導致Activity
無法被回收,從而引起記憶體洩漏。
而Handler
例項無法被銷燬掉最常見的情況就是,我們通過它傳送了一個延時訊息出去,此時這個訊息會被放入到該執行緒所對應的Looper
中的MessageQueue
當中,而該訊息為了能在得到執行之後,回撥到對應的Handler
,因此它會持有這個Handler
的例項。
從引用鏈的角度來看就是下面的情況:
這個大家見得很多,就不多舉例子了。
3.2 在子執行緒中例項化 new Handler()
在子執行緒中new Handler()
有下面兩個需要注意的點:
(1) 在 new Handler() 之前呼叫 Looper.prepare() 方法
另外一個錯誤就是我們在子執行緒當中,直接呼叫new Handler()
,那麼這時候會丟擲異常,例如下面這樣:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread() {
@Override
public void run() {
mHandler = new MyHandler();
}
}.start();
}
複製程式碼
丟擲的異常為:
原因是在Handler
的建構函式中,會去檢查當前執行緒的Looper
是否已經被初始化:
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
複製程式碼
如果沒有,那麼就會丟擲上面程式碼當中的異常,正確的做法,是需要保證我們在new Handler
之前,要保證當前執行緒的Looper
物件已經被初始化,也就是Looper
當中的下面這個方法被呼叫:
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
複製程式碼
(2) 如果希望通過該 Looper 接收訊息那麼要呼叫 Looper.loop() 方法,並且需要放在最後一句呼叫
假如我們只呼叫了prepare()
方法,僅僅只是初始化了一個執行緒的私有的變數,此時是無法通過這個Looper
構建的Handler
來接收訊息的,例如下面這樣:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread() {
@Override
public void run() {
Looper.prepare();
mHandler = new MyHandler();
}
}.start();
}
public void anrButton(View view) {
mHandler.sendEmptyMessage(0);
}
private static class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
Log.e("TAG", "handleMessageId=" + Thread.currentThread().getId());
}
}
複製程式碼
此時我們應當呼叫loop
方法讓這個迴圈佇列開始工作,並且該呼叫一定要位於最後一句,因為一旦呼叫了loop
方法,那麼它就會進入等待 -> 收到訊息 -> 喚醒 -> 處理訊息 -> 等待的迴圈過程,而這一過程只有等到loop
方法返回的時候才會結束,因此,我們初始化Handler
的語句要放在loop
方法之前,類似於下面這樣:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread() {
@Override
public void run() {
Looper.prepare();
mHandler = new MyHandler();
//最後一句再呼叫
Looper.loop();
}
}.start();
}
複製程式碼
四、小結
Handler
的機制似乎已經成為面試必問的題目,如果大家能在回答完內部的實現原理,再根據這些實現原理引申出上面的幾個應用和注意事項,可以加分不少哦~
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/