深入理解Android中Handler機(jī)制

Looper

對于一位Android開發(fā)者來說约谈,對HandlerLooper子房、Message三個乖寶貝應(yīng)該再熟悉不過了狭莱,這里我們先簡單介紹下這三者的關(guān)系,之后再用Looper.loop方法做點(diǎn)有意思的事情皆愉,加深對運(yùn)行循環(huán)的理解嗜价。

一、源碼理解Handler幕庐、Looper久锥、Message

通常我們在使用Handler時會在主線程中new出一個Handler來接收消息,我們來看下Handler源碼:

/**
     * Default constructor associates this handler with the {@link Looper} for the
     * current thread.
     *
     * If this thread does not have a looper, this handler won't be able to receive messages
     * so an exception is thrown.
     */
    public Handler() {
        this(null, false);
    }

在源碼注釋中說到默認(rèn)的構(gòu)造方法創(chuàng)建Handler异剥,會從當(dāng)前線程中取出Looper瑟由,如果當(dāng)前線程沒有Looper,這個Handler不能夠接收到消息并會拋出異常届吁。
我們繼續(xù)點(diǎn)進(jìn)去:

public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        //獲取Looper
        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;
    }

 /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static Looper myLooper() {
        return sThreadLocal.get();//從ThreadLocal中獲取
    }

既然Looper是從ThreadLocal中獲取的错妖,那必然有時機(jī)要存進(jìn)去,我們看下Looper是什么時候存進(jìn)去的:

 /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void 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));
    }

也就是說我們在調(diào)用Looper. prepare方法時會創(chuàng)建Looper并存入ThreadLocal中疚沐,注意默認(rèn)quitAllowed參數(shù)都為true暂氯,也就是默認(rèn)創(chuàng)建的Looper都是可以退出的,我們可以點(diǎn)進(jìn)去看看:


    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    //進(jìn)去MessageQueue.java
       MessageQueue(boolean quitAllowed) {
          mQuitAllowed = quitAllowed;
          mPtr = nativeInit();
        }

??注意:MessageQueue的成員變量mQuitAllowed亮蛔,在調(diào)用Looper.quit方法時會進(jìn)入MessageQueuemQuitAllowed進(jìn)行判斷痴施,可以簡單看下源碼,后面會再說到:

//MessageQueue.java
void quit(boolean safe) {
        //如果mQuitAllowed為false究流,也就是不允許退出時會報出異常
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;

            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }

看到這里我們應(yīng)該是有疑問的辣吃,

  1. 第一個疑問:默認(rèn)我們調(diào)用Looper.prepare方法時mQuitAllowed變量都為true的,那它什么時候為false?又是被如何設(shè)為false的芬探?
  1. 第二個疑問:我們在創(chuàng)建Handler時神得,并沒有往ThreadLocal中存Looper,而卻直接就取出了ThreadLocal中的Looper偷仿,那么這個Looper是什么時候創(chuàng)建并存入的哩簿?

這里就要說到ActivityThreadmain方法了宵蕉。Zygote進(jìn)程孵化出新的應(yīng)用進(jìn)程后,會執(zhí)行ActivityThread類的main方法节榜。在該方法里會先準(zhǔn)備好Looper和消息隊列羡玛,并將Looper存入ThreadLocal中,然后調(diào)用attach方法將應(yīng)用進(jìn)程綁定到ActivityManagerService宗苍,然后進(jìn)入loop循環(huán)稼稿,不斷地讀取消息隊列里的消息,并分發(fā)消息讳窟。

//ActivityThread
 public static void main(String[] args) {
        SamplingProfilerIntegration.start();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        Process.setArgV0("<pre-initialized>");
        //創(chuàng)建主線程的阻塞隊列
        Looper.prepareMainLooper();

         // 創(chuàng)建ActivityThread實例
        ActivityThread thread = new ActivityThread();
        //執(zhí)行初始化
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        AsyncTask.init();

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
        //開啟循環(huán)
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

我們看下開啟的loop循環(huán)吧:

/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            //注意這里   msg.target為發(fā)送msg的Handler
            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

Looper.loop方法內(nèi)部是個死循環(huán)(for(;;))让歼。queue.next();是從阻塞隊列里取走頭部的Message,當(dāng)沒有Message時主線程就會阻塞丽啡。view繪制是越,事件分發(fā),activity啟動碌上,activity的生命周期回調(diào)等等都是一個個的Message倚评,系統(tǒng)會把這些Message插入到主線程中唯一的queue中,所有的消息都排隊等待主線程的執(zhí)行馏予。
回過來我們捋一下思路天梧,首先霞丧,我們在主線程中創(chuàng)建了Handler,在Handler的構(gòu)造方法中會判斷是否創(chuàng)建了Looper蛹尝,由于在ActivityThread.main方法中我們初始化了Looper并將其存入ThreadLocal中,所以可以正常創(chuàng)建Handler突那。(而如果不是在主線程中創(chuàng)建Handler挫酿,則需要在創(chuàng)建之前手動調(diào)用Looper.prepare方法。)在Looper的構(gòu)造方法中創(chuàng)建了MessageQueue消息隊列用于存取Message早龟。然后猫缭,Handler.sendMessage發(fā)送消息葱弟,在queue.enqueueMessage(msg, uptimeMillis)方法中將Message存入MessageQueue中,并最終在Loop.loop循環(huán)中取出消息調(diào)用msg.target.dispatchMessage(msg);也就是發(fā)送消息的HandlerdispatchMessage方法處理消息猜丹,在dispatchMessage最終調(diào)用了handleMessage(msg);方法芝加。這樣我們就可以正常處理發(fā)送到主線程的消息了藏杖。

二、用Looper搞事情

  1. 異步任務(wù)時阻塞線程制市,讓程序按需要順序執(zhí)行
  1. 判斷主線程是否阻塞
  2. 防止程序異常崩潰

1. 異步任務(wù)時阻塞線程弊予,讓程序按需要順序執(zhí)行
在處理異步任務(wù)的時候,通常我們會傳入回調(diào)來處理請求成功或者失敗的邏輯误褪,而我們通過Looper處理消息機(jī)制也可以讓其順序執(zhí)行碾褂,不使用回調(diào)。我們來看下吧:

    String a = "1";
    public void click(View v){
        new Thread(new Runnable() {
            @Override
            public void run() {
                //模擬耗時操作
                SystemClock.sleep(2000);
                a = "22";
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mHandler.getLooper().quit();
                    }
                });
            }
        }).start();

        try{
            Looper.loop();
        }catch (Exception e){
        }
       Toast.makeText(getApplicationContext(),a,Toast.LENGTH_LONG).show();
    }

當(dāng)點(diǎn)擊按鈕的時候我們開啟線程處理耗時操作嘀略,之后調(diào)用Looper.loop();方法處理消息循環(huán)乓诽,也就是說主線程又開始不斷的讀取queue中的Message并執(zhí)行。這樣當(dāng)執(zhí)行mHandler.getLooper().quit();時會調(diào)用MessageQueuequit方法:

 void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
        ...
}

這個就到了之前我們分析的變量mQuitAllowed,主線程不允許退出讼育,這里會拋出異常稠集,而最終這段代碼是在Looper.loop方法中獲取消息調(diào)用msg.target.dispatchMessage執(zhí)行的剥纷,我們將Looper.loop的異常給捕獲住了痹籍,從而之后代碼繼續(xù)執(zhí)行晦鞋,彈出Toast。

2. 判斷主線程是否阻塞
一般來說吼砂,Loop.loop方法中會不斷取出Message鼎文,調(diào)用其綁定的Handler在UI線程進(jìn)行執(zhí)行主線程刷新操作拇惋。

            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            //注意這里   msg.target為發(fā)送msg的Handler
            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

也就是這里抹剩,基本上可以說msg.target.dispatchMessage(msg);我們可以根據(jù)這行代碼的執(zhí)行時間來判斷UI線程是否有耗時操作蓉坎。

msg.target.dispatchMessage(msg);前后,分別有logging判斷并打印>>>>> Dispatching to<<<<< Finished to的log钳踊,我們可以設(shè)置logging并打印相應(yīng)時間勿侯,基本就可以判斷消耗時間。

          Looper.getMainLooper().setMessageLogging(new Printer() {
            private static final String START = ">>>>> Dispatching";
            private static final String END = "<<<<< Finished";

            @Override
            public void println(String x) {
                if (x.startsWith(START)) {
                    //開始
                }
                if (x.startsWith(END)) {
                   //結(jié)束
                }
            }
        });

3. 防止程序異常崩潰
既然主線程異常事件最終都是在Looper.loop調(diào)用中發(fā)生的祭埂,那我們在Looper.loop方法中將異常捕獲住蛆橡,那主線程的異常也就不會導(dǎo)致程序異常了:

 private Handler mHandler = new Handler();
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_test);

        mHandler.post(new Runnable() {
            @Override
            public void run() {
               while (true){
                   try{
                       Looper.loop();
                   }catch (Exception e){
                   }
               }
            }
        });
    }
    public void click2(View v){
        int a = 1/0;//除數(shù)為0  運(yùn)行時報錯
    }

主線程的所有異常都會從我們手動調(diào)用的Looper.loop處拋出掘譬,一旦拋出就會被try{}catch捕獲,這樣主線程就不會崩潰了粥血。此原理的開源項目:Cockroach酿箭,有興趣可以看下具體實現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缔御,一起剝皮案震驚了整個濱河市妇蛀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌眷茁,老刑警劉巖纵诞,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異登刺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)皇耗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進(jìn)店門郎楼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來女轿,“玉大人壕翩,你說我怎么就攤上這事”本龋” “怎么了芜抒?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長攘宙。 經(jīng)常有香客問我拐迁,道長,這世上最難降的妖魔是什么铺韧? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任缓淹,我火速辦了婚禮讯壶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伏蚊。我一直安慰自己,他們只是感情好肺孵,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布平窘。 她就那樣靜靜地躺著,像睡著了一般瑰艘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上均蜜,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天囤耳,我揣著相機(jī)與錄音偶芍,去河邊找鬼。 笑死匪蟀,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的观挎。 我是一名探鬼主播段化,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼穗泵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了佃延?” 一聲冷哼從身側(cè)響起履肃,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎封锉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碾局,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奴艾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年蕴潦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片潭苞。...
    茶點(diǎn)故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡此疹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出振诬,到底是詐尸還是另有隱情衍菱,我是刑警寧澤肩豁,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站琼锋,受9級特大地震影響祟昭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谜叹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一搬葬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧女仰,春花似錦、人聲如沸疾忍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽推汽。三九已至歧沪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诊胞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工迈着, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留裕菠,地道東北人闭专。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像画髓,于是被迫代替她去往敵國和親平委。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評論 2 359

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