Android線上卡頓監(jiān)控

平時看博客或者學知識,學到的東西比較零散,沒有獨立的知識模塊概念,而且學了之后很容易忘。于是我建立了一個自己的筆記倉庫 (一個我長期維護的筆記倉庫,感興趣的可以點個star~你的star是我寫作的巨大大大大的動力),將平時學到的東西都歸類然后放里面,需要的時候呢也方便復習。

1. 卡頓與ANR的關(guān)系

卡頓是UI沒有及時的按照用戶的預期進行反饋,沒有及時地渲染出來,從而看起來不連續(xù)、不一致。產(chǎn)生卡頓的原因太多了,很難一一列舉,但ANR是Google認為規(guī)定的概念,產(chǎn)生ANR的原因最多只有4個。分別是:

  • Service Timeout:比如前臺服務在20s內(nèi)未執(zhí)行完成,后臺服務Timeout時間是前臺服務的10倍,200s;
  • BroadcastQueue Timeout:比如前臺廣播在10s內(nèi)未執(zhí)行完成,后臺60s
  • ContentProvider Timeout:內(nèi)容提供者,在publish過超時10s;
  • InputDispatching Timeout: 輸入事件分發(fā)超時5s,包括按鍵和觸摸事件。

假如我在一個button的onClick事件中,有一個耗時操作,這個耗時操作的時間是10秒,但這個耗時操作并不會引發(fā)ANR,它只是一次卡頓。

一方面,兩者息息相關(guān),長時間的UI卡頓是導致ANR的最常見的原因;但另一方面,從原理上來看,兩者既不充分也不必要,是兩個緯度的概念。

市面上的一些卡頓監(jiān)控工具,經(jīng)常被用來監(jiān)控ANR(卡頓閾值設置為5秒),這其實很不嚴謹:首先,5秒只是發(fā)生ANR的其中一種原因(Touch事件5秒未被及時消費)的閾值,而其他原因發(fā)生ANR的閾值并不是5秒;另外,就算是主線程卡頓了5秒,如果用戶沒有輸入任何的Touch事件,同樣不會發(fā)生ANR,更何況還有后臺ANR等情況。真正意義上的ANR監(jiān)控方案應該是類似matrix里面那樣監(jiān)控signal信號才算。

2. 卡頓原理

主線程從ActivityThread的main方法開始,準備好主線程的looper,啟動loop循環(huán)。在loop循環(huán)內(nèi),無消息則利用epoll機制阻塞,有消息則處理消息。因為主線程一直在loop循環(huán)中,所以要想在主線程執(zhí)行什么邏輯,則必須發(fā)個消息給主線程的looper然后由這個loop循環(huán)觸發(fā),由它來分發(fā)消息,然后交給msg的target(Handler)處理。舉個例子:ActivityThread.H。

public static void loop() {
        ......
        for (;;) {
            Message msg = queue.next(); // might block
            ......
            msg.target.dispatchMessage(msg);
        }
}

loop循環(huán)中可能導致卡頓的地方有2個:

  1. queue.next() :有消息就返回,無消息則使用epoll機制阻塞(nativePollOnce里面),不會使主線程卡頓。
  2. dispatchMessage耗時太久:也就是Handler處理消息,app卡頓的話大多數(shù)情況下可以認為是這里處理消息太耗時了

3. 卡頓監(jiān)控

  • 方案1:WatchDog,往主線程發(fā)消息,然后延遲看該消息是否被處理,從而得出主線程是否卡頓的依據(jù)。
  • 方案2:利用loop循環(huán)時的消息分發(fā)前后的日志打?。╩atrix使用了這個)

3.1 WatchDog

開啟一個子線程,死循環(huán)往主線程發(fā)消息,發(fā)完消息后等待5秒,判斷該消息是否被執(zhí)行,沒被執(zhí)行則主線程發(fā)生ANR,此時去獲取主線程堆棧。

  • 優(yōu)點:簡單,穩(wěn)定,結(jié)果論,可以監(jiān)控到各種類型的卡頓
  • 缺點:輪詢不優(yōu)雅,不環(huán)保,有不確定性,隨機漏報

輪詢的時間間隔越小,對性能的負面影響就越大,而時間間隔選擇的越大,漏報的可能性也就越大。

  • UI線程要不斷處理我們發(fā)送的Message,必然會影響性能和功耗
  • 隨機漏報:ANRWatchDog默認的輪詢時間間隔為5秒,當主線程卡頓了2秒之后,ANRWatchDog的那個子線程才開始往主線程發(fā)送消息,并且主線程在3秒之后不卡頓了,此時主線程已經(jīng)卡頓了5秒了,子線程發(fā)送的那個消息也隨之得到執(zhí)行,等子線程睡5秒起床的時候發(fā)現(xiàn)消息已經(jīng)被執(zhí)行了,它沒意識到主線程剛剛發(fā)生了卡頓。

假設將間隔時間改為

改進:

  • 監(jiān)控到發(fā)生ANR時,除了獲取主線程堆棧,再獲取一下CPU、內(nèi)存占用等信息
  • 還可結(jié)合ProcessLifecycleOwner,app在前臺才開啟檢測,在后臺停止檢測

另外有些方案的思路,如果我們不斷縮小輪詢的時間間隔,用更短的輪詢時間,連續(xù)幾個周期消息都沒被處理才視為一次卡頓。則更容易監(jiān)控到卡頓,但對性能損耗大一些。即使是縮小輪詢時間間隔,也不一定能監(jiān)控到。假設每2秒輪詢一次,如果連續(xù)三次沒被處理,則認為發(fā)生了卡頓。在02秒之間主線程開始發(fā)生卡頓,在第2秒時開始往主線程發(fā)消息,這樣在到達次數(shù),也就是8秒時結(jié)束,但主線程的卡頓在68秒之間就剛好結(jié)束了,此時子線程在第8秒時醒來發(fā)現(xiàn)消息已經(jīng)被執(zhí)行了,它沒意識到主線程剛剛發(fā)生了卡頓。

3.2 Looper Printer

替換主線程Looper的Printer,監(jiān)控dispatchMessage的執(zhí)行時間(大部分主線程的操作最終都會執(zhí)行到這個dispatchMessage中)。這種方案在微信上有較大規(guī)模使用,總體來說性能不是很差,matrix目前的EvilMethodTracer和AnrTracer就是用這個來實現(xiàn)的。

  • 優(yōu)點:不會隨機漏報,無需輪詢,一勞永逸
  • 缺點:某些類型的卡頓無法被監(jiān)控到,但有相應解決方案

queue.next()可能會阻塞,這種情況下監(jiān)控不到。

//Looper.java
for (;;) {
        //這里可能會block,Printer無法監(jiān)控到next里面發(fā)生的卡頓
    Message msg = queue.next(); // might block
    
    // 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);
    }

    msg.target.dispatchMessage(msg);

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

//MessageQueue.java
for (;;) {
    if (nextPollTimeoutMillis != 0) {
        Binder.flushPendingCommands();
    }

    nativePollOnce(ptr, nextPollTimeoutMillis);

    //......

    // 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 {
                        //IdleHandler的queueIdle,如果Looper是主線程,那么這里明顯是在主線程執(zhí)行的,雖然現(xiàn)在主線程空閑,但也不能
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }
    //......
}
  1. 主線程空閑時會阻塞next(),具體是阻塞在nativePollOnce(),這種情況下無需監(jiān)控
  2. Touch事件大部分是從nativePollOnce直接到了InputEventReceiver,然后到ViewRootImpl進行分發(fā)
  3. IdleHandler的queueIdle()回調(diào)方法也無法監(jiān)控到
  4. 還有一類相對少見的問題是SyncBarrier(同步屏障)的泄漏同樣無法被監(jiān)控到

第一種情況我們不用管,接下來看一下后面3種情況下如何監(jiān)控卡頓。

<span id="head6">3.2.1 監(jiān)控TouchEvent卡頓</span>

首先,Touch是怎么傳遞到Activity的?給一個view設置一個OnTouchListener,然后看一些Touch的調(diào)用棧。

com.xfhy.watchsignaldemo.MainActivity.onCreate$lambda-0(MainActivity.kt:31)
com.xfhy.watchsignaldemo.MainActivity.$r8$lambda$f2Bz7skgRCh8TKh1SZX03s91UhA(Unknown Source:0)
com.xfhy.watchsignaldemo.MainActivity$$ExternalSyntheticLambda0.onTouch(Unknown Source:0)
android.view.View.dispatchTouchEvent(View.java:13695)
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3249)
android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2881)
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3249)
android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2881)
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3249)
android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2881)
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3249)
android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2881)
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3249)
android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2881)
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3249)
android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2881)
com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:741)
com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:2013)
android.app.Activity.dispatchTouchEvent(Activity.java:4180)
androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:70)
com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:687)
android.view.View.dispatchPointerEvent(View.java:13962)
android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:6420)
android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6215)
android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5604)
android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5657)
android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5623)
android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5781)
android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5631)
android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5838)
android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5604)
android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5657)
android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5623)
android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5631)
android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5604)
android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8701)
android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8621)
android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:8574)
android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8959)
android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:239)
android.os.MessageQueue.nativePollOnce(Native Method)
android.os.MessageQueue.next(MessageQueue.java:363)
android.os.Looper.loop(Looper.java:176)
android.app.ActivityThread.main(ActivityThread.java:8668)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)

當有觸摸事件時,nativePollOnce()會收到消息,然后會從native層直接調(diào)用InputEventReceiver.dispatchInputEvent()。

public abstract class InputEventReceiver {
    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null");
        }
        if (looper == null) {
            throw new IllegalArgumentException("looper must not be null");
        }

        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        //在這里進行的注冊,native層會將該實例記錄下來,每當有事件到達時就會派發(fā)到這個實例上來
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                inputChannel, mMessageQueue);

        mCloseGuard.open("dispose");
    }

    // Called from native code.
    @SuppressWarnings("unused")
    @UnsupportedAppUsage
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }
}

InputReader(讀取、攔截、轉(zhuǎn)換輸入事件)和InputDispatcher(分發(fā)事件)都是運行在system_server系統(tǒng)進程中,而我們的應用程序運行在自己的應用進程中,這里涉及到跨進程通信,這里的跨進程通信用的非binder方式,而是用的socket。

https://mmbiz.qpic.cn/mmbiz_png/v1LbPPWiaSt75wx54bPicc7ePltiazSOdUYaBUceygVRv5ic7Q3YrNkZM3ugo8iaFpf4IzGoh2pgiaqQ6Hfub10nY5GQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1

InputDispatcher會與我們的應用進程建立連接,它是socket的服務端;我們應用進程的native層會有一個socket的客戶端,客戶端收到消息后,會通知我們應用進程里ViewRootImpl創(chuàng)建的WindowInputEventReceiver(繼承自InputEventReceiver)來接收這個輸入事件。事件傳遞也就走通了,后面就是上層的View樹事件分發(fā)了。

這里為啥用socket而不用binder?Socket可以實現(xiàn)異步的通知,且只需要兩個線程參與(Pipe兩端各一個),假設系統(tǒng)有N個應用程序,跟輸入處理相關(guān)的線程數(shù)目是N+1(1是Input Dispatcher線程)。然而,如果用Binder實現(xiàn)的話,為了實現(xiàn)異步接收,每個應用程序需要兩個線程,一個Binder線程,一個后臺處理線程(不能在Binder線程里處理輸入,因為這樣太耗時,將會阻塞住發(fā)射端的調(diào)用線程)。在發(fā)射端,同樣需要兩個線程,一個發(fā)送線程,一個接收線程來接收應用的完成通知,所以,N個應用程序需要2(N+1)個線程。相比之下,Socket還是高效多了。

//frameworks/native/services/inputflinger/InputDispatcher.cpp
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
        const sp<Connection>& connection) {
    ......
    status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,
                    keyEntry->deviceId, keyEntry->source,
                    dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags,
                    keyEntry->keyCode, keyEntry->scanCode,
                    keyEntry->metaState, keyEntry->repeatCount, keyEntry->downTime,
                    keyEntry->eventTime);
    ......
}

//frameworks/native/libs/input/InputTransport.cpp
status_t InputPublisher::publishKeyEvent(
        uint32_t seq,
        int32_t deviceId,
        int32_t source,
        int32_t action,
        int32_t flags,
        int32_t keyCode,
        int32_t scanCode,
        int32_t metaState,
        int32_t repeatCount,
        nsecs_t downTime,
        nsecs_t eventTime) {
    ......

    InputMessage msg;
    ......
    msg.body.key.keyCode = keyCode;
    ......
    return mChannel->sendMessage(&msg);
}

//frameworks/native/libs/input/InputTransport.cpp
//調(diào)用 socket 的 send 接口來發(fā)送消息
status_t InputChannel::sendMessage(const InputMessage* msg) {
    size_t msgLength = msg->size();
    ssize_t nWrite;
    do {
        nWrite = ::send(mFd, msg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
    } while (nWrite == -1 && errno == EINTR);
    ......
}

有了上面的知識鋪墊,現(xiàn)在回到我們的主問題上來,如何監(jiān)控TouchEvent卡頓。既然它們是用socket來進行通信的,那么我們可以通過PLT Hook,去Hook這對socket的發(fā)送(send)和接收(recv)方法,從而監(jiān)控Touch事件。當調(diào)用到了recvfrom時(send和recv最終會調(diào)用sendto和recvfrom,這2個函數(shù)的具體定義在socket.h源碼),說明我們的應用接收到了Touch事件,當調(diào)用到了sendto時,說明這個Touch事件已經(jīng)被成功消費掉了,當兩者的時間相差過大時即說明產(chǎn)生了一次Touch事件的卡頓。

[圖片上傳失敗...(image-70f00b-1671015448520)]

Touch事件的處理過程

PLT Hook是什么,它是一種native hook,另外還有一種native hook方式是inline hook。PLT hook的優(yōu)點是穩(wěn)定性可控,可線上使用,但它只能hook通過PLT表跳轉(zhuǎn)的函數(shù)調(diào)用,這在一定程度上限制了它的使用場景。

對PLT Hook的具體原理感興趣的同學可以看一下下面2篇文章:

目前市面上比較流行的PLT Hook開源庫主要有2個,一個是愛奇藝開源的xhook,一個是字節(jié)跳動開源的bhook。我這里使用xhook來舉例,InputDispatcher.cpp最終會被編譯成libinput.so具體Android.mk信息看這里)。那我們就直接hook這個libinput.so的sendto和recvfrom函數(shù)。

理論知識有了,直接開干:

ssize_t (*original_sendto)(int sockfd, const void *buf, size_t len, int flags,
                           const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t my_sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen) {
    //應用端已消費touch事件
    if (getCurrentTime() - lastTime > 5000) {
        __android_log_print(ANDROID_LOG_DEBUG, "xfhy_touch", "Touch有點卡頓");
        //todo xfhy 在這里調(diào)用java去dump主線程堆棧
    }
    long ret = original_sendto(sockfd, buf, len, flags, dest_addr, addrlen);
    return ret;
}

ssize_t (*original_recvfrom)(int sockfd, void *buf, size_t len, int flags,
                             struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t my_recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen) {
    //收到touch事件
    lastTime = getCurrentTime();
    long ret = original_recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
    return ret;
}

void Java_com_xfhy_touch_TouchTest_start(JNIEnv *env, jclass clazz) {
    xhook_register(".*libinput\\.so$", "__sendto_chk",(void *) my_sendto, (void **) (&original_sendto));
    xhook_register(".*libinput\\.so$", "sendto",(void *) my_sendto, (void **) (&original_sendto));
    xhook_register(".*libinput\\.so$", "recvfrom",(void *) my_recvfrom, (void **) (&original_recvfrom));
}

上面這個是我寫的demo,完整代碼看這里,這個demo肯定是不夠完善的。但方案是可行的。完善的方案請看matrix的Touch相關(guān)源碼。

<span id="head7">3.2.2 監(jiān)控IdleHandler卡頓</span>

IdleHandler任務最終會被存儲到MessageQueue的mIdleHandlers (一個ArrayList)中,在主線程空閑時,也就是MessageQueue的next方法暫時沒有message可以取出來用時,會從mIdleHandlers 中取出IdleHandler任務進行執(zhí)行。那我們可以把這個mIdleHandlers 替換成自己的,重寫add方法,添加進來的 IdleHandler 給它包裝一下,包裝的那個類在執(zhí)行 queueIdle 時進行計時,這樣添加進來的每個IdleHandler在執(zhí)行的時候我們都能拿到其 queueIdle 的執(zhí)行時間 。如果超時我們就進行記錄或者上報。

fun startDetection() {
      val messageQueue = mHandler.looper.queue
      val messageQueueJavaClass = messageQueue.javaClass
      val mIdleHandlersField = messageQueueJavaClass.getDeclaredField("mIdleHandlers")
      mIdleHandlersField.isAccessible = true

      //雖然mIdleHandlers在Android Q以上被標記為UnsupportedAppUsage,但居然可以成功設置.  只有在反射訪問mIdleHandlers時,才會觸發(fā)系統(tǒng)的限制
      mIdleHandlersField.set(messageQueue, MyArrayList())
}
class MyArrayList : ArrayList<IdleHandler>() {

    private val handlerThread by lazy {
        HandlerThread("").apply {
            start()
        }
    }
    private val threadHandler by lazy {
        Handler(handlerThread.looper)
    }

    override fun add(element: IdleHandler): Boolean {
        return super.add(MyIdleHandler(element, threadHandler))
    }

}
class MyIdleHandler(private val originIdleHandler: IdleHandler, private val threadHandler: Handler) : IdleHandler {

    override fun queueIdle(): Boolean {
        log("開始執(zhí)行idleHandler")

        //1. 延遲發(fā)送Runnable,Runnable收集主線程堆棧信息
        val runnable = {
            log("idleHandler卡頓 \n ${getMainThreadStackTrace()}")
        }
        threadHandler.postDelayed(runnable, 2000)
        val result = originIdleHandler.queueIdle()
        //2. idleHandler如果及時完成,那么就移除Runnable。如果上面的Runnable得到執(zhí)行,說明主線程的idleHandler已經(jīng)執(zhí)行了2秒還沒執(zhí)行完,可以收集信息,對照著檢查一下代碼了
        threadHandler.removeCallbacks(runnable)
        return result
    }
}

反射完成之后,我們簡單添加一個IdleHandler,然后在里面sleep(10000)測試一下,得到結(jié)果如下:

2022-10-17 07:33:50.282 28825-28825/com.xfhy.allinone D/xfhy_tag: 開始執(zhí)行idleHandler
2022-10-17 07:33:52.286 28825-29203/com.xfhy.allinone D/xfhy_tag: idleHandler卡頓
     java.lang.Thread.sleep(Native Method)
    java.lang.Thread.sleep(Thread.java:443)
    java.lang.Thread.sleep(Thread.java:359)
    com.xfhy.allinone.actual.idlehandler.WatchIdleHandlerActivity$startTimeConsuming$1.queueIdle(WatchIdleHandlerActivity.kt:47)
    com.xfhy.allinone.actual.idlehandler.MyIdleHandler.queueIdle(WatchIdleHandlerActivity.kt:62)
    android.os.MessageQueue.next(MessageQueue.java:465)
    android.os.Looper.loop(Looper.java:176)
    android.app.ActivityThread.main(ActivityThread.java:8668)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)

從日志堆棧里面很清晰地看到具體是哪里發(fā)生了卡頓。

3.2.3 監(jiān)控SyncBarrier泄漏

什么是SyncBarrier泄漏?在說這個之前,我們得知道什么是SyncBarrier,它翻譯過來叫同步屏障,聽起來很牛逼,但實際上就是一個Message,只不過這個Message沒有target。沒有target,那這個Message拿來有什么用?當MessageQueue中存在SyncBarrier的時候,同步消息就得不到執(zhí)行,而只會去執(zhí)行異步消息。我們平時用的Message一般是同步的,異步的Message主要是配合SyncBarrier使用。當需要執(zhí)行一些高優(yōu)先級的事情的時候,比如View繪制啥的,就需要往主線程MessageQueue插個SyncBarrier,然后ViewRootlmpl 將mTraversalRunnable 交給 Choreographer ,Choreographer 等到下一個VSYNC信號到來時,及時地去執(zhí)行mTraversalRunnable ,交給Choreographer 之后的部分邏輯優(yōu)先級是很高的,比如執(zhí)行mTraversalRunnable 的時候,這種邏輯是放到異步消息里面的。回到ViewRootImpl之后將SyncBarrier移除。

關(guān)于同步屏障和Choreographer 的詳細邏輯可以看我之前的文章

Handler同步屏障

Choreographer原理及應用

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //插入同步屏障,mTraversalRunnable的優(yōu)先級很高,我需要及時地去執(zhí)行它
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //mTraversalRunnable里面會執(zhí)行doTraversal
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

void unscheduleTraversals() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        performTraversals();
    }
}

再來說說什么是同步屏障泄露:我們看到在一開始的時候scheduleTraversals里面插入了一個同步屏障,這時只能執(zhí)行異步消息了,不能執(zhí)行同步消息。假設出現(xiàn)了某種狀況,讓這個同步屏障無法被移除,那么消息隊列中就一直執(zhí)行不到同步消息,可能導致主線程假死,你想想,主線程里面同步消息都執(zhí)行不了了,那豈不是要完蛋。那什么情況下會導致出現(xiàn)上面的異常情況?

  1. scheduleTraversals線程不安全,萬一不小心post了多個同步屏障,但只移除了最后一個,那有的同步屏障沒被移除的話,同步消息無法執(zhí)行
  2. scheduleTraversals中post了同步屏障之后,假設某些操作不小心把異步消息給移除了,導致沒有移除該同步屏障,也會造成同樣的悲劇

問題找到了,怎么解決?有什么好辦法能監(jiān)控到這種情況嗎(雖然這種情況比較少見)?微信的同學給出了一種方案,我簡單描述下:

  1. 開個子線程,輪詢檢查主線程的MessageQueue里面的message,檢查是否有同步屏障消息的when已經(jīng)過去了很久了,但還沒得到執(zhí)行
  2. 此時可以合理懷疑該同步屏障消息可能已泄露,但還不能確定
  3. 這個時候,往主線程發(fā)一個同步消息和一個異步消息(可以間隔地多發(fā)幾次,增加可信度),如果同步消息沒有得到執(zhí)行,但異步消息得到執(zhí)行了,這說明什么?說明主線程的MessageQueue中有一個同步屏障一直沒得到移除,所以同步消息才沒得到執(zhí)行,而異步消息得到執(zhí)行了。
  4. 此時,可以激進一點,把這個泄露的同步泄露消息給移除掉。

下面是此方案的核心代碼,完整源碼在這里

override fun run() {
    while (!isInterrupted) {
        val messageHead = mMessagesField.get(mainThreadMessageQueue) as? Message
        messageHead?.let { message ->
            //該消息為同步屏障 && 該消息3秒沒得到執(zhí)行,先懷疑該同步屏障發(fā)生了泄露
            if (message.target == null && message.`when` - SystemClock.uptimeMillis() < -3000) {
                //查看MessageQueue#postSyncBarrier(long when)源碼得知,同步屏障message的arg1會攜帶token,
                // 該token類似于同步屏障的序號,每個同步屏障的token是不同的,可以根據(jù)該token唯一標識一個同步屏障
                val token = message.arg1
                startCheckLeaking(token)
            }
        }
        sleep(2000)
    }
}

private fun startCheckLeaking(token: Int) {
    var checkCount = 0
    barrierCount = 0
    while (checkCount < 5) {
        checkCount++
        //1. 判斷該token對應的同步屏障是否還存在,不存在就退出循環(huán)
        if (isSyncBarrierNotExist(token)) {
            break
        }
        //2. 存在的話,發(fā)1條異步消息給主線程Handler,再發(fā)1條同步消息給主線程Handler,
        // 看一下同步消息是否得到了處理,如果同步消息發(fā)了幾次都沒處理,而異步消息則發(fā)了幾次都被處理了,說明SyncBarrier泄露了
        if (detectSyncBarrierOnce()) {
            //發(fā)生了SyncBarrier泄露
            //3. 如果有泄露,那么就移除該泄露了的同步屏障(反射調(diào)用MessageQueue的removeSyncBarrier(int token))
            removeSyncBarrier(token)
            break
        }
        SystemClock.sleep(1000)
    }
}

private fun detectSyncBarrierOnce(): Boolean {
    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when (msg.arg1) {
                -1 -> {
                    //異步消息
                    barrierCount++
                }
                0 -> {
                    //同步消息 說明主線程的同步消息是能做事的啊,就沒有SyncBarrier一說了
                    barrierCount = 0
                }
                else -> {}
            }
        }
    }

    val asyncMessage = Message.obtain()
    asyncMessage.isAsynchronous = true
    asyncMessage.arg1 = -1

    val syncMessage = Message.obtain()
    syncMessage.arg1 = 0

    handler.sendMessage(asyncMessage)
    handler.sendMessage(syncMessage)

    //超過3次,主線程的同步消息還沒被處理,而異步消息缺得到了處理,說明確實是發(fā)生了SyncBarrier泄露
    return barrierCount > 3
}

4. 小結(jié)

文中詳細介紹了卡頓與ANR的關(guān)系,以及卡頓原理和卡頓監(jiān)控,詳細捋下來可對卡頓有更深的理解。對于Looper Printer方案來說,是比較完善的,而且微信也在使用此方案,該踩的坑也踩完了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容