[譯] 探索 Android 大殺器——Handler
- 原文地址:Android Handler Internals
- 原文作者:Jag Saund
- 譯文出自:掘金翻譯計劃
- 譯者:Jamweak
- 校對者:Newt0n, 寫代碼的猴子
如果你想要讓一個 Android 應用程序反應靈敏,那麼你必須防止它的 UI 線程被阻塞。同樣地,將這些阻塞的或者計算密集型的任務轉到工作線程去執行也會提高程序的響應靈敏性。然而,這些任務的執行結果通常需要更新UI組件的顯示,但該操作只能在UI線程中去執行。有一些方法解決了 UI 線程的阻塞問題,例如阻塞隊列,共享內存以及管道技術。在 Android 中,為解決這個問題,提供了一種自有的消息傳遞機制——Handler。Handler 是 Android Framework 架構中的一個基礎組件,它實現了一種非阻塞的消息傳遞機制,在消息轉換的過程中,消息的生產者和消費者都不會阻塞。
雖然 Handler 被使用的頻率非常高,它的工作原理卻很容易被忽視。本篇文章深入地剖析 Handler 眾多內部組件的實現,它將會向您揭示 Handler 的強大之處,而不僅僅作為一個工作線程和 UI 線程通信的工具。
圖片瀏覽示例
讓我們從一個例子開始了解如何在應用中使用 Handler。設想一個 Activity 需要從網路上獲取圖片並顯示。有幾種方式來做這件事,在下面的例子中,我們創建了一個新的工作線程去執行網路請求以獲取圖片。
public class ImageFetcherActivity extends AppCompactActivity { class WorkerThread extends Thread { void fetchImage(String url) { // network logic to create and execute request handler.post(new Runnable() { @Override public void run() { imageView.setImageBitmap(image); } }); } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { // prepare the view, maybe setContentView, etc new WorkerThread().fetchImage(imageUrl); }}
另一種方法則是使用 Handler Messages 來代替 Runnable 類。
public class ImageFetcherAltActivity extends AppCompactActivity { class WorkerThread extends Thread { void fetchImage(String url) { handler.sendEmptyMessage(MSG_SHOW_LOADER); // network call to load image handler.obtainMessage(MSG_SHOW_IMAGE, imageBitmap).sendToTarget(); } } class UIHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SHOW_LOADER: { progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_HIDE_LOADER: { progressIndicator.setVisibility(View.GONE); break; } case MSG_SHOW_IMAGE: { progressIndicator.setVisibility(View.GONE); imageView.setImageBitmap((Bitmap) msg.obj); break; } } } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { // prepare the view, maybe setContentView, etc new WorkerThread().fetchImage(imageUrl); }}
在第二個例子中,工作線程從網路獲取到一張圖片,一旦下載完成,我們需用使用下載好的 bitmap 去更新 ImageView 顯示內容。我們知道不能在非 UI 線程中更新 UI 組件,因此我們使用 Handler。Handler 扮演了工作線程和 UI 線程的中間人的角色。消息在工作線程中被 Handler 加入隊列,隨後在 UI 線程中被 Handler 處理。
深入了解 Handler
Handler 由以下部分組成:
- Handler
- Message
- Message Queue
- Looper
我們接下來將學習各個組件以及他們之間的交互。
Handler
Handler[2] 是線程間傳遞消息的即時介面,生產線程和消費線程調用以下操作來使用 Handler:
- 在消息隊列中創建、插入或移除消息
- 在消費線程中處理消息
android.os.Handler 組件
每個 Handler 都有一個與之關聯的 Looper 和消息隊列。有兩種創建 Handler 的方式:
- 通過默認的構造方法,使用當前線程中關聯的 Looper
- 顯式地指定使用的 Looper
沒有指定 Looper 的 Handler 是無法工作的,因為它無法將消息放到消息隊列中。同樣地,它無法獲取要處理的消息。
public Handler(Callback callback, boolean async) { // code removed for simplicity 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;}
上面的代碼段展示了創建一個新的 Handler 的邏輯。Handler 在創建時檢查了當前的線程有沒有可用的 Looper 對象,如果沒有,它會拋出一個運行時的異常。如果正常的話,Handler 則會持有 Looper 中消息隊列對象的引用。
注意:同一線程中的多個 Handler 分享一個同樣的消息隊列,因為他們分享的是同一個 Looper 對象。
Callback 參數是一個可選參數,如果提供的話,它將會處理由 Looper 分發過來的消息。
Message
Message[3] 是容納任意數據的容器。生產線程發送消息給 Handler,Handler 將消息加入到消息隊列中。消息提供了三種額外的信息,以供 Handler 和消息隊列處理時使用:
- what?——一種標識符,Handler 能使用它來區分不同消息,從而採取不同的處理方法
- time?——告知消息隊列何時處理消息
- target?——?表示哪一個 Handler 應當處理消息
消息一般是通過 Handler 中以下方法來創建的:
public final Message obtainMessage()public final Message obtainMessage(int what)public final Message obtainMessage(int what, Object obj)public final Message obtainMessage(int what, int arg1, int arg2)public final Message obtainMessage(int what, int arg1, int arg2, Object obj)
消息從消息池中獲取得到,方法中提供的參數會放到消息體的對應欄位中。Handler 同樣可以設置消息的目標為其自身,這允許我們可以進行鏈式調用,比如:
mHandler.obtainMessage(MSG_SHOW_IMAGE, mBitmap).sendToTarget();
消息池是一個消息體對象的 LinkedList 集合,它的最大長度是 50。在 Handler 處理完這條消息之後,消息隊列把這個對象返回到消息池中,並且重置其所有欄位。
當使用 Handler 調用 post 方法來執行一個 Runnable 時,Handler 隱式地創建了一個新的消息,並且設置 callback 參數來存儲這個 Runnable。
Message m = Message.obtain();m.callback = r;
在上圖中,我們能看到生產線程和 Handler 的交互。生產者創建了一個消息,並且發送給了 Handler,隨後 Handler 將這個消息加入消息隊列中,在未來的某個時間,Handler 會在消費線程中處理這個消息。
Message Queue
Message Queue[4] 是一個消息體對象的無界的 LinkedList 集合。它按時序將消息插入隊列,最小的時間戳將會被首先處理。
android.os.MessageQueue 組件
消息隊列也通過 SystemClock.uptimeMillis 獲取當前時間,維護著一個阻塞閾值(dispatch barrier)。當一個消息體的時間戳低於這個值的時候,消息就會被分發給 Handler 進行處理。
Handler 提供了三種方式來發送消息:
public final boolean sendMessageDelayed(Message msg, long delayMillis)public final boolean sendMessageAtFrontOfQueue(Message msg)public boolean sendMessageAtTime(Message msg, long uptimeMillis)
以延遲的方式發送消息,是設置了消息體的 time 欄位為 SystemClock.uptimeMillis() + delayMillis 。
延遲發送的消息設置了其時間欄位為 SystemClock.uptimeMillis() + delayMillis。然而,通過 sendMessageAtFrontOfQueue() 方法把消息插入到隊首,會將其時間欄位設置為 0,消息會在下一次輪詢時被處理。需要謹慎使用這個方法,因為它可能會影響消息隊列,造成順序問題,或是其它不可預料的副作用。
Handler 常與一些 UI 組件相關聯,而這些 UI 組件通常持有對 Activity 的引用。Handler 持有的對這些組件的引用可能會導致潛在的 Activity 泄露。考慮如下場景:
public class MainActivity extends AppCompatActivity { private static final String IMAGE_URL = "https://www.android.com/static/img/android.png"; private static final int MSG_SHOW_PROGRESS = 1; private static final int MSG_SHOW_IMAGE = 2; private ProgressBar progressIndicator; private ImageView imageView; private Handler handler; class ImageFetcher implements Runnable { final String imageUrl; ImageFetcher(String imageUrl) { this.imageUrl = imageUrl; } @Override public void run() { handler.obtainMessage(MSG_SHOW_PROGRESS).sendToTarget(); InputStream is = null; try { // Download image over the network URL url = new URL(imageUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setDoInput(true); conn.connect(); is = conn.getInputStream(); // Decode the byte payload into a bitmap final Bitmap bitmap = BitmapFactory.decodeStream(is); handler.obtainMessage(MSG_SHOW_IMAGE, bitmap).sendToTarget(); } catch (IOException ignore) { } finally { if (is != null) { try { is.close(); } catch (IOException ignore) { } } } } } class UIHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SHOW_PROGRESS: { imageView.setVisibility(View.GONE); progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_SHOW_IMAGE: { progressIndicator.setVisibility(View.GONE); imageView.setVisibility(View.VISIBLE); imageView.setImageBitmap((Bitmap) msg.obj); break; } } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); progressIndicator = (ProgressBar) findViewById(R.id.progress); imageView = (ImageView) findViewById(R.id.image); handler = new UIHandler(); final Thread workerThread = new Thread(new ImageFetcher(IMAGE_URL)); workerThread.start(); }}
在這個例子中,Activity 開啟了一個新的工作線程去下載並且在 ImageView 中展示圖片。工作線程通過 UIHandler 去通知 UI 更新,這樣就會持有了對 View 的引用,以便更新這些 View 的狀態(切換可見性、設置圖片等)。
讓我們假設工作線程由於網路差,需要很長的時間去下載圖片。在工作線程下載完成之前銷毀這個 Activity 會導致 Activity 泄露。在本例中,有兩個強引用關係,一個在工作線程和 UIHandler 之間,另一個在 UIHandler 和 View 之間。這就阻止了垃圾回收機制回收 Activity 的引用。
現在,讓我們來看看另一個例子:
public class MainActivity extends AppCompatActivity { private static final String TAG = "Ping"; private Handler handler; class PingHandler extends Handler { @Override public void handleMessage(Message msg) { Log.d(TAG, "Ping message received"); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); handler = new PingHandler(); final Message msg = handler.obtainMessage(); handler.sendEmptyMessageDelayed(0, TimeUnit.MINUTES.toMillis(1)); }}
在這個例子中,將按順序發生如下事件:
- PingHandler 被創建
- Activity 發送了一個帶延遲的消息給 Handler,隨後消息加入到消息隊列中
- Activity 在消息到達之前被銷毀
- 消息被分發,並被 UIHandler 處理,輸出一條日誌
雖然起初看起來不那麼明顯,但本例中的 Activity 也存在著泄露。
在銷毀 Activity 之後,Handler 應當可以被垃圾回收,然而當創建了一個消息對象之後,它也會持有對 Handler 的引用:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis);}
上面的 Android 代碼段表明,所有被發送到 Handler 的消息最終都會觸發 enqueueMessage 方法。注意到 Handler 的引用被顯式地賦給了 msg.target,以此來告訴 Looper 對象當消息從消息隊列出隊時,選擇哪一個 Handler 來對其進行處理。
消息加入消息隊列後,消息隊列就獲得了對消息的引用。它同樣有一個與之關聯的 Looper。一個自定義的 Looper 對象的生命周期一直持續到它被結束,然而主線程中的 Looper 對象在程序的生命周期內一直存在。因此,消息中持有的對 Handler 的引用會一直維持到該消息被消息隊列回收之前,一旦消息被回收,它內部的各欄位,包括目標 target 的引用都會被清空。
雖然 Handler 能存活很長時間,但是當 Activity 發生泄露時,Handler 不會被清空。為了檢查是否發生泄露,我們必須檢查 Handler 是否在本類範圍內持有 Activity 的引用。在本例中,它確實持有:非靜態內部類持有一個對其外部類的隱式引用。明確一點來說,PingHandler 沒有定義成一個靜態類,所以它持有一個隱式的 Activity 引用。
通過結合使用弱引用和靜態類修飾符可以阻止 Handler 導致的 Activity 泄露。當 Activity 被銷毀時,弱引用允許垃圾回收器去回收你想要留存的對象(通常來說是 Activity)。在 Handler 內部類前加入靜態修飾符可以阻止對外部類持有隱式引用。
讓我們來修改上例中的 UIHandler 來解決這個煩惱:
static class UIHandler extends Handler { private final WeakReference<ImageFetcherActivity> mActivityRef; UIHandler(ImageFetcherActivity activity) { mActivityRef = new WeakReference(activity); } @Override public void handleMessage(Message msg) { final ImageFetcherActivity activity = mActivityRef.get(); if (activity == null) { return } switch (msg.what) { case MSG_SHOW_LOADER: { activity.progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_HIDE_LOADER: { activity.progressIndicator.setVisibility(View.GONE); break; } case MSG_SHOW_IMAGE: { activity.progressIndicator.setVisibility(View.GONE); activity.imageView.setImageBitmap((Bitmap) msg.obj); break; } } }}
現在,UIHandler 的構造方法中需要傳入 Activity,而這個引用會被弱引用包裝。這樣就允許垃圾回收器在 Activity 銷毀時回收這個引用。當與 Activity 中的 UI 組件交互時,我們需要從 mActivityRef 中獲得一個 Activity 的強引用。由於我們正在使用一個弱引用,我們必須小心翼翼地去訪問 Activity。如果僅僅能通過弱引用的方式去訪問 Activity,垃圾回收器也許已經將其回收了,因此我們需要檢查回收是否發生。如果確實被回收,Handler 實際上已經與 Activity 無關了,那麼這條消息就應該被丟棄。
雖然這個邏輯解決了內存泄露問題,但仍舊存在一個問題。Activity 已經被銷毀,但垃圾回收器還沒來得及回收引用,依賴於操作系統運行時的狀況,這可能會使你的程序導致潛在的崩潰。為解決這個問題,我們需要獲取 Activity 當前的狀態。
讓我們更新 UIHandler 的邏輯來解決如上場景的問題:
static class UIHandler extends Handler { private final WeakReference<ImageFetcherActivity> mActivityRef; UIHandler(ImageFetcherActivity activity) { mActivityRef = new WeakReference(activity); } @Override public void handleMessage(Message msg) { final ImageFetcherActivity activity = mActivityRef.get(); if (activity == null || activity.isFinishing() || activity.isDestroyed()) { removeCallbacksAndMessages(null); return } switch (msg.what) { case MSG_SHOW_LOADER: { activity.progressIndicator.setVisibility(View.VISIBLE); break; } case MSG_HIDE_LOADER: { activity.progressIndicator.setVisibility(View.GONE); break; } case MSG_SHOW_IMAGE: { activity.progressIndicator.setVisibility(View.GONE); activity.imageView.setImageBitmap((Bitmap) msg.obj); break; } } }}
現在,我們可以概括消息隊列、Handler、生產線程的交互:
消息隊列、Handler、生產線程的交互
在上圖中,多個生產線程提交消息到不同的 Handler 中。然而,不同的 Handler 都與同一個 Looper 對象關聯,因此所有的消息都加入到同一個消息隊列中。這一點非常重要,Android 中創建的許多不同 Handler 都關聯到主線程的 Looper:
- The Choreographer: 處理垂直同步與幀更新
- The ViewRoot: 處理輸入和窗口事件,配置修改等等
- The InputMethodManager: 處理鍵盤觸摸事件及其它
小貼士:確保生產線程不會大量生成消息,因為這可能會抑制處理系統生成消息。
主線程 Looper 分發消息的小示例調試幫助: 你可以通過附加一個 LogPrinter 到 Looper 上來 debug/dump 被 Looper 分發的消息:
final Looper looper = getMainLooper();looper.setMessageLogging(new LogPrinter(Log.DEBUG, "Looper"));
同樣地,你可以 debug/dump 所有在消息隊列中等待的消息,通過在與消息隊列相關聯的 Handler 上附加一個 LogPrinter 來實現:
handler.dump(new LogPrinter(Log.DEBUG, "Handler"), "");
Looper
Looper[5] 從消息隊列中讀取消息,然後分發給對應的 Handler 處理。一旦消息超過阻塞閾,那麼 Looper 就會在下一輪讀取過程中讀取到它。Looper 在沒有消息分發的時候會變為阻塞狀態,當有消息可用時會繼續輪詢。
每個線程只能關聯一個 Looper,給線程附加另外的 Looper 會導致運行時的異常。通過使用 Looper 類中的 ThreadLocal 對象可以保證每個線程只關聯一個 Looper 對象。
調用 Looper.quit() 方法會立即終止 Looper,並且會丟棄消息隊列中已經通過阻塞閾的所有消息。調用 Looper.quitSafely() 方法能夠保證所有待分發的消息在列隊中等待的消息被丟棄前得到處理。
Handler 與消息隊列和 Looper 直接交互的整體流程
Looper 應在線程的 run 方法中初始化。調用靜態方法 Looper.prepare() 會檢查線程是否與一個已存在的 Looper 關聯。這個過程的實現是通過 Looper 類中的 ThreadLocal 對象來檢查 Looper 對象是否存在。如果 Looper 不存在,將會創建一個新的 Looper 對象和一個新的消息隊列。Android 代碼 中的如下片段展示了這個過程。
注意:公有的 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));}
Handler 現在能接收到消息並加入消息隊列中,執行靜態方法 Looper.loop() 方法會開始將消息從隊列中出隊。每次輪詢迭代器指向下一條消息,接著分發消息到對應目標的 Handler,然後回收消息到消息池中。Looper.loop() 方法會循環執行這個過程,直到 Looper 終止。 Android 代碼 中的如下片段展示了這個過程:
public static void loop() { if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasnt called on this thread."); } final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } msg.target.dispatchMessage(msg); msg.recycleUnchecked(); }}
並沒有必要自己去創建關聯 Looper 的線程。Android 提供了一個簡便的類做這件事——HandlerThread。它繼承 Thread 類,並且提供對 Looper 創建的管理。下面的代碼描述了它的一般使用過程:
private final Handler handler;private final HandlerThread handlerThread;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(); handlerThread = new HandlerThread("HandlerDemo"); handlerThread.start(); handler = new CustomHandler(handlerThread.getLooper());}@Overrideprotected void onDestroy() { super.onDestroy(); handlerThread.quit();}
onCreate() 方法構造了一個 HandlerThread,當 HandlerThread 啟動後,它準備創建 Looper 與它的線程關聯,隨後 Looper 開始處理 HandlerThread 的消息隊列中的消息。
注意:當 Activity 被銷毀時,結束 HandlerThread 是很重要的,這個動作也會終止關聯的 Looper。
總結
Android 中的 Handler 在應用的生命周期中扮演著不可缺少的角色。它是構成半同步/半非同步模式架構的基礎。許多內部和外部的代碼都依賴 Handler 去非同步地分發事件,它能以最小的代價去維持線程安全。
更深入地理解組件的工作方式能夠幫助解決疑難雜症。這也能讓我們以最佳的方法使用組件的 API。我們通常將 Handler 作為工作線程和UI線程間的通信機制,但 Handler 並不僅限於此。它出現在 IntentService[6], 和 Camera2[7] 和許多其它的 API 中。在這些 API 調用中,Handler 更多情形下是被用作任意線程間的通信工具。
在深入理解了 Handler 的原理後,我們能運用其構建更有效率、更簡潔、更健壯的應用程序。
推薦閱讀:
※如何評價近日曝光的 Oxygen OS 中 Weather App 的設計風格?
※Android開發如何進階?
※手機跑分的實踐性是否有如一些廠商所吹的那麼高?
※如何評價R11s開始測試基於Android 8.1的系統,並支持Project Treble?