面試官還問Handler?那我要給你講個故事

來吧小兄弟,說說Handler怎么回事

Handler的相關博客太多了,隨便一搜都一大把,但是基本都是上來就貼源碼,講姿勢,短時間不太好弄明白整體的關系,和流程.

面試官,你坐好,聽聽我這個故事吹的怎么樣?

本文就以生活點餐的例子再結合源碼原理進行解析。希望對你有一點幫助。 來,咱們進入角色。

Handler,Looper,MessageQueue,Message的全程協作的關系就好比一個餐廳的整體運作關系

  • Handler好比點餐員
  • Looper好比后廚廚師長。
  • MessageQueue好比訂單打單機。
  • Message好比一桌一桌的訂單。

接下來我們回顧下我們餐廳點餐的場景,餐廳點餐分為標準點餐和特殊點餐,我們分解來看。

標準流程

    1. 首先進入一家店,通過點餐員點餐把數據提交到后廚打單機。
    1. 然后廚師長一張一張的拿起訂單,按照點餐的先后順序,交代后廚的廚師開始制作。
    1. 制作好后上菜,并標記已做好的訂單。

特殊流程

    1. 訂單為延遲訂單,比如客人要求30分鐘后人齊了再制作,這時會把該訂單按時間排序放到訂單隊列的合適位置,并通過SystemClock.uptimeMillis()定好鬧鈴。至于為什么用uptimeMillis是因為該時間是系統(tǒng)啟動開始計算的毫秒時間,不受手動調控時間的影響。
    1. 如果打單機中全是延遲訂單,則下令給后廚廚師休息,并在門口貼上免打擾的牌子(needWake),等待鬧鈴提醒,如有新的即時訂單進來并且發(fā)現有免打擾的牌子,則通過nativeWake()喚醒廚師再開始制作上菜。
    1. 但是為了提升店鋪菜品覆蓋,很多相鄰的店鋪都選擇合作經營,就是你可以混搭旁邊店的餐到本店吃,此時只需點餐員提交旁邊店的訂單即可,這樣旁邊店的廚師長就可以通過打單機取出訂單并進行制作和上菜。

總結

一家店可以有多個點餐員,但是廚師長只能有一個。打單機也只能有一個。

映射到以上場景中

  • 一家店就好比一個Thread
  • 而一個Thread中可以有多個Handler(點餐員)
  • 但是一家店只能有一個Looper(廚師長),一個MessageQueue(打單機),和多個Message(訂單)。

面試官,我差不多吹完了,你要還不信,那就不好意思了?

根據以上的例子我們類比看下源碼,充分研究下整個機制的流程,和實現原理。

Looper的工作流程

ActivityThread.main();//初始化入口
    1. Looper.prepareMainLooper(); //初始化
          Looper.prepare(false); //設置不可關閉
              Looper.sThreadLocal.set(new Looper(quitAllowed)); //跟線程綁定
                    1.1.Looper.mQueue = new MessageQueue(quitAllowed); //Looper和MessageQueue綁定
                    1.2.Looper.mThread = Thread.currentThread();
    2. Looper.loop();
        2.1.myLooper().mQueue.next(); //循環(huán)獲取MessageQueue中的消息
              nativePollOnce(); //阻塞隊列
                  native -> pollInner() //底層阻塞實現
                        native -> epoll_wait();
        2.2.Handler.dispatchMessage(msg);//消息分發(fā)

myLooper().mQueue.next()實現原理

    1. 通過myLooper().mQueue.next() 循環(huán)獲取MessageQueue中的消息,如遇到同步屏障 則優(yōu)先處理異步消息.
    1. 同步屏障即為用Message.postSyncBarrier()發(fā)送的消息,該消息的target沒有綁定Handler。在Hnandler中異步消息優(yōu)先級高于同步消息。
    1. 可通過創(chuàng)建new Handler(true)發(fā)送異步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保證UI繪制優(yōu)先執(zhí)行。

Handler.dispatchMessage(msg)實現原理

    1. 優(yōu)先回調msg.callback。
    1. 其次回調handler構造函數中的callback。
    1. 最后回調handler handleMessage()。

Hander發(fā)送消息的流程

1.Handler handler = new Handler();//初始化Handler
        1.Handler.mLooper = Looper.myLooper();//獲取當前線程Looper。
        2.Handler.mQueue = mLooper.mQueue;//獲取Looper綁定的MessageQueue對象。

2.handler.post(Runnable);//發(fā)送消息
        sendMessageDelayed(Message msg, long delayMillis);
            sendMessageAtTime(Message msg, long uptimeMillis);
                Handler.enqueueMessage();//Message.target 賦值為this。
                    Handler.mQueue.enqueueMessage();//添加消息到MessageQueue。


MessageQueue.enqueueMessage()方法實現原理

    1. 如果消息隊列被放棄,則拋出異常。
    1. 如果當前插入消息是即時消息,則將這個消息作為新的頭部元素,并將此消息的next指向舊的頭部元素,并通過needWake喚醒Looper線程。
    1. 如果消息為異步消息則通過Message.when長短插入到隊列對應位置,不喚醒Looper線程。

    接下來該面試官問了

經常有人問為什么主線程的Looper阻塞不會導致ANR?

    1. 首先我們得知道ANR是主線程5秒內沒有響應。
    1. 什么叫5秒沒有響應呢?Android系統(tǒng)中所有的操作均通過Handler添加事件到事件隊列,Looper循環(huán)去隊列去取事件進行執(zhí)行。如果主線程事件反饋超過了5秒則提示ANR。
    1. 如果沒有事件進來,基于Linux pipe/epoll機制會阻塞loop方法中的queue.next()中的nativePollOnce()不會報ANR。
    1. 對于以上的例子來說,ANR可以理解為用戶進行即時點餐后沒按時上菜(當然未按時上菜的原因很多,可能做的慢(耗時操作IO等),也可能廚具被占用(死鎖),還有可能廚師不夠多(CPU性能差)等等。。。),顧客發(fā)起了投訴,或差評。但如果約定時間還沒到,或者當前沒人點餐,是不會有差評或投訴產生的,因此也不會產生ANR。

以上的所有內容均圍繞原理,源碼,接下來我們舉幾個特殊場景的例子

  • 1.為什么子線程不能直接new Handler()?
       new Thread(new Runnable() {
           @Override
           public void run() {
              Handler handler = new Handler();
           }
       }).start();


       以上代碼會報以下下錯誤

java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
       at android.os.Handler.<init>(Handler.java:207)
       at android.os.Handler.<init>(Handler.java:119)
       at com.example.test.MainActivity$2.run(MainActivity.java:21)
       at java.lang.Thread.run(Thread.java:919)


  • 通過報錯提示 “not called Looper.prepare()” 可以看出提示沒有調用Looper.prepare(),至于為什么我們還得看下源碼
 public Handler(Callback callback, boolean async) {
        ...省略若干代碼

       //通過 Looper.myLooper()獲取了mLooper 對象,如果mLooper ==null則拋異常
        mLooper = Looper.myLooper();
        if (mLooper == null) {
             //可以看到異常就是從這報出去的
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

 public static @Nullable Looper myLooper() {
        //而myLooper()是通過sThreadLocal.get()獲取的,那sThreadLocal又是個什么鬼?
        return sThreadLocal.get();
    }

 //可以看到sThreadLocal 是一個ThreadLocal<Looper>對象,那ThreadLocal值從哪賦值的?
 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

//sThreadLocal 的值就是在這個方法里賦值的
 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));
    }

  • 通過以上的源碼注釋,完全明白了報錯的日志的意思,報錯日志提示我們沒有調用Looper.prepare()方法,而Looper.prepare()方法就是sThreadLocal賦值的位置。

那子線程怎么創(chuàng)建Handler呢?只需在new Handler()之前調用下Looper.prepare()即可。


  • 2. 為什么主線程可以直接new Handler?

  • 子線程直接new Handler會報錯,主線程為什么就不會報錯呢?主線程我也沒有調用Looper.prepare()啊?那么我們還得看下源碼了。

    //我們看下ActivityMain的入口main方法,調用了 Looper.prepareMainLooper();
    public static void main(String[] args) {
       ...
        Looper.prepareMainLooper();
        ...
    }

  //看到這一下就明白了,原來主線程在啟動的時候默認就調用了prepareMainLooper(),而在這個方法中調用了prepare()。  
 //提前將sThreadLocal 進行賦值了。
  public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }


  • 3.Handler為什么會內存泄露?

  • 首先普及下什么叫內存泄露,當一個對象不再使用本該被回收時,但另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這種情況下就產生了內存泄漏。

  • 我們舉一個Handler內存泄露的場景。

public class HandlerActivity extends AppCompatActivity {
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        handler.sendEmptyMessageDelayed(1,5000);
    }
}

  • 當以上代碼寫完后編譯器立馬會報黃并提示 “this handler should be static or leaks might occur...Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.”

大致意思就說 “由于這個處理程序被聲明為一個內部類,它可以防止外部類被垃圾回收。如果處理程序正在對主線程以外的線程使用Looper或MessageQueue,則不存在問題。如果處理程序正在使用主線程的Looper或MessageQueue,則需要修復處理程序聲明,如下所示:將處理程序聲明為靜態(tài)類;并且通過WeakReference引用外部類”。

  • 說了這么一大堆,簡單意思就是說以上這種寫法,默認會引用HandlerActivity,當HandlerActivity被finish的時候,可能Handler還在執(zhí)行不能會回收,同時由于Handler隱式引用了HandlerActivity,導致了HandlerActivity也不能被回收,所以內存泄露了。

我們來寫一種正確的寫法

public class HandlerActivity extends AppCompatActivity {
      MyHandler handler = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        handler.sendEmptyMessageDelayed(1,5000);
    }
    private static class MyHandler extends Handler{
        private WeakReference<HandlerActivity> activityWeakReference;

        public MyHandler(HandlerActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

  • 以上寫法使用了靜態(tài)內部類+弱引用的方式,其實如果在handleMessage()方法中無需訪問HandlerActivity 的成員則無需使用弱引用,只需靜態(tài)內部類即可,弱引用只是方便調用HandlerActivity 內部成員。
  • 非靜態(tài)內部類和非靜態(tài)匿名內部類中確實都持有外部類的引用, 靜態(tài)內部類中未持有外部類的引用,不影響后續(xù)的回收,因此沒有內存泄露。

4. 補充個小知識點,啥是隱式引用?

  • 其實我們寫的非靜態(tài)內部類和非靜態(tài)匿名內部類,在編譯器編譯過程中,隱式幫我們傳入了this這個參數,這也是為什么,我們平時在方法中能使用this這個關鍵字的原因,了解了隱式引用,那么為什么它會是導致內存泄漏? 這里又得說明一下,虛擬機的垃圾回收策略。
  • 垃圾回收機制:Java采用根搜索算法,當GC Roots不可達時,并且對象finalize沒有自救的情況下,才會回收。也就是說GC會收集那些不是GC roots且沒有被GC roots引用的對象,就像下邊這個圖一樣。
垃圾回收.png
  • 上圖中的對象之間的連線就是這些對象之間的引用,垃圾回收的判定條件就在這些連線上,要預防非靜態(tài)內部類的泄漏問題,就得管理好對象間的引用關系。
  • 去除隱式引用(通過靜態(tài)內部類來去除隱式引用) 手動管理對象引用(修改靜態(tài)內部類的構造方式,手動引入其外部類引用) 當內存不可用時,不執(zhí)行不可控代碼(Android可以結合智能指針,WeakReference包裹外部類實例)是解決內存泄露比較好的方式。

注意 : 不是所有內部類都建議使用靜態(tài)內部類,只有在該內部類中的生命周期不可控的情況下,建議采用靜態(tài)內部類。其他情況還是可以使用非靜態(tài)內部類的。

好了Handler的介紹到此結束了,篇幅略長,如果給你帶來了一點幫助,麻煩給個點贊。

視頻:Android程序員備戰(zhàn)2022FrameWork必問:handler原理
原文: https://juejin.cn/post/6995854886386532388

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 大家好,我是方木,一名在帝都干飯的程序員。本篇文章已經收錄到【面試官爸爸】系列,歡迎各位大兄弟捧場歡迎關注我...
    方木Rudy閱讀 240評論 0 1
  • Handler的相關博客太多了,隨便一搜都一大把,但是基本都是上來就貼源碼,講姿勢,短時間不太好弄明白整體的關系,...
    醬爆大頭菜閱讀 6,708評論 9 11
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 6,098評論 0 4
  • 公元:2019年11月28日19時42分農歷:二零一九年 十一月 初三日 戌時干支:己亥乙亥己巳甲戌當月節(jié)氣:立冬...
    石放閱讀 6,909評論 0 2
  • 年紀越大,人的反應就越遲鈍,腦子就越不好使,計劃稍有變化,就容易手忙腳亂,亂了方寸。 “玩壞了”也是如此,不但會亂...
    玩壞了閱讀 2,171評論 2 1