Android性能优化之ANR

一、前言:

        面对如何分析ANR这个问题,一般都是通过查看/data/anr/ 下的log,分析主线程堆栈、cpu、锁信息等。但是呢,很多时候是没有堆栈信息给你分析的,例如有些高版本设备需要root权限才能访问/data/anr/ 目录,或者来自线上用户反馈,只有一些截图录屏,或者一句话描述。

        对于这种线上的ANR就束手无策了,所以要有合适的监控方案,要知道导致ANR的原因,比如卡顿,死锁等

二、ANR简介

Application Not Responding,即应用无响应。

出现的原因:

  • KeyDispatchTimeout(5 seconds)按键或触摸事件在特定时间内无响应
  • BroadcastTimeout(10 seconds)BoradcastReceiver 在特定的时间内无法处理
  • ServiceTimeout(20 seconds)小概率类型 Service 在特定的时间内无法处理完成 

三、卡顿分析

每一帧的执行都超过 16.6ms(60fps 的情况下),那么就可能会出现掉帧,也就是卡顿

如果界面线程被阻塞超过一定的时间,就会出现ANR

想要监控应用界面是否发生卡顿,需要先了解一下Android应用主线程的渲染机制

1.Handler处理流程

在应用进程启动的时候,Zygote通过反射调用ActivityThread的main方法,在main方法中调用Looper.loop()来启动looper循环

ActivityThread#main

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

Looper.loop()中的for循环

Looper#loop

        final Looper me = myLooper();
        ……    
        for (;;) {
            ……
            //从MessageQueue中去取消息
            Message msg = me.mQueue.next(); // might block

            final Printer logging = me.mLogging;
            ……
            //消息处理前
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what);
            }
            ……
            //交由handler去分发处理消息
            msg.target.dispatchMessage(msg);
            
            ……
            //消息处理后
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " +                 msg.callback);
            }
        }

在这个for循环中,通过MessageQueue调用next方法去取消息

MessageQueue#next

    Message next() {
        for (;;) {
            //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;
                }
        }
    }

MessageQueue#next流程解析:

  1. MessageQueue是一个链表数据结构,判断MessageQueue的头部是不是一个同步屏障消息
    所谓同步屏障消息,就是给同步消息加一层屏障,让同步消息不被处理,只会处理异步消息
  2. 如果是同步屏障消息,也就是target为null,就会跳过MessageQueue中的同步消息,只获取里面的异步消息来处理。如果里面没有异步消息,就会将nextPollTimeoutMillis设置为-1,然后调用nativePollOnce(ptr, nextPollTimeoutMillis); 进入阻塞
  3. 在正常的消息处理中,不管是异步还是同步消息,先判断是否有带延时,如果是,nextPollTimeoutMillis就会被赋值,下次循环调用nativePollOnce就会阻塞一段时间,如果不是delay消息,就直接返回这个msg,给handler处理;

总结:

        next方法是不断从MessageQueue里取出消息,有消息就处理,没有消息就调用nativePollOnce阻塞

    nativePollOnce底层是Linux的epoll机制

2.同步屏障消息

在MessageQueue#next流程中可以看到同步屏障消息跟其他消息相比是没有target,也就是handler的

Android是禁止App往MessageQueue插入同步屏障消息的,我们在调用同步屏障消息时会报错,加了hide的

Looper.getMainLooper().getQueue().postSyncBarrier();

只有系统内部才会使用到同步屏障消息,比如View的绘制流程。

3.View的绘制流程

        我们的手机屏幕刷新频率有不同的类型,60Hz、120Hz等。60Hz表示屏幕在一秒内刷新60次,也就是每隔16.6ms刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。

        view绘制的起点是在 viewRootImpl.requestLayout() 方法开始,这个方法会去执行View三大绘制任务测量布局绘制。   

        调用requestLayout()方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏障,并设置VSYNC信号监听,当VSYNC信号的到来,会发送一个异步消息到主线程Handler,执行设置的绘制监听任务,并移除同步屏障

好处:保证在ASYNC信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //发送同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //设置ASYNC信号的callback
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //ASYNC信号回来之后 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            }
            ...
   }

Choreographer$FrameDisplayEventReceiver就是用来接收vsync信号回调的

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信号回调,会往主线程MessageQueue post一个异步消息,保证doFrame优先执行。

然后真正开始绘制View,会调用ViewRootImpldoTraversalperformTraversals,紧接着会调用View的三大流程measure、layout、draw。

3.异步消息

 MessageQueue中的Message的变量isAsynchronous,标志了这个Message是否是异步消息;标记为true称为异步消息,标记为false称为同步消息。另一个变量target,标志了这个Message最终由哪个Handler处理

正常使用handler,将Message在插入到MessageQueue中的时候,也就是enqueueMessage时,会强制其target属性不能为null

boolean enqueueMessage(Message msg, long when) {
  // Hanlder不允许为空
  if (msg.target == null) {
      throw new IllegalArgumentException("Message must have a target.");
  }
  ……
}

postSyncBarrier方法被标记为@hide,也就是我们无法调用这个方法

但是还是可以发送异步消息的。在系统添加同步屏障的时候,用以下方法可以将消息设置成异步消息,

  • 使用异步类型的Handler发送的全部Message都是异步的
    通过Handler有一个需要传递boolean async的构造函数,赋值给mAsynchronous,在发送消息的时候就会给Message赋值,但是带异步的构造函数是hide的是无法直接调用的,低版本可以使用反射的方式去创建,api28之后添加了createAsync,可以直接创建异步的Handler,异步Handler发出来的消息则全是异步的。
  • 给Message标志异步
    通过Message.setAsynchronous方法

App要谨慎使用异步消息

4.dispatchMessage

以上分析,都不可能造成卡顿,只剩下Handler#dispatchMessage了

    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

调用逻辑:

  1. Handler#post(Runnable r):将这个r封装到Message的callback中
  2. 构造方法传CallBack,public Handler(@Nullable Callback callback, boolean async) {}
  3. Handler重写handleMessage方法

5.卡顿总结

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

四、卡顿排查工具

1.CPU Profiler

Android studio自带的工具,可以查看 CPU、内存、网络和电池资源的使用情况

以图形的形式展示执行时间、调用栈,信息全面包含所有线程

使用方式:

Debug.startMethodTracing("xxx"); 
...
Debug.stopMethodTracing();

可以通过它来检测区间的代码,最终会在 sd 卡的 Android/data/packagename/files 文件夹下生成一个 xxx.trace 文件

可以通过AS来分析出这段时间内函数的调用以及耗时,帧率,CPU 以及其他线程的情况

缺点:

工具本身会带来很大的性能开销,可能无法反映真实的情况

2.Systrace

五、卡顿监控

1.方案一:

        在Looper#loop方法中,在dispatchMessage前后,提供了logging.println这个接口,调用Looper.getMainLooper().setMessageLogging(printer),即可从回调中拿到Handler处理一个消息的前后时间,来监听Handler处理消息耗时

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

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

注意:

        这种方式只适合线下,因为会打印大量日志,存在字符串拼接,频繁调用,会创建大量对象,造成内存抖动。后台线程频繁获取主线程堆栈,对性能有一定影响,获取主线程堆栈,会暂停主线程的运行

2.方案二:

字节码插桩

在编译期或运行期修改Java或Kotlin源代码编译生成的字节码的过程。通过字节码插桩,我们可以在不改变源代码的情况下,对应用程序的行为进行定制和增强

  1. 避免方法数暴增:在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数。
  2. 过滤简单的函数:过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。
  3. 优化编译期的耗时。可以通过线程池以异步的方式插桩,结合 Future。
  4. 运行时存储方法信息的方式。将方法信息以 int 值保存,类似于 MeasureSpec 的设计

微信的开源项目 matrix 中 Trace Canary 模块已经解决上述问题

六、ANR分析

ANR发生时,会把发生ANR时的线程堆栈、cpu等信息保存起来

一般都是分析 /data/anr/traces.txt 文件

死锁导致ANR分析:

首先看主线程,搜索 main,

ANR日志中有很多信息,可以看到,主线程id是1(tid=1),在等待一个锁,这个锁一直被id为t0的程持有

- waiting to lock <xxxxxxx>(a java.lang.0bject) held by thread t0

locked <yyyyyyyy> (a java.lang.0bject)

t0线程是Blocked状态,正在等待一个锁,这个锁被id为1的线程持有,同时这个t0线程还持有一个锁,这个锁是主线程想要的。

- waiting to lock <yyyyyyyy>(a java,lang.0bject) held by thread 1

locked <xxxxxxx>(a iava.lang.0biect)

通过ANR日志,可以很清楚分析出这个ANR是死锁导致的,并且有具体堆栈信息。

实际项目中,还有很多情况会导致ANR,例如内存不足、CPU被抢占、系统服务没有及时响应

七、ANR 监控

常规的线下分析方法有点繁琐,需要pull出anr日志,然后分析线程堆栈等信息,并且不能带到线上

如何搭建一个完善的ANR监控系统?

1.抓取系统traces.txt 上传

步骤:

  1. 当监控线程发现主线程卡死时,主动向系统发送SIGNAL_QUIT信号
  2. 等待/data/anr/traces.txt文件生成
  3. 文件生成以后进行上报

问题:

  • traces.txt 里面包含所有线程的信息,上传之后需要人工过滤分析
  • 很多高版本系统需要root权限才能读取 /data/anr这个目录

2.ANRWatchDog

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值