進階之路 | 奇妙的Handler之旅

前言

本文已經收錄到我的Github個人部落格,歡迎大佬們光臨寒舍:

我的GIthub部落格

需要已經具備的知識:

  • Handler的基本概念及使用

學習導圖:

學習導圖

一.為什麼要學習Handler?

Android平台上,主要用到的通訊機制有兩種:HandlerBinder,前者用於進程內部的通訊,後者主要用於跨進程通訊。

在多執行緒的應用場景中,Handler將工作執行緒中需更新UI的操作資訊 傳遞到 UI主執行緒,從而實現工作執行緒對UI的更新處理,最終實現非同步消息的處理。

作為一個Android程式猿,知其然而必須知其所以然,理解其源碼能更好地了解Handler機制的原理。下面,我就從消息機制入手,帶大家暢遊在Handler的世界中,體會Google工程師的智慧之光。

二.核心知識點歸納

2.1 消息機制概述

A.作用:跨執行緒通訊

B.常用場景:當子執行緒中進行耗時操作後需要更新UI時,通過Handler將有關UI的操作切換到主執行緒中執行

  • 系統不建議在子執行緒訪問UI的原因:UI控制項非執行緒安全,在多執行緒中並發訪問可能會導致UI控制項處於不可預期的狀態

  • 而不對UI控制項的訪問加上機制的原因有:

    1.上鎖會讓UI控制項變得複雜和低效

    2.上鎖後會阻塞某些進程的執行

C.四要素:

  • Message:需要被傳遞的消息,其中包含了消息ID,消息處理對象以及處理的數據等,由MessageQueue統一列隊,最終由Handler處理
  • MessageQueue:用來存放Handler發送過來的消息,內部通過單鏈表的數據結構來維護消息列表,等待Looper的抽取。
  • Handler:負責Message的發送及處理
  • Handler.sendMessage():向消息隊列發送各種消息事件
  • Handler.handleMessage()處理相應的消息事件
  • Looper:通過Looper.loop()不斷地從MessageQueue中抽取Message,按分發機制將消息分發給目標處理者,可以看成是消息泵

Thread:負責調度整個消息循環,即消息循環的執行場所

存在關係:

  • 一個Thread只能有Looper,可以有Handler
  • LooperMessageQueue,可以處理來自HandlerMessage
  • MessageQueue有一組待處理的Message,這些Message可來自不同的Handler
  • Message中記錄了負責發送和處理消息的Handler
  • Handler中有LooperMessageQueue

關係圖

數量關係

D.使用方法:

  • ActivityThread主執行緒實例化一個全局的Handler對象
  • 在需要執行UI操作的子執行緒里實例化一個Message並填充必要數據,調用Handler.sendMessage(Message)方法發送出去
  • 重寫handleMessage()方法,對不同Message執行相關操作

E.總體工作流程:

這裡先總體地說明一下Android消息機制的工作流程,具體的ThreadLocal,MessageQueue,Looper,Handler的工作原理會在下文詳細解析

  • Handler.sendMessage()發送消息時,會通過MessageQueue.enqueueMessage()MessageQueue中添加一條消息
  • 通過Looper.loop()開啟循環後,不斷輪詢調用MessageQueue.next()
  • 調用目標Handler.dispatchMessage()去傳遞消息,目標Handler收到消息後調用Handler.handleMessage()處理消息

簡單來看,即HandlerMessage發送到Looper的成員變數MessageQueue中,之後Looper不斷循環遍歷MessageQueue從中讀取Message,最終回調給Handler處理。如圖:

總體工作流程

2.2 消息機制分析

2.2.1 ThreadLocal

了解ThreadLocal,有助於我們後面對Looper的探究

Q1:ThreadLocal是什麼

首先我們來看一下官方源碼(Android 9.0

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

大致意思:

ThreadLocal是一個執行緒內部的數據存儲類,通過它可以在指定的執行緒中存儲數據,只有在指定執行緒中才能獲取到存儲的數據(也就是說,每個執行緒的一個變數,有自己的值)

Q2:ThreadLocal使用場景

  • 當某些數據是以執行緒為作用域且每個執行緒特有數據副本
  • Android中具體的使用場景:Looper,ActivityThread,AMS

  • 如果不採用ThreadLocal的話,需要採取的措施:提供一個全局哈希表

  • 複雜邏輯下的對象傳遞,比如:監聽器的傳遞
  • 採用ThreadLocal讓監聽器作為執行緒中的全局對象,執行緒內部只有通過get方法即可得到監聽器

  • 如果不採用ThreadLocal的方案:

    a.將監聽器作為參數傳遞

    缺點:當調用棧很深的時候,程式設計看起來不美觀

    b.將監聽器作為靜態變數

    缺點:狀態不具有可擴充性

Q3:ThreadLocalsynchronized的區別:

  • 對於多執行緒資源共享的問題,synchronized機制採用了「以時間換空間」的方式
  • ThreadLocal採用了「以空間換時間」的方式
  • 前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響,所以ThreadLocalsynchronized都能保證執行緒安全,但是應用場景卻大不一樣。

Q4:原理

ThreadLocal主要操作為set,get操作,下面分別介紹流程

A1:set的原理

set流程圖

A2:get的原理

get流程圖

綜上所述,ThreadLocal之所以有這麼奇妙的效果,是因為:

  • 不同執行緒訪問同一個ThreadLocal.get(),其內部會從各種執行緒中取出table數組,然後根據當前ThreadLocal的索引查找出對應的values

想要了解ThreadLocal源碼的讀者,推薦一篇文章:ThreadLocal詳解

2.2.2 MessageQueue

  • 數據結構:MessageQueue的數據結構是單鏈表

  • 操作:

    A.enqueueMessage

    主要操作是單鏈表的插入操作

    B.next

    是一個無限循環的方法,如果沒有消息,會一直阻塞;當有消息的時候,next會返回消息並將其從單鏈表中移出

2.2.3 Looper

Q1:Looper的作用

  • 作為消息循環的角色
  • 它會不停地從MessageQueue中查看是否有新消息,若有新消息則立即處理,否則一直阻塞(不是ANR
  • Handler需要Looper,否則將報錯

Q2:Looper的使用

a1:開啟:

UI執行緒會自動創建Looper,子執行緒需自行創建

//子執行緒中需要自己創建一個Looper  new Thread(new Runnable() {              @Override              public void run() {                  Looper.prepare();//為子執行緒創建Looper                  Handler handler = new Handler();                  Looper.loop(); //開啟消息輪詢              }          }).start();
  • 除了prepare(),還提供prepareMainLooper(),本質也是通過prepare()
  • getMainLooper() 作用:獲取主執行緒的Looper

a2:關閉:

  • quit:直接退出
  • quitSafely:設定退出標記,待MessageQueue中處理完所有消息再退出

退出Looper的話,子執行緒會立刻終止;因此:建議在不需要的時候終止Looper

Q3:原理:

Looper原理

2.2.4 Handler

Q1:Handler的兩種使用方式:

注意:創建Handler實例之前必須先創建Looper實例,否則會拋RuntimeException(UI執行緒自動創建Looper)

  • send方式
//第一種:send方式的Handler創建  Handler mHandler = new Handler() {              @Override              public void handleMessage(Message msg) {                  //如UI操作                }          };  //send  mHandler.sendEmptyMessage(0);
  • post方式

最終是通過一系列send方法來實現

//實例化Handler  private Handler mHandler = new Handler();  //這裡調用了post方法,和sendMessage一樣達到了更新UI的目的       mHandler.post(new Runnable() {              @Override              public void run() {                  mTextView.setText(new_str);              }          });

Q2:Handler處理消息過程

Handler發送消息流程

一張圖總結Handler

2.3 Handler 的延伸

2.3.1 記憶體泄露

在初學Handler的時候,往往會發現AS亮起一大塊黃色,以警告可能會發生記憶體泄漏

Handler警告

  • 發生場景:Handler 允許我們發送延時消息,如果在延時期間用戶關閉了Activity,那麼該Activity會泄露

  • 原因:這個泄露是因為因為 Java 的特性,內部類會持有外部類Handler 持有 Activity 的引用,Message持有Handler的引用,而MessageQueue會持有Message的引用,而MessageQueue是屬於TLS(ThreadLocalStorage)執行緒,是與Activity不同的生命周期。所以當Activity的生命周期結束後,而MessageQueue中還存在未處理的消息,那麼上面一連串的引用鏈就不允許Activity的對象被回收,就造成了記憶體泄漏

  • 解決方式:

    A.Activity銷毀時,清空Handler中未執行或正在執行的Callback以及Message

    // 清空消息隊列,移除對外部類的引用      @Override      protected void onDestroy() {          super.onDestroy();          mHandler.removeCallbacksAndMessages(null);      }

​ B.靜態內部類+弱引用

  private static class AppHandler extends Handler {      //弱引用,在垃圾回收時,被回收      WeakReference<Activity> mActivityReference;        AppHandler(Activity activity){          mActivityReference=new WeakReference<Activity>(activity);      }        public void handleMessage(Message message){          switch (message.what){               HandlerActivity activity=mActivityReference.get();               super.handleMessage(message);              if(activity!=null){                  //執行業務邏輯                }          }      }  }

Java各種引用

2.3.2 Handler里藏著的Callback

首先看下Handler.dispatchMessage(msg)

public void dispatchMessage(Message msg) {    //這裡的 callback 是 Runnable    if (msg.callback != null) {      handleCallback(msg);    } else {      //如果 callback 處理了該 msg 並且返回 true, 就不會再回調 handleMessage      if (mCallback != null) {        if (mCallback.handleMessage(msg)) {          return;        }      }      handleMessage(msg);    }  }

可以看到 Handler.Callback優先處理消息的權利

  • 當一條消息被 Callback 處理並攔截(返回 true,那麼 Handler.handleMessage(Msg) 方法就不會被調用了
  • 如果Callback處理了消息,但是並沒有攔截,那麼就意味著一個消息可以同時被Callback以及 Handler 處理

這個就很有意思了,這有什麼作用呢?

我們可以利用 Callback 這個攔截機制來攔截 Handler 的消息!

場景:Hook ActivityThread.mH ,筆者在進階之路 | 奇妙的四大組件之旅介紹過ActivityThread,在 ActivityThread中有個成員變數 mH ,它是個 Handler,又是個極其重要的類,幾乎所有的插件化框架都使用了這個方法

限於當前知識水平,筆者尚未研究過插件化的知識,以後有機會的話希望能給大家介紹!

2.3.3 創建 Message 的最佳方式

為了節省開銷,盡量復用 Message ,減少記憶體消耗

法一:Message msg=Message.obtain();

法二:Message msg=handler.obtainMessage();

2.3.4 妙用 Looper 機制

我們可以利用Looper的機制來幫助我們做一些事情:

  • Runnable post 到主執行緒執行
  • 利用 Looper 判斷當前執行緒是否是主執行緒
public final class MainThread {        private MainThread() {      }        private static final Handler HANDLER = new Handler(Looper.getMainLooper());        //將 Runnable post 到主執行緒執行      public static void run(@NonNull Runnable runnable) {          if (isMainThread()) {              runnable.run();          }else{              HANDLER.post(runnable);          }      }        //判斷當前執行緒是否是主執行緒      public static boolean isMainThread() {          return Looper.myLooper() == Looper.getMainLooper();      }    }  

2.3.5 Android中為什麼主執行緒不會因Looper.loop()的死循環卡死?

這個是老生常談的問題了,記得當初被學長問到這個問題的時候,一臉懵逼,然後胡說一通,實屬羞愧

要弄清這個問題,我們可以通過幾個問題來逐層深入剖析

Q1:什麼是執行緒?

執行緒是一段可執行的程式碼,當可執行程式碼執行完成後,執行緒生命周期便該終止了,執行緒退出

Q2:進入死循環是不是說明一定會阻塞

前面也說到了執行緒既然是一段可執行的程式碼,當可執行程式碼執行完成後,執行緒生命周期便該終止了,執行緒退出。而對於主執行緒,我們是絕不希望會被運行一段時間,自己就退出,那麼如何保證能一直存活呢?簡單做法就是可執行程式碼是能一直執行下去的,死循環便能保證不會被退出

想到這就理解,主執行緒也是一個執行緒,它也要維持自己的周期,所以也是需要一個死循環的。所以死循環並不是那麼讓人擔心。

Q3:什麼是Looper的阻塞?

  • Looper的阻塞,前提是沒有輸入事件,此時MessageQueue是空的,Looper進入空閑,執行緒進入阻塞,釋放CPU,等待輸入事件的喚醒
  • Looper阻塞的時候,主執行緒大多數時候都是處於休眠狀態,並不會消耗大量CPU資源

Looper的阻塞涉及到Linux pipe/epoll機制,想了解的讀者可自行Google

Q4:聊聊ANR

  • 其實初學者很容易將ANRLooper的阻塞二者相混淆
  • UI耗時導致卡死,前提是要有輸入事件,此時MessageQueue不是空的,Looper正常輪詢,執行緒並沒有阻塞,但是該事件執行時間過長(一般5秒),而且與此期間其他的事件(按鍵按下,螢幕點擊..也是通過Looper處理的)都沒辦法處理(卡死),然後就ANR異常了

Q5:卡死的真正原因:

  • 真正卡死的原因是:在回調方法onCreate/onStart/onResume等操作時間過長

三.課堂小測試

恭喜你!已經看完了前面的文章,相信你對Handler已經有一定深度的了解,下面,進行一下課堂小測試,驗證一下自己的學習成果吧!PS:限於篇幅,筆者就不提供答案了,不過答案一搜就有了

Q1:如何將一個Thread執行緒變成Looper執行緒?Looper執行緒有哪些特點

Q2:簡述下HandlerMessageLooper的作用,以及他們之間的關係

Q3: 簡述消息機制的回調處理過程,怎麼保證消息處理機制的唯一性

Q4:為什麼發送消息在子執行緒,而處理消息就變成主執行緒了,在哪兒跳轉的


如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力

本文參考鏈接: