之前写了篇ANR线上监控的文章,这篇是它的姊妹篇。本篇详细介绍了卡顿与ANR的联系以及线上怎么监控卡顿问题,文中是一些我的了解和实战。

1. 卡顿与ANR的联系

卡顿是UI没有及时的按照用户的预期进行反馈,没有及时地渲染出来,从而看起来不接连、不一致。产生卡顿的原因太多了,很难一一列举,但ANR是Google人为规则的概念,产生ANR的原因最多只有4个。分别是:

  • Service Timeout:比如前台服务在20s内未履行完结,后台服务Timeout时刻是前台服务的10倍,200s;
  • BroadcastQueue Timeout:比如前台播送在10s内未履行完结,后台60s
  • ContentProvider Timeout:内容提供者,在publish过超时10s;
  • InputDispatching Timeout: 输入工作分发超时5s,包括按键和接触工作。

假定我在一个button的onClick工作中,有一个耗时操作,这个耗时操作的时刻是10秒,但这个耗时操作并不会引发ANR,它仅仅一次卡顿。

一方面,两者休戚相关,长时刻的UI卡顿是导致ANR的最常见的原因;但另一方面,从原理上来看,两者既不充沛也不必要,是两个纬度的概念。

市面上的一些卡顿监控工具,经常被用来监控ANR(卡顿阈值设置为5秒),这其实很不谨慎:首要,5秒仅仅产生ANR的其间一种原因(Touch工作5秒未被及时消费)的阈值,而其他原因产生ANR的阈值并不是5秒;别的,就算是主线程卡顿了5秒,假如用户没有输入任何的Touch工作,相同不会产生ANR,更何况还有后台ANR等状况。真正意义上的ANR监控计划应该是相似matrix里边那样监控signal信号才算。

2. 卡顿原理

主线程从ActivityThread的main办法开端,准备好主线程的looper,启动loop循环。在loop循环内,无音讯则利用epoll机制堵塞,有音讯则处理音讯。由于主线程一向在loop循环中,所以要想在主线程履行什么逻辑,则有必要发个音讯给主线程的looper然后由这个loop循环触发,由它来分发音讯,然后交给msg的target(Handler)处理。举个比如:ActivityThread.H。

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

loop循环中或许导致卡顿的当地有2个:

  1. queue.next() :有音讯就回来,无音讯则运用epoll机制堵塞(nativePollOnce里边),不会使主线程卡顿。
  2. dispatchMessage耗时太久:也便是Handler处理音讯,app卡顿的话大多数状况下能够以为是这儿处理音讯太耗时了

3. 卡顿监控

  • 计划1:WatchDog,往主线程发音讯,然后延迟看该音讯是否被处理,从而得出主线程是否卡顿的依据。
  • 计划2:利用loop循环时的音讯分发前后的日志打印(matrix运用了这个)

3.1 WatchDog

敞开一个子线程,死循环往主线程发音讯,发完音讯后等待5秒,判断该音讯是否被履行,没被履行则主线程产生ANR,此刻去获取主线程仓库。

  • 优点:简略,稳定,成果论,能够监控到各种类型的卡顿
  • 缺陷:轮询不优雅,不环保,有不确定性,随机漏报

轮询的时刻距离越小,对功能的负面影响就越大,而时刻距离挑选的越大,漏报的或许性也就越大。

  • UI线程要不断处理咱们发送的Message,必然会影响功能和功耗
  • 随机漏报:ANRWatchDog默认的轮询时刻距离为5秒,当主线程卡顿了2秒之后,ANRWatchDog的那个子线程才开端往主线程发送音讯,并且主线程在3秒之后不卡顿了,此刻主线程现已卡顿了5秒了,子线程发送的那个音讯也随之得到履行,等子线程睡5秒起床的时分发现音讯现已被履行了,它没意识到主线程刚刚产生了卡顿。

假定将距离时刻改为

改进:

  • 监控到产生ANR时,除了获取主线程仓库,再获取一下CPU、内存占用等信息
  • 还可结合ProcessLifecycleOwner,app在前台才敞开检测,在后台中止检测

别的有些计划的思路,假如咱们不断缩小轮询的时刻距离,用更短的轮询时刻,接连几个周期音讯都没被处理才视为一次卡顿。则更简单监控到卡顿,但对功能损耗大一些。即使是缩小轮询时刻距离,也不必定能监控到。假定每2秒轮询一次,假如接连三次没被处理,则以为产生了卡顿。在02秒之间主线程开端产生卡顿,在第2秒时开端往主线程发音讯,这样在抵达次数,也便是8秒时结束,但主线程的卡顿在68秒之间就刚好结束了,此刻子线程在第8秒时醒来发现音讯现已被履行了,它没意识到主线程刚刚产生了卡顿。

3.2 Looper Printer

替换主线程Looper的Printer,监控dispatchMessage的履行时刻(大部分主线程的操作终究都会履行到这个dispatchMessage中)。这种计划在微信上有较大规划运用,整体来说功能不是很差,matrix现在的EvilMethodTracer和AnrTracer便是用这个来完结的。

  • 优点:不会随机漏报,无需轮询,一了百了
  • 缺陷:某些类型的卡顿无法被监控到,但有相应解决计划

queue.next()或许会堵塞,这种状况下监控不到。

//Looper.java
for (;;) {
		//这儿或许会block,Printer无法监控到next里边产生的卡顿
    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是主线程,那么这儿明显是在主线程履行的,尽管现在主线程闲暇,但也不能做耗时操作
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }
        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }
    //......
}
  1. 主线程闲暇时会堵塞next(),详细是堵塞在nativePollOnce(),这种状况下无需监控
  2. Touch工作大部分是从nativePollOnce直接到了InputEventReceiver,然后到ViewRootImpl进行分发
  3. IdleHandler的queueIdle()回调办法也无法监控到
  4. 还有一类相对罕见的问题是SyncBarrier(同步屏障)的走漏相同无法被监控到

第一种状况咱们不必管,接下来看一下后边3种状况下怎么监控卡顿。

3.2.1 监控TouchEvent卡顿

首要,Touch是怎么传递到Activity的?给一个view设置一个OnTouchListener,然后看一些Touch的调用栈。

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层直接调用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层会将该实例记录下来,每逢有工作抵达时就会派发到这个实例上来
        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(读取、阻拦、转化输入工作)和InputDispatcher(分发工作)都是运行在system_server体系进程中,而咱们的运用程序运行在自己的运用进程中,这儿涉及到跨进程通讯,这儿的跨进程通讯用的非binder方式,而是用的socket。

Android 线上卡顿监控

InputDispatcher会与咱们的运用进程建立连接,它是socket的服务端;咱们运用进程的native层会有一个socket的客户端,客户端收到音讯后,会告诉咱们运用进程里ViewRootImpl创立的WindowInputEventReceiver(继承自InputEventReceiver)来接纳这个输入工作。工作传递也就走通了,后边便是上层的View树工作分发了。

这儿为啥用socket而不必binder?Socket能够完结异步的告诉,且只需求两个线程参与(Pipe两端各一个),假定体系有N个运用程序,跟输入处理相关的线程数目是N+1(1是Input Dispatcher线程)。然而,假如用Binder完结的话,为了完结异步接纳,每个运用程序需求两个线程,一个Binder线程,一个后台处理线程(不能在Binder线程里处理输入,由于这样太耗时,将会堵塞住发射端的调用线程)。在发射端,相同需求两个线程,一个发送线程,一个接纳线程来接纳运用的完结告诉,所以,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
//调用 socket 的 send 接口来发送音讯
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);
    ......
}

有了上面的常识衬托,现在回到咱们的主问题上来,怎么监控TouchEvent卡顿。已然它们是用socket来进行通讯的,那么咱们能够经过PLT Hook,去Hook这对socket的发送(send)和接纳(recv)办法,从而监控Touch工作。当调用到了recvfrom时(send和recv终究会调用sendto和recvfrom,这2个函数的详细定义在socket.h源码),阐明咱们的运用接纳到了Touch工作,当调用到了sendto时,阐明这个Touch工作现已被成功消费掉了,当两者的时刻相差过大时即阐明产生了一次Touch工作的卡顿。

Android 线上卡顿监控

PLT Hook是什么,它是一种native hook,别的还有一种native hook方式是inline hook。PLT hook的优点是稳定性可控,可线上运用,但它只能hook经过PLT表跳转的函数调用,这在必定程度上限制了它的运用场景。

对PLT Hook的详细原理感兴趣的同学能够看一下下面2篇文章:

  • Android PLT hook 概述
  • 字节跳动开源 Android PLT hook 计划 bhook

现在市面上比较流行的PLT Hook开源库主要有2个,一个是爱奇艺开源的xhook,一个是字节跳动开源的bhook。我这儿运用xhook来举例,InputDispatcher.cpp终究会被编译成libinput.so(详细Android.mk信息看这儿)。那咱们就直接hook这个libinput.so的sendto和recvfrom函数。

理论常识有了,直接开干:

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 在这儿调用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相关源码。

3.2.2 监控IdleHandler卡顿

IdleHandler使命终究会被存储到MessageQueue的mIdleHandlers (一个ArrayList)中,在主线程闲暇时,也便是MessageQueue的next办法暂时没有message能够取出来用时,会从mIdleHandlers 中取出IdleHandler使命进行履行。那咱们能够把这个mIdleHandlers 替换成自己的,重写add办法,添加进来的 IdleHandler 给它包装一下,包装的那个类在履行 queueIdle 时进行计时,这样添加进来的每个IdleHandler在履行的时分咱们都能拿到其 queueIdle 的履行时刻 。假如超时咱们就进行记录或许上报。

fun startDetection() {
      val messageQueue = mHandler.looper.queue
      val messageQueueJavaClass = messageQueue.javaClass
      val mIdleHandlersField = messageQueueJavaClass.getDeclaredField("mIdleHandlers")
      mIdleHandlersField.isAccessible = true
      //尽管mIdleHandlers在Android Q以上被标记为UnsupportedAppUsage,但居然能够成功设置.  只有在反射拜访mIdleHandlers时,才会触发体系的限制
      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("开端履行idleHandler")
        //1. 延迟发送Runnable,Runnable搜集主线程仓库信息
        val runnable = {
            log("idleHandler卡顿 \n ${getMainThreadStackTrace()}")
        }
        threadHandler.postDelayed(runnable, 2000)
        val result = originIdleHandler.queueIdle()
        //2. idleHandler假如及时完结,那么就移除Runnable。假如上面的Runnable得到履行,阐明主线程的idleHandler现已履行了2秒还没履行完,能够搜集信息,对照着检查一下代码了
        threadHandler.removeCallbacks(runnable)
        return result
    }
}

反射完结之后,咱们简略添加一个IdleHandler,然后在里边sleep(10000)测试一下,得到成果如下:

2022-10-17 07:33:50.282 28825-28825/com.xfhy.allinone D/xfhy_tag: 开端履行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)

从日志仓库里边很清晰地看到详细是哪里产生了卡顿。

3.2.3 监控SyncBarrier走漏

什么是SyncBarrier走漏?在说这个之前,咱们得知道什么是SyncBarrier,它翻译过来叫同步屏障,听起来很牛逼,但实际上便是一个Message,只不过这个Message没有target。没有target,那这个Message拿来有什么用?当MessageQueue中存在SyncBarrier的时分,同步音讯就得不到履行,而只会去履行异步音讯。咱们平常用的Message一般是同步的,异步的Message主要是合作SyncBarrier运用。当需求履行一些高优先级的工作的时分,比如View绘制啥的,就需求往主线程MessageQueue插个SyncBarrier,然后ViewRootlmpl 将mTraversalRunnable 交给 ChoreographerChoreographer 等到下一个VSYNC信号到来时,及时地去履行mTraversalRunnable ,交给Choreographer 之后的部分逻辑优先级是很高的,比如履行mTraversalRunnable 的时分,这种逻辑是放到异步音讯里边的。回到ViewRootImpl之后将SyncBarrier移除。

关于同步屏障和Choreographer 的详细逻辑能够看我之前的文章:Handler同步屏障、Choreographer原理及运用

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
		//插入同步屏障,mTraversalRunnable的优先级很高,我需求及时地去履行它
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
		//mTraversalRunnable里边会履行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里边插入了一个同步屏障,这时只能履行异步音讯了,不能履行同步音讯。假定呈现了某种状况,让这个同步屏障无法被移除,那么音讯队列中就一向履行不到同步音讯,或许导致主线程假死,你想想,主线程里边同步音讯都履行不了了,那岂不是要完蛋。那什么状况下会导致呈现上面的异常状况?

  1. scheduleTraversals线程不安全,如果不小心post了多个同步屏障,但只移除了最后一个,那有的同步屏障没被移除的话,同步音讯无法履行
  2. scheduleTraversals中post了同步屏障之后,假定某些操作不小心把异步音讯给移除了,导致没有移除该同步屏障,也会形成相同的悲剧

问题找到了,怎么解决?有什么好办法能监控到这种状况吗(尽管这种状况比较罕见)?微信的同学给出了一种计划,我简略描述下:

  1. 开个子线程,轮询检查主线程的MessageQueue里边的message,检查是否有同步屏障音讯的when现已过去了很久了,但还没得到移除
  2. 此刻能够合理置疑该同步屏障音讯或许已走漏,但还不能确定(有或许是主线程卡顿,导致没有及时移除)
  3. 这个时分,往主线程发一个同步音讯和一个异步音讯(能够距离地多发几次,增加可信度),假如同步音讯没有得到履行,但异步音讯得到履行了,这阐明什么?阐明主线程有处理音讯的能力,不卡顿,且主线程的MessageQueue中有一个同步屏障一向没得到移除,所以同步音讯才没得到履行,而异步音讯得到履行了。
  4. 此刻,能够激进一点,把这个走漏的同步走漏音讯给移除掉。

下面是此计划的核心代码,完好源码在这儿

override fun run() {
    while (!isInterrupted) {
        val messageHead = mMessagesField.get(mainThreadMessageQueue) as? Message
        messageHead?.let { message ->
            //该音讯为同步屏障 && 该音讯3秒没得到履行,先置疑该同步屏障产生了走漏
            if (message.target == null && message.`when` - SystemClock.uptimeMillis() < -3000) {
                //检查MessageQueue#postSyncBarrier(long when)源码得知,同步屏障message的arg1会带着token,
                // 该token相似于同步屏障的序号,每个同步屏障的token是不同的,能够依据该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对应的同步屏障是否还存在,不存在就退出循环
        if (isSyncBarrierNotExist(token)) {
            break
        }
        //2. 存在的话,发1条异步音讯给主线程Handler,再发1条同步音讯给主线程Handler,
        // 看一下同步音讯是否得到了处理,假如同步音讯发了几次都没处理,而异步音讯则发了几次都被处理了,阐明SyncBarrier走漏了
        if (detectSyncBarrierOnce()) {
            //产生了SyncBarrier走漏
            //3. 假如有走漏,那么就移除该走漏了的同步屏障(反射调用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次,主线程的同步音讯还没被处理,而异步音讯缺得到了处理,阐明确实是产生了SyncBarrier走漏
    return barrierCount > 3
}

4. 小结

文中详细介绍了卡顿与ANR的联系,以及卡顿原理和卡顿监控,详细捋下来可对卡顿有更深的了解。对于Looper Printer计划来说,是比较完善的,并且微信也在运用此计划,该踩的坑也踩完了。