线上卡顿监控

news2024/11/26 16:25:24

文章目录

    • 1. 卡顿与ANR的关系
    • 2. 卡顿原理
    • 3. 卡顿监控
      • 3.1 WatchDog
      • 3.2 Looper Printer
        • 3.2.1 监控TouchEvent卡顿
        • 3.2.2 监控IdleHandler卡顿
        • 3.2.3 监控SyncBarrier泄漏
    • 4. 小结

平时看博客或者学知识,学到的东西比较零散,没有独立的知识模块概念,而且学了之后很容易忘。于是我建立了一个自己的笔记仓库 (一个我长期维护的笔记仓库,感兴趣的可以点个star~你的star是我写作的巨大大大大的动力),将平时学到的东西都归类然后放里面,需要的时候呢也方便复习。

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。

在这里插入图片描述

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事件的卡顿。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Wc5NHJV-1671015189446)(https://raw.githubusercontent.com/xfhy/Android-Notes/master/Images/Touch事件的处理过程.png)]

Touch事件的处理过程

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方案来说,是比较完善的,而且微信也在使用此方案,该踩的坑也踩完了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88878.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

世界杯期间,抖音的涨粉秘诀是什么?

纵览11月抖音涨粉趋势&#xff0c;生活、体育、美食等领域有不少账号迅速圈粉。据新抖『粉丝飙升榜』TOP30显示&#xff0c;11月上榜达人的更替率高达83.3%&#xff0c;其中&#xff0c;有达人凭3条人物随拍视频涨粉千万&#xff1b;有达人凭硬核美食教程&#xff0c;被网友戏称…

网络工程毕业设计 SSM音乐管理系统(源码+论文)

文章目录1 项目简介2 实现效果2.1 界面展示3 设计方案3.1 概述3.2 系统流程3.2.1 系统开发流程3.3 系统结构设计4 项目获取1 项目简介 Hi&#xff0c;各位同学好呀&#xff0c;这里是M学姐&#xff01; 今天向大家分享一个今年(2022)最新完成的毕业设计项目作品&#xff0c;【…

【Python】基础语法 5(字典和文件)

1. 字典 1.1 字典是什么 字典是一种存储键值对的结构。 键值对是计算机/生活中一个非常广泛使用的概念。 把 键(key) 和 值(value) 进行一个一对一的映射, 然后就可以根据键, 快速找到值。 1.2 创建字典 创建一个空的字典&#xff0c;使用 { } 表示字典 a { } b dict() prin…

RabbitMQ之交换机

Exchanges概念 RabbitMQ消息传递模型的核心思想是&#xff1a;生产者生产的消息从不会直接发送到队列。实际上&#xff0c;通常生产者甚至都不知道这些消息传递到了哪些队列中。 相反&#xff0c;生产者只能将消息发送到交换机&#xff0c;交换机工作的内容很简单&#xff0c;…

【数据结构】详解七大排序算法(有源码)

目录☀️直接插入排序☀️希尔排序☀️直接选择排序☀️堆排序☀️冒泡排序☀️快速排序☀️归并排序☀️排序算法复杂度及稳定性分析☀️直接插入排序 1、基本思想 把待排序的数按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所以的记录插入完为止…

【设计模式】装饰者模式Decorator(Java)

文章目录1. 装饰者模式定义2. 类图3.Java实现3.1 饮料Beverage3.2 小料CondimentDecorator3.3 椰果Coconut3.4 红豆RedBean3.5 奶茶MikeTea3.6 果茶JuiceTea3.7 商店主方法StoreMain1. 装饰者模式定义 装饰者模式动态地将责任附加到对象上。若要扩展功能&#xff0c;装饰者提供…

享元模式

文章目录享元模式1.享元模式的本质2.何时选用享元模式3.优缺点4.享元模式的结构5.实现最初实现享元模式初步改造享元模式再改进享元模式再优化享元模式 享元模式最开始看就是类似缓存&#xff0c;缓存一些信息&#xff0c;节约查询时间&#xff0c;以空间换时间 但是再理解后才…

一行代码就能完成的事情,为什么要写两行

今天休息休息&#xff0c;复习一下使用的简洁运算方式以及常用的单行代码 三元运算符 用三元运算符代替简单的if else if (age < 18) {me 小姐姐; } else {me 老阿姨; } 改用三元运算符,一行就能搞定 me age < 18 ? 小姐姐 : 老阿姨; 复杂的判断三元运算符就有点…

这才是最适合新手的python教程(最新版python3.10)

前言 这几年&#xff0c;Python 凭借着语法简洁、跨平台、类库丰富、可扩展、开放源码等特点&#xff0c;成为了 AI 和机器学习时代的第一编程语言。甚至击破铁三角&#xff08; Java、C、C&#xff09;的架构&#xff0c;荣登 TIOBE 榜单的榜首。 身边有不少程序员都选择 Pyt…

LEADTOOLS 入门教程: 检测和提取条形码 - .NET Core

LEADTOOLS是一个综合工具包的集合&#xff0c;用于将识别、文档、医疗、成像和多媒体技术整合到桌面、服务器、平板电脑、网络和移动解决方案中&#xff0c;是一项企业级文档自动化解决方案&#xff0c;有捕捉&#xff0c;OCR&#xff0c;OMR&#xff0c;表单识别和处理&#x…

漏洞丨PDF Explorer 1.5.66.2 - Buffer Overflow

作者&#xff1a;黑蛋 一、漏洞简介 这是一个栈溢出漏洞&#xff0c;一个叫PDF Explorer的软件&#xff08;干嘛的咱没必要知道&#xff09;&#xff0c;他对于用户输入内容长度没有限制造成栈溢出漏洞。 二、漏洞环境 虚拟机 目标程序 调试器 win7x86 PDF Explorer x32…

NeurIPS 2022 | MoVQ: 基于Modulating Quantized Vectors的高保真图像生成

原文标题&#xff1a;MoVQ: Modulating Quantized Vectors for High-Fidelity Image Generation 一、问题提出 虽然两级Vector Quantized (VQ)【指VQVAE-2】生成模型允许合成高保真和高分辨率图像&#xff0c;但它们的量化操作符将图像中的相似patch编码到相同的索引中&#…

2023年MBA联考英语(二)大作文:关于某高校大学生的十大主题

2023年管理类联考倒计时9天&#xff01;在历年的联考英语二作文主题中&#xff0c;大学生群体是时长会出现一期的&#xff0c;这既是对联考中部分专业涉及大学生群体的一种反映&#xff0c;也是因为这个群体的话题对大多数考生来讲都相对熟悉&#xff0c;毕竟都是从这个阶段经历…

世界杯竞猜项目Dapp-第四章(subgraph)

subgraph 是什么 subgraph 索引协议作为 Dapp 领域最重要的基建之一&#xff08;如 uniswap、wave 等都在使用&#xff09;&#xff0c;主要用来做链上数据索引&#xff0c;即在链下对链上事件进行捕捉&#xff08;扫链、计算、存储&#xff09;&#xff0c;然后可对存储下来的…

CAN总线学习笔记 | CAN盒测试STM32的CAN中断接收

CAN基础知识介绍文中介绍了CAN协议的基础知识&#xff0c;以及STM32F4芯片的CAN控制器相关知识&#xff0c;下面将通过实例&#xff0c;利用STM32CubeMX图形化配置工具&#xff0c;并配合CAN盒&#xff0c;来实现CAN通讯的中断收发测试 一、STM32CubeMX配置 CAN是挂载在APB1总…

《钱进球场》:球场争锋·棒球1号位

动画《钱进球场》改编自森高夕次原作、足立刑事著同名体育漫画&#xff0c;于2017年3月宣布动画化 &#xff0c;由STUDIO DEEN负责动画制作&#xff0c;于2018年4月6日起播出。动画第二期于2018年10月5日开始播出。全24话。 中文名 钱进球场 原版名称 グラゼニ 动画制作 Stud…

pytorch 生成手写数字图像

生成对抗网络的概念 最基本的GAN模型由一个生成器 G 和判别器 D 组成。生成器用于生成假样本&#xff0c;判别器用于判断样本是真实的还是假的。 生成器(Generator)&#xff1a;通过机器生成数据&#xff08;大部分情况下是图像&#xff09;&#xff0c;目的是“骗过”判别器…

jQuery - AJAX 简介

什么是 AJAX&#xff1f; AJAX 异步 JavaScript 和 XML&#xff08;Asynchronous JavaScript and XML&#xff09;。 简短地说&#xff0c;在不重载整个网页的情况下&#xff0c;AJAX 通过后台加载数据&#xff0c;并在网页上进行显示。 使用 AJAX 的应用程序案例&#xff…

个人简介网页设计作业 静态HTML个人介绍网页作业 DW个人网站模板下载 WEB静态大学生简单网页 个人网页作品代码 个人网页制作 学生个人网页

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

[附源码]Nodejs计算机毕业设计基于百度AI平台的财税报销系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…