進階之路 | 奇妙的Handler之旅

前言

本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:java

個人GIthub博客git

須要已經具有的知識:

  • Handler的基本概念及使用

學習導圖:

學習導圖

一.爲何要學習Handler?

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

在多線程的應用場景中,Handler將工做線程中需更新UI的操做信息 傳遞到 UI主線程,從而實現工做線程對UI的更新處理,最終實現異步消息的處理。面試

做爲一個Android程序猿,知其然而必須知其因此然,理解其源碼能更好地瞭解Handler機制的原理。下面,我就從消息機制入手,帶你們暢遊在Handler的世界中,體會Google工程師的智慧之光。c#

二.核心知識點概括

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:爲何發送消息在子線程,而處理消息就變成主線程了,在哪兒跳轉的


若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力

本文參考連接:

相關文章
相關標籤/搜索