Android源碼剖析:基于 Handler、Looper 實(shí)現(xiàn)攔截全局崩潰萤悴、監(jiān)控ANR等

相信很多人都會(huì)有一個(gè)疑問(wèn)瘾腰,我們?yōu)楹我ラ喿x源碼,工作上又用不上覆履,這個(gè)問(wèn)題很棒蹋盆,我們就先從使用出發(fā),然后分析這些用法的實(shí)現(xiàn)原理硝全,這樣才能體現(xiàn)出閱讀源碼的意義栖雾。

  1. 基于 Handler 和 Looper 攔截全局崩潰(主線程),避免 APP 退出伟众。
  2. 基于 Handler 和 Looper 實(shí)現(xiàn) ANR 監(jiān)控析藕。
  3. 基于 Handler 實(shí)現(xiàn)單線程的線程池。
實(shí)現(xiàn)代碼
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        var startWorkTimeMillis = 0L
        Looper.getMainLooper().setMessageLogging {
            if (it.startsWith(">>>>> Dispatching to Handler")) {
                startWorkTimeMillis = System.currentTimeMillis()
            } else if (it.startsWith("<<<<< Finished to Handler")) {
                val duration = System.currentTimeMillis() - startWorkTimeMillis
                if (duration > 100) {
                    Log.e("主線程執(zhí)行耗時(shí)過(guò)長(zhǎng)","$duration 毫秒凳厢,$it")
                }
            }
        }
        val handler = Handler(Looper.getMainLooper())
        handler.post {
            while (true) {
                try {
                    Looper.loop()
                } catch (e: Throwable) {
                    // TODO 主線程崩潰账胧,自行上報(bào)崩潰信息
                    if (e.message != null && e.message!!.startsWith("Unable to start activity")) {
                        android.os.Process.killProcess(android.os.Process.myPid())
                        break
                    }
                    e.printStackTrace()
                }
            }
        }
        Thread.setDefaultUncaughtExceptionHandler { thread, e ->
            e.printStackTrace()
            // TODO 異步線程崩潰竞慢,自行上報(bào)崩潰信息
        }
    }
}

通過(guò)上面的代碼就可以就可以實(shí)現(xiàn)攔截UI線程的崩潰,耗時(shí)性能監(jiān)控治泥。但是也并不能夠攔截所有的異常筹煮,如果在Activity的onCreate出現(xiàn)崩潰,導(dǎo)致Activity創(chuàng)建失敗居夹,那么就會(huì)顯示黑屏败潦。

ANR獲取堆棧信息《Android:基于 Handler、Looper 實(shí)現(xiàn) ANR 監(jiān)控准脂,獲取堆棧

源碼剖析

通過(guò)上面簡(jiǎn)單的代碼劫扒,我們就實(shí)現(xiàn)崩潰和ANR的攔截和監(jiān)控,但是我們可能并不知道是為何實(shí)現(xiàn)的狸膏,包括我們知道出現(xiàn)了ANR沟饥,但是我們還需要進(jìn)一步分析為何處出現(xiàn)ANR,如何解決环戈。今天分析的問(wèn)題有:

  1. 如何攔截全局崩潰,避免APP退出澎灸。
  2. 如何實(shí)現(xiàn) ANR 監(jiān)控院塞。
  3. 利用 Handler 實(shí)現(xiàn)單線程池功能。
  4. Activity 的生命周期為什么用 Handler 發(fā)送執(zhí)行性昭。
  5. Handler 的延遲操作如何實(shí)現(xiàn)拦止。

涉及的源碼

/java/android/os/Handler.java
/java/android/os/MessageQueue.java
/java/android/os/Looper.java
/java/android.app/ActivityThread.java

我們先從APP啟動(dòng)開(kāi)始分析,APP的啟動(dòng)方法是在ActivityThread中糜颠,在main方法中創(chuàng)建了主線程的Looper汹族,也就是當(dāng)前進(jìn)程創(chuàng)建。并且在main方法的最后調(diào)用了 Looper.loop()其兴,在這個(gè)方法中處理主線程的任務(wù)調(diào)度顶瞒,一旦執(zhí)行完這個(gè)方法就意味著APP被退出了,如果我們要避免APP被退出元旬,就必須讓APP持續(xù)執(zhí)行Looper.loop()榴徐。

package android.app;
public final class ActivityThread extends ClientTransactionHandler {
    ...
    public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}

Looper.loop()

那我們進(jìn)一步分析Looper.loop()方法,在這個(gè)方法中寫了一個(gè)循環(huán)匀归,只有當(dāng) queue.next() == null 的時(shí)候才退出坑资,看到這里我們心里可能會(huì)有一個(gè)疑問(wèn),如果沒(méi)有主線程任務(wù)穆端,是不是Looper.loop()方法就退出了呢袱贮?實(shí)際上queue.next()其實(shí)就是一個(gè)阻塞的方法,如果沒(méi)有任務(wù)或沒(méi)有主動(dòng)退出体啰,會(huì)一直在阻塞攒巍,一直等待主線程任務(wù)添加進(jìn)來(lái)嗽仪。

當(dāng)隊(duì)列有任務(wù),就會(huì)打印信息 Dispatching to ...窑业,然后就調(diào)用 msg.target.dispatchMessage(msg);執(zhí)行任務(wù)钦幔,執(zhí)行完畢就會(huì)打印信息 Finished to ...,我們就可以通過(guò)打印的信息來(lái)分析 ANR常柄,一旦執(zhí)行任務(wù)超過(guò)5秒就會(huì)觸發(fā)系統(tǒng)提示ANR鲤氢,但是我們對(duì)自己的APP肯定要更加嚴(yán)格,我們可以給我們?cè)O(shè)定一個(gè)目標(biāo)西潘,超過(guò)指定的時(shí)長(zhǎng)就上報(bào)統(tǒng)計(jì)卷玉,幫助我們進(jìn)行優(yōu)化。

public final class Looper {
    final MessageQueue mQueue;
    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;
        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
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {}
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            msg.recycleUnchecked();
        }
    }
    public void quit() {
        mQueue.quit(false);
    }
}

如果主線程發(fā)生了異常喷市,就會(huì)退出循環(huán)相种,意味著APP崩潰,所以我們我們需要進(jìn)行try-catch品姓,避免APP退出寝并,我們可以在主線程再啟動(dòng)一個(gè) Looper.loop() 去執(zhí)行主線程任務(wù),然后try-catch這個(gè)Looper.loop()方法腹备,就不會(huì)退出衬潦。

基于 Handler 實(shí)現(xiàn)單線程的線程池

從上面的 Looper.loop() ,我們可以利用 Handler 實(shí)現(xiàn)單線程池功能植酥,而且這個(gè)線程池和主線程一樣擁有立刻執(zhí)行post()镀岛、延遲執(zhí)行postDelayed()、定時(shí)執(zhí)行postAtTime()等強(qiáng)大功能友驮。

// 錯(cuò)誤用法
var handler: Handler? = null
Thread({
    handler = Handler()
}).start()

當(dāng)我們?cè)诋惒骄€程執(zhí)行上面的代碼漂羊,就會(huì)報(bào)錯(cuò) Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
這個(gè)是因?yàn)?Handler 的工作是依靠 Looper 卸留,必須為線程創(chuàng)建 Looper 才能正常功能走越,正確的用法如下:

// 正確用法
var handler: Handler? = null
Thread({
    Looper.prepare()
    handler = Handler()
    Looper.loop()
}).start()

測(cè)試:

button.setOnClickListener {
    handler?.post {
        println(Thread.currentThread())
    }
    handler?.post {
        println(Thread.currentThread())
    }
}

輸出結(jié)果:

System.out: Thread[Thread-2,5,main]
System.out: Thread[Thread-2,5,main]
HandlerThread

HandlerThread 是 Android 對(duì)Thread的封裝,增加了Handler的支持耻瑟,實(shí)現(xiàn)就是實(shí)現(xiàn)了前面例子的功能

val handlerThread = HandlerThread("test")
handlerThread.start()
handler = Handler(handlerThread.looper)

MessageQueue 源碼剖析

我們都知道Handler的功能非常豐富买喧,擁有立刻執(zhí)行post()、延遲執(zhí)行postDelayed()匆赃、定時(shí)執(zhí)行postAtTime()等執(zhí)行方式淤毛。下面就從源碼分析是如何實(shí)現(xiàn)的。

public final class MessageQueue {
    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
}

MessageQueue.next() 是一個(gè)帶有阻塞的方法算柳,只有退出或者有任務(wù)才會(huì)return低淡,起阻塞的實(shí)現(xiàn)是使用Native層的 nativePollOnce() 函數(shù),如果消息隊(duì)列中沒(méi)有消息存在nativePollOnce就不會(huì)返回,一直處于Native層等待狀態(tài)蔗蹋。直到調(diào)用 quit() 退出或者調(diào)用 enqueueMessage(Message msg, long when) 有新的任務(wù)進(jìn)來(lái)調(diào)用了Native層的nativeWake()函數(shù)何荚,才會(huì)重新喚醒。
android_os_MessageQueue.cpp

nativePollOnce(long ptr, int timeoutMillis)

nativePollOnce 是一個(gè)帶有兩個(gè)參數(shù)的Native函數(shù)猪杭,第一個(gè)參數(shù)是作為當(dāng)前任務(wù)隊(duì)列ID餐塘;第二個(gè)參數(shù)是等待時(shí)長(zhǎng),如果是-1皂吮,就代表無(wú)消息戒傻,會(huì)進(jìn)入等待狀態(tài),如果是 0蜂筹,再次查找未等待的消息需纳。如果大于0,就等到指定時(shí)長(zhǎng)然后返回艺挪。

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

在這行代碼進(jìn)行延時(shí)的賦值不翩,從而實(shí)現(xiàn)postDelayed、postAtTime的功能

enqueueMessage()

看到這里我們可能會(huì)有一個(gè)疑問(wèn)麻裳,既然是隊(duì)列口蝠,先進(jìn)先出的原則,那么以下代碼輸出的結(jié)果是如何津坑?

handler?.postDelayed({ println("任務(wù)1") },5000)
handler?.post { println("任務(wù)2") }
handler?.postDelayed({ println("任務(wù)3") },3000)
// 輸出結(jié)果
任務(wù)2
任務(wù)3
任務(wù)1

之所以是如此妙蔗,是因?yàn)樵?enqueueMessage(Message msg, long when) 添加任務(wù)的時(shí)候已經(jīng)就已經(jīng)按照?qǐng)?zhí)行的時(shí)間要求做好了排序。

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

攔截主進(jìn)程崩潰

攔截主進(jìn)程崩潰其實(shí)也有一定的弊端国瓮,因?yàn)榻o用戶的感覺(jué)是點(diǎn)擊沒(méi)有反應(yīng)灭必,因?yàn)楸罎⒁呀?jīng)被攔截了狞谱。如果是Activity.create崩潰乃摹,會(huì)出現(xiàn)黑屏問(wèn)題,所以如果Activity.create崩潰跟衅,必須殺死進(jìn)程孵睬,讓APP重啟,避免出現(xiàn)改問(wèn)題伶跷。

public class MyApplication extends Application {
    
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        new Handler(getMainLooper()).post(() -> {
            while (true) {
                try {
                    Looper.loop();
                } catch (Throwable e) {
                    e.printStackTrace();
                    // TODO 需要手動(dòng)上報(bào)錯(cuò)誤到異常管理平臺(tái)掰读,比如bugly,及時(shí)追蹤問(wèn)題所在叭莫。
                    if (e.getMessage() != null && e.getMessage().startsWith("Unable to start activity")) {
                        // 如果打開(kāi)Activity崩潰蹈集,就殺死進(jìn)程,讓APP重啟雇初。
                        Process.killProcess(Process.myPid());
                        break;
                    }
                }
            }
        });
    }
}

總結(jié)

經(jīng)過(guò)上述的分析拢肆,我覺(jué)得弄懂Handler和Looper MessageQueue還是很有意義的,可以幫助我們更好處理崩潰、ANR郭怪、Handler的使用等支示。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鄙才,隨后出現(xiàn)的幾起案子颂鸿,更是在濱河造成了極大的恐慌,老刑警劉巖攒庵,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嘴纺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡叙甸,警方通過(guò)查閱死者的電腦和手機(jī)颖医,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裆蒸,“玉大人熔萧,你說(shuō)我怎么就攤上這事×诺唬” “怎么了佛致?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辙谜。 經(jīng)常有香客問(wèn)我俺榆,道長(zhǎng),這世上最難降的妖魔是什么装哆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任罐脊,我火速辦了婚禮,結(jié)果婚禮上蜕琴,老公的妹妹穿的比我還像新娘萍桌。我一直安慰自己,他們只是感情好凌简,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布上炎。 她就那樣靜靜地躺著,像睡著了一般雏搂。 火紅的嫁衣襯著肌膚如雪藕施。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天凸郑,我揣著相機(jī)與錄音裳食,去河邊找鬼。 笑死芙沥,一個(gè)胖子當(dāng)著我的面吹牛诲祸,可吹牛的內(nèi)容都是我干的尘盼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼烦绳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼卿捎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起径密,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤午阵,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后享扔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體底桂,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年惧眠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了籽懦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氛魁,死狀恐怖暮顺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秀存,我是刑警寧澤捶码,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站或链,受9級(jí)特大地震影響惫恼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜澳盐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一祈纯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叼耙,春花似錦腕窥、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)革娄。三九已至倾贰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拦惋,已是汗流浹背匆浙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厕妖,地道東北人首尼。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親软能。 傳聞我的和親對(duì)象是個(gè)殘疾皇子迎捺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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