5分鐘了解Handler機(jī)制,Handler的錯(cuò)誤使用場(chǎng)景

Handler的相關(guān)博客太多了,隨便一搜都一大把,但是基本都是上來就貼源碼,講姿勢(shì),短時(shí)間不太好弄明白整體的關(guān)系,和流程,本文就以生活點(diǎn)餐的例子再結(jié)合源碼原理進(jìn)行解析。希望對(duì)你有一點(diǎn)幫助。

來,咱們進(jìn)入角色。

Hander,Looper,MessageQueue,Message的全程協(xié)作的關(guān)系就好比一個(gè)餐廳的整體運(yùn)作關(guān)系

Handler好比點(diǎn)餐員
Looper好比后廚廚師長(zhǎng)。
MessageQueue好比訂單打單機(jī)。
Message好比一桌一桌的訂單。


接下來我們回顧下我們餐廳點(diǎn)餐的場(chǎng)景,餐廳點(diǎn)餐分為標(biāo)準(zhǔn)點(diǎn)餐和特殊點(diǎn)餐,我們分解來看。

標(biāo)準(zhǔn)流程

    1. 首先進(jìn)入一家店,通過點(diǎn)餐員點(diǎn)餐把數(shù)據(jù)提交到后廚打單機(jī)。
    1. 然后廚師長(zhǎng)一張一張的拿起訂單,按照點(diǎn)餐的先后順序,交代后廚的廚師開始制作。
    1. 制作好后上菜,并標(biāo)記已做好的訂單。

特殊流程

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

總結(jié)

一家店可以有多個(gè)點(diǎn)餐員,但是廚師長(zhǎng)只能有一個(gè)。打單機(jī)也只能有一個(gè)。

映射到以上場(chǎng)景中,一家店就好比一個(gè)Thread,而一個(gè)Thread中可以有多個(gè)Handler(點(diǎn)餐員),但只能有一個(gè)Looper(廚師長(zhǎng)),一個(gè)MessageQueue(打單機(jī)),和多個(gè)Message(訂單)。


根據(jù)以上的例子我們類比看下源碼,充分研究下整個(gè)機(jī)制的流程,和實(shí)現(xiàn)原理。

Looper的工作流程

ActivityThread.main();//初始化入口
    1. Looper.prepareMainLooper(); //初始化
          Looper.prepare(false); //設(shè)置不可關(guān)閉
              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(); //阻塞隊(duì)列
                  native -> pollInner() //底層阻塞實(shí)現(xiàn)
                        native -> epoll_wait();
        2.2.Handler.dispatchMessage(msg);//消息分發(fā)

myLooper().mQueue.next()實(shí)現(xiàn)原理

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

Handler.dispatchMessage(msg)實(shí)現(xiàn)原理

    1. 優(yōu)先回調(diào)msg.callback。
    1. 其次回調(diào)handler構(gòu)造函數(shù)中的callback。
    1. 最后回調(diào)handler handleMessage()。

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

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

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()方法實(shí)現(xiàn)原理

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

經(jīng)常有人問為什么主線程的Looper阻塞不會(huì)導(dǎo)致ANR?

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

以上的所有內(nèi)容均圍繞原理,源碼,接下來我們舉幾個(gè)特殊場(chǎng)景的例子

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


       以上代碼會(huì)報(bào)以下下錯(cuò)誤

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)

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

       //通過 Looper.myLooper()獲取了mLooper 對(duì)象,如果mLooper ==null則拋異常
        mLooper = Looper.myLooper();
        if (mLooper == null) {
             //可以看到異常就是從這報(bào)出去的
            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又是個(gè)什么鬼?
        return sThreadLocal.get();
    }

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

//sThreadLocal 的值就是在這個(gè)方法里賦值的
 private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //具體的賦值點(diǎn)
        sThreadLocal.set(new Looper(quitAllowed));
    }
  • 通過以上的源碼注釋,完全明白了報(bào)錯(cuò)的日志的意思,報(bào)錯(cuò)日志提示我們沒有調(diào)用Looper.prepare()方法,而Looper.prepare()方法就是sThreadLocal賦值的位置。
    那子線程怎么創(chuàng)建Handler呢?只需在new Handler()之前調(diào)用下Looper.prepare()即可。

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

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

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

  //看到這一下就明白了,原來主線程在啟動(dòng)的時(shí)候默認(rèn)就調(diào)用了prepareMainLooper(),而在這個(gè)方法中調(diào)用了prepare()。  
 //提前將sThreadLocal 進(jìn)行賦值了。
  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為什么會(huì)內(nèi)存泄露?

  • 首先普及下什么叫內(nèi)存泄露,當(dāng)一個(gè)對(duì)象不再使用本該被回收時(shí),但另外一個(gè)正在使用的對(duì)象持有它的引用從而導(dǎo)致它不能被回收,這導(dǎo)致本該被回收的對(duì)象不能被回收而停留在堆內(nèi)存中,這種情況下就產(chǎn)生了內(nèi)存泄漏。

  • 我們舉一個(gè)Handler內(nèi)存泄露的場(chǎng)景。

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);
    }
}
  • 當(dāng)以上代碼寫完后編譯器立馬會(huì)報(bào)黃并提示 “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.”
    大致意思就說 “由于這個(gè)處理程序被聲明為一個(gè)內(nèi)部類,它可以防止外部類被垃圾回收。如果處理程序正在對(duì)主線程以外的線程使用Looper或MessageQueue,則不存在問題。如果處理程序正在使用主線程的Looper或MessageQueue,則需要修復(fù)處理程序聲明,如下所示:將處理程序聲明為靜態(tài)類;并且通過WeakReference引用外部類”。

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

我們來寫一種正確的寫法

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)內(nèi)部類+弱引用的方式,其實(shí)如果在handleMessage()方法中無需訪問HandlerActivity 的成員則無需使用弱引用,只需靜態(tài)內(nèi)部類即可,弱引用只是方便調(diào)用HandlerActivity 內(nèi)部成員。
  • 非靜態(tài)內(nèi)部類和非靜態(tài)匿名內(nèi)部類中確實(shí)都持有外部類的引用, 靜態(tài)內(nèi)部類中未持有外部類的引用,不影響后續(xù)的回收,因此沒有內(nèi)存泄露。

4. 補(bǔ)充個(gè)小知識(shí)點(diǎn),啥是隱式引用?

  • 其實(shí)我們寫的非靜態(tài)內(nèi)部類和非靜態(tài)匿名內(nèi)部類,在編譯器編譯過程中,隱式幫我們傳入了this這個(gè)參數(shù),這也是為什么,我們平時(shí)在方法中能使用this這個(gè)關(guān)鍵字的原因,了解了隱式引用,那么為什么它會(huì)是導(dǎo)致內(nèi)存泄漏? 這里又得說明一下,虛擬機(jī)的垃圾回收策略。
  • 垃圾回收機(jī)制:Java采用根搜索算法,當(dāng)GC Roots不可達(dá)時(shí),并且對(duì)象finalize沒有自救的情況下,才會(huì)回收。也就是說GC會(huì)收集那些不是GC roots且沒有被GC roots引用的對(duì)象,就像下邊這個(gè)圖一樣。


    垃圾回收.png
  • 上圖中的對(duì)象之間的連線就是這些對(duì)象之間的引用,垃圾回收的判定條件就在這些連線上,要預(yù)防非靜態(tài)內(nèi)部類的泄漏問題,就得管理好對(duì)象間的引用關(guān)系。
  • 去除隱式引用(通過靜態(tài)內(nèi)部類來去除隱式引用) 手動(dòng)管理對(duì)象引用(修改靜態(tài)內(nèi)部類的構(gòu)造方式,手動(dòng)引入其外部類引用) 當(dāng)內(nèi)存不可用時(shí),不執(zhí)行不可控代碼(Android可以結(jié)合智能指針,WeakReference包裹外部類實(shí)例)是解決內(nèi)存泄露比較好的方式。

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

好了Handler的介紹到此結(jié)束了,篇幅略長(zhǎng),如果給你帶來了一點(diǎn)幫助,麻煩給個(gè)點(diǎn)贊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容