卡顿ANR死锁,线上如何监控?

Posted 涂程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了卡顿ANR死锁,线上如何监控?相关的知识,希望对你有一定的参考价值。

作者:蓝师傅

一、前言

最近参加了几轮面试,发现很多5-7年工作经验的候选人在性能优化这一块,基本上只能说出传统的分析方式,例如ANR分析,是通过查看/data/anr/ 下的log,分析主线程堆栈、cpu、锁信息等

然而,这种方法有一定的局限性,并不是每次都奏效,很多时候是没有堆栈信息给你分析的,例如有些高版本设备需要root权限才能访问/data/anr/ 目录,或者是线上用户的反馈,只有一张ANR的截图加上一句话描述。

假如你的App没有实现ANR监控上报,那么你大概率会把这个问题当成“未复现”处理掉,而没有真正解决问题。

于是我整理了这一篇文章,主要关于卡顿、ANR、死锁监控方案。

二、卡顿原理和监控

2.1 卡顿原理

一般来说,主线程有耗时操作会导致卡顿,卡顿超过阈值,触发ANR。

从源码层面一步步分析卡顿原理:

首先应用进程启动的时候,Zygote会反射调用 ActivityThread 的 main 方法,启动 loop 循环

->ActivityThread

public static void main(String[] args) 
      ...
	Looper.prepareMainLooper();
	Looper.loop();
	...

看下Looper的loop方法

->Looper

public static void loop() 
      for (;;) 
            //1、取消息
            Message msg = queue.next(); // might block
            ...
            //2、消息处理前回调
            if (logging != null) 
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            
            ...

            //3、消息开始处理
            msg.target.dispatchMessage(msg);// 分发处理消息
            ...

            //4、消息处理完回调
            if (logging != null) 
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            
       
       ...

由于loop循环存在,所以主线程可以长时间运行。如果想要在主线程执行某个任务,唯一的办法就是通过主线程Handler post一个任务到消息队列里去,然后loop循环中拿到这个msg,交给这个msg的target处理,这个target是Handler。

从上面的代码块可以看出,导致卡顿的原因可能有两个地方

  • 注释1的queue.next()阻塞,
  • 注释3的dispatchMessage耗时太久。

2.1.1 MessageQueue#next 耗时

看下源码

MessageQueue#next

    Message next() 
        for (;;) 
            //1、nextPollTimeoutMillis 不为0则阻塞
            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;
                // 2、先判断当前第一条消息是不是同步屏障消息,
                if (msg != null && msg.target == null) 
                    //3、遇到同步屏障消息,就跳过去取后面的异步消息来处理,同步消息相当于被设立了屏障
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do 
                        prevMsg = msg;
                        msg = msg.next;
                     while (msg != null && !msg.isAsynchronous());
                

                //4、正常的消息处理,判断是否有延时
                if (msg != null) 
                    if (now < msg.when) 
                        //3.1 
                        // 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 
                    //5、如果没有取到异步消息,那么下次循环就走到1那里去了,nativePollOnce为-1,会一直阻塞
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                
    

next方法的大致流程是这样的:

  1. MessageQueue是一个链表数据结构,判断MessageQueue的头部(第一个消息)是不是一个同步屏障消息,所谓同步屏障消息,就是给同步消息加一层屏障,让同步消息不被处理,只会处理异步消息;

  2. 如果遇到同步屏障消息,就会跳过MessageQueue中的同步消息,只获取里面的异步消息来处理。如果里面没有异步消息,那就会走到注释5,nextPollTimeoutMillis设置为-1,下次循环调用注释1的nativePollOnce就会阻塞;

  3. 如果looper能正常获取到消息,不管是异步消息或者同步消息,处理流程都是一样的,在注释4,先判断是否带延时,如果是,nextPollTimeoutMillis就会被赋值,然后下次循环调用注释1的nativePollOnce就会阻塞一段时间。如果不是delay消息,就直接返回这个msg,给handler处理;

从上面分析可以看出,next方法是不断从MessageQueue里取出消息,有消息就处理,没有消息就调用nativePollOnce阻塞,nativePollOnce 底层是Linux的epoll机制,这里涉及到一个Linux IO 多路复用的知识点

Linux IO 多路复用,select、poll、epoll

Linux 上IO多路复用方案有 select、poll、epoll。它们三个中 epoll 的性能表现是最优秀的,能支持的并发量也最大。

  1. select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理。
  2. poll:它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制
  3. epoll:epoll 主要就是针对select的这三个可优化点进行了改进

1、内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。 2、内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。 3、内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

关于epoll机制就总结这么多啦,可以参考 图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的

回到 MessageQueuenext 方法,看看哪里可能阻塞

同步屏障消息没移除导致next一直阻塞

有一种情况,在存在同步屏障消息的情况下,当异步消息被处理完之后,如果没有及时把同步屏障消息移除,会导致同步消息一直没有机会处理,一直阻塞在nativePollOnce

同步屏障消息

android 是禁止App往MessageQueue插入同步屏障消息的,代码会报错

系统一些高优先级的操作会使用到同步屏障消息,例如View在绘制的时候,最终都要调用ViewRootImplscheduleTraversals方法,会往MessageQueue插入同步屏障消息,绘制完成后会移除同步屏障消息。

->ViewRootImpl

    @UnsupportedAppUsage
    void scheduleTraversals() 
        if (!mTraversalScheduled) 
            mTraversalScheduled = true;
            //插入同步屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) 
                scheduleConsumeBatchedInput();
            
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        
    

    void unscheduleTraversals() 
        if (mTraversalScheduled) 
            mTraversalScheduled = false;
            //移除同步屏障消息
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        
    

为了保证View的绘制过程不被主线程其它任务影响,View在绘制之前会先往MessageQueue插入同步屏障消息,然后再注册Vsync信号监听,Choreographer$FrameDisplayEventReceiver就是用来接收vsync信号回调的

Choreographer$FrameDisplayEventReceiver

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable 
        ...
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) 
           ...
            //
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            //1、发送异步消息
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        

        @Override
        public void run() 
            // 2、doFrame优先执行
            doFrame(mTimestampNanos, mFrame);
        
    

收到Vsync信号回调,注释1会往主线程MessageQueue post一个异步消息,保证注释2的doFrame优先执行。

doFrame才是View真正开始绘制的地方,会调用ViewRootImpldoTraversalperformTraversals

performTraversals里面会调用我们熟悉的View的onMeasureonLayoutonDraw

这里还可以延伸到vsync信号原理,以及为什么要等vsync信号回调才开始View的绘制流程、掉帧的原理、屏幕的双缓冲、三缓冲,由于文章篇幅关系,不是本文的重点,就不一一分析了~

虽然app无法发送同步屏障消息,但是使用异步消息是允许的

异步消息

首先,SDK中限制了App不能post异步消息到MessageQueue里去的,相关字段被加了UnsupportedAppUsage注解

-> Message

    @UnsupportedAppUsage
    /*package*/ int flags;

    /**
     * Returns true if the message is asynchronous, meaning that it is not
     * subject to @link Looper synchronization barriers.
     *
     * @return True if the message is asynchronous.
     *
     * @see #setAsynchronous(boolean)
     */
    public boolean isAsynchronous() 
        return (flags & FLAG_ASYNCHRONOUS) != 0;
    

不过呢,高版本的Handler的构造方法可以通过传async=true,来使用异步消息

public Handler(@Nullable Callback callback, boolean async) 

然后在Handler发送消息的时候,都会走到 enqueueMessage 方法,如下代码块所示,每个消息都带了异步属性,有优先处理权

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) 
        ...
        //如果mAsynchronous为true,就都设置为异步消息
        if (mAsynchronous) 
            msg.setAsynchronous(true);
        
        return queue.enqueueMessage(msg, uptimeMillis);
    

对于低版本SDK,想要使用异步消息,可以通过反射调用Handler(@Nullable Callback callback, boolean async),参考androidx内部的一段代码如下

->androidx.arch.core.executor.DefaultTaskExecutor

    private static Handler createAsync(@NonNull Looper looper) 
        if (Build.VERSION.SDK_INT >= 28) 
            return Handler.createAsync(looper);
        
        if (Build.VERSION.SDK_INT >= 16) 
            try 
                return Handler.class.getDeclaredConstructor(Looper.class, Handler.Callback.class,
                        boolean.class)
                        .newInstance(looper, null, true);
             catch (IllegalAccessException ignored) 
             catch (InstantiationException ignored) 
             catch (NoSuchMethodException ignored) 
             catch (InvocationTargetException e) 
                return new Handler(looper);
            
        
        return new Handler(looper);
    

需要注意的是,App要谨慎使用异步消息,使用不当的情况下可能会出现主线程假死的问题,排查也比较困难,具体可以参考这一篇文章:今日头条 ANR 优化实践系列 - Barrier 导致主线程假死

分析完MessageQueue#next再回头来看看 HandlerdispatchMessage方法

2.1.2 dispatchMessage

上面说到next方法轮循取消息一般情况下是没有问题的,那么只剩下处理消息的逻辑

Handler#dispatchMessage

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) 
        if (msg.callback != null) 
            handleCallback(msg);
         else 
            if (mCallback != null) 
                if (mCallback.handleMessage(msg)) 
                    return;
                
            
            handleMessage(msg);
        
    

dispatchMessage 有三个逻辑,分别对应Handler 使用的三种方式

  1. Handler#post(Runnable r)
  2. 构造方法传CallBack,public Handler(@Nullable Callback callback, boolean async)
  3. Handler 重写 handleMessage 方法

所以,应用卡顿,原因一般都可以认为是Handler处理消息太耗时导致的,细分的原因可能是方法本身太耗时、算法效率低、cpu被抢占、内存不足、IPC超时等等。

2.2 卡顿监控

面试中,被问到如何监控App卡顿,统计方法耗时,我们可以从源码开始切入,讲讲如何通过Looper提供的Printer接口,计算Handler处理一个消息的耗时,判断是否出现卡顿。

2.2.1 卡顿监控方案一

看下Looper 循环的注释2和注释4,可以找到一种卡顿监控的方法

Looper#loop
public static void loop() 
        for (;;) 
            //1、取消息
            Message msg = queue.next(); // might block
            ...
            //2、消息处理前回调
            if (logging != null) 
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            
            ...

            //3、消息开始处理
            msg.target.dispatchMessage(msg);// 分发处理消息
            ...

            //4、消息处理完回调
            if (logging != null) 
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            
       
       ...

注释2和注释4的logging.println是谷歌提供给我们的一个接口,可以监听Handler处理消息耗时,我们只需要调用Looper.getMainLooper().setMessageLogging(printer),即可从回调中拿到Handler处理一个消息的前后时间。

需要注意的是,监听到发生卡顿之后,dispatchMessage 早已调用结束,已经出栈,此时再去获取主线程堆栈,堆栈中是不包含卡顿的代码的。

所以需要在后台开一个线程,定时获取主线程堆栈,将时间点作为key,堆栈信息作为value,保存到Map中,在发生卡顿的时候,取出卡顿时间段内的堆栈信息即可。

不过这种方案只适合线下使用,原因如下:

  1. logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);存在字符串拼接,频繁调用,会创建大量对象,造成内存抖动。
  2. 后台线程频繁获取主线程堆栈,对性能有一定影响,获取主线程堆栈,会暂停主线程的运行

2.2.2 卡顿监控方案二

对于线上卡顿监控,需要了解字节码插桩技术。

通过Gradle Plugin+ASM,编译期在每个方法开始和结束位置分别插入一行代码,统计方法耗时,

伪代码如下

插桩前
fun method()
   run()


插桩后
fun method()
   input(1)
   run()
   output(1)

目前微信的Matrix 使用的卡顿监控方案就是字节码插桩,如下图所示

插桩需要注意的问题:

  1. 避免方法数暴增:在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数。

  2. 过滤简单的函数:过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。

微信Matrix做了大量优化,整体包体积增加1%-2%,帧率下降2帧以内,对性能影响整体可以接受,不过依然只会在灰度包使用。

再来说说ANR~

三、ANR 原理

ANR 的类型和触发ANR的流程

3.1 哪些场景会造成ANR呢

  • Service Timeout:比如前台服务在20s内未执行完成,后台服务Timeout时间是前台服务的10倍,200s;
  • BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s
  • ContentProvider Timeout:内容提供者,在publish过超时10s;
  • InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。

相关超时定义可以参考ActivityManagerService

// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;

// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;

3.2 ANR触发流程

来简单分析下源码,ANR触发流程其实可以比喻成埋炸弹拆炸弹的过程,

以后台Service为例

3.2.1 埋炸弹

Context.startService
调用链如下:
AMS.startService
ActiveServices.startService
ActiveServices.realStartServiceLocked

ActiveServices.realStartServiceLocked
private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException 
    ...
    //1、这里会发送delay消息(SERVICE_TIMEOUT_MSG)
    bumpServiceExecutingLocked(r, execInFg, "create");
    try 
        ...
        //2、通知AMS创建服务
        app.thread.scheduleCreateService(r, r.serviceInfo,
                mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                app.repProcState);
     
    ...

注释1的bumpServiceExecutingLocked内部调用scheduleServiceTimeoutLocked

    void scheduleServiceTimeoutLocked(ProcessRecord proc) 
        ...
        Message msg = mAm.mHandler.obtainMessage(
                ActivityManagerService.SERVICE_TIMEOUT_MSG);
        msg.obj = proc;
        // 发送deley消息,前台服务是20s,后台服务是10s
        mAm.mHandler.sendMessageDelayed(msg,
                proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
    

注释2通知AMS启动服务之前,注释1处发送Handler延时消息,埋下炸弹,如果10s内(前台服务是20s)没人来拆炸弹,炸弹就会爆炸,即ActiveServices#serviceTimeout方法会被调用

3.2.2 拆炸弹

启动一个Service,先要经过AMS管理,然后AMS会通知应用进程执行Service的生命周期, ActivityThreadhandleCreateService方法会被调用

-> ActivityThread#handleCreateService

    private void handleCreateService(CreateServiceData data) 
        try 
           ...
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManager.getService());
             //1、service onCreate调用
            service.onCreate();
            mServices.put(data.token, service);
            try 
            	//2、拆炸弹在这里
                ActivityManager.getService().serviceDoneExecuting(
                        data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
             catch (RemoteException e) 
                throw e.rethrowFromSystemServer();
            
        

    

注释1,ServiceonCreate方法被调用,
注释2,调用AMS的serviceDoneExecuting方法,最终会调用到ActiveServices. serviceDoneExecutingLocked

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
              boolean finishing) 

...
	//移除delay消息
	mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
...

 

可以看到,onCreate方法调用完之后,就会移除delay消息,炸弹被拆除。

3.2.3 引爆炸弹

假设Service的onCreate执行超过10s,那么炸弹就会引爆,也就是

ActiveServices#service

以上是关于卡顿ANR死锁,线上如何监控?的主要内容,如果未能解决你的问题,请参考以下文章

Android性能优化高阶:卡顿ANR死锁,线上如何监控?

卡顿死锁ANR原理,线上监控方案分析

字节二面:如何构建一个线上性能监控的日志系统?

Java多层循环导致系统整体卡顿两例(线上JVM排查之四)

Java多层循环导致系统整体卡顿两例(线上JVM排查之四)

Java多层循环导致系统整体卡顿两例(线上JVM排查之四)