Android性能优化四:卡顿监测方案及原理

1.卡顿介绍及优化工具的选择

1.1背景介绍

很多性能问题不易被发现,但是卡顿却很容易被直观感受,另外一方面,对开发者来说,卡顿的问题的排查和定位有一定的难度。因为卡死产生的原因是错综复杂的,比如代码问题,内存环境、绘制过程,IO操作等等情形都有可能导致卡顿,尤其是线上的卡顿问题,受限于用户具体的运行环境,在线下难以复现,所以最好是能记录卡顿产生时的环境。

1.2工具介绍

Profiler介绍

  • 图形的形式展示执行的时间、调用栈等信息

  • 信息全面、包含所有线程

  • 运行时开销严重,应用整体变慢(可能带偏优化方向)

使用方式

  • Debug.startMethodTracing();
  • Debug.stopMethodTracing();
  • 生成文件在sd卡:Android/data/packagename/files

Systrace介绍

结合Android内核的数据,生成Html报告

API18以上使用,推荐TraceCompat向下兼容

扫描二维码关注公众号,回复: 11218313 查看本文章

使用方式

  • TraceCompat.beginSection(“xxx”);// 手动埋点起始点

  • TraceCompat.endSection();// 手动埋点结束点

  • python systrace.py -b 32768 -t 5 -a packageName -o trace.html sched gfx view wm am app

优势:

  • 轻量级,开销小

  • 直观反映cpu利用率

  • 根据问题给出建议(比如绘制慢或GC频繁)

Systrace具体使用不是本文重点,有兴趣的可参考: Android应用开发性能优化完全分析

StrictMode介绍

严苛模式,Android提供的一种运行时检测机制,可以用来帮助检测代码中一些不规范的问题,项目中有成千上万行代码,如果通过肉眼对代码进行review,这样不但效率低下,而且还容易遗漏问题。使用StrictMode之后,系统会自动检测出来主线程当中一些违例的情况,同时按照配置给出相应的反应。它主要用来检测两大问题,一个是线程策略,另一个是虚拟机策略,StrictMode方便强大,但是容易因为不被熟悉而忽视。

线程策略的检测内容主要包括一下几个方面

  • 自定义的耗时调用,detectCustomSlowCalls()

  • 磁盘读取写入操作,detectDiskReads

  • 网络操作,detectNetwork

虚拟机策略检测内容主要包括以下几个方面

  • Activity泄露,detectActivityLeaks()
  • 未关闭的Closable对象泄漏,detectLeakedClosableObjects()
  • Sqlite对象泄露,detectLeadedSqlLiteObjects
  • 检测某个具体实例数量,setClassInstanceLimit()

StrictMode一般用于线下检测,可以在应用的Application、Activity或者其他应用组件的onCreate方法中加入检测代码

if (BuildConfig.DEBUG) {
            //线程策略检测
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()// or .detectAll() for all detectable problems
                    .penaltyLog() //在Logcat 中打印违规异常信息
                    .build());

            //虚拟机策略检测
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .setClassInstanceLimit(UserBean.class, 1)
                    .detectLeakedClosableObjects() //API等级11
                    .penaltyLog()
                    .build());
        }

可以通过配置peanltyLog(),在Logcat 中打印违规异常信息,或者penaltyDialog(),通过Dialog的方式提示违规异常信息。

2.自动化卡顿检测方案原理

前面简单介绍了Profiler、Systrace系统工具,但是系统工具比较适合线下问题针对性分析,但是卡顿问题和实际使用的场景紧密结合,因此线上环境及测试环节需要自动化检测方案,来帮我们定位卡顿,更重要的是记录卡顿发生时的场景。

2.1自动化卡顿监测原理

它的原理基于Android的消息处理机制,一个线程无论有多少个Handler,都只有一个Looper,主线程中执行的任何代码,都会通过Looper.loop()分发执行,而Looper中有一个mLogging对象,它在每个message处理前后都会被调用,如果主线程发生了卡顿,一定是在dispatchMessage中执行了耗时操作,因此可以通过mLogging对dispatchMessage执行的时间进行监控。

public static void loop() {
        
		...
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // 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);
            }
         
            try {
                msg.target.dispatchMessage(msg);
                if (observer != null) {
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                ...
                throw exception;
            } finally {
                ...
            }
            ...

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

2.2具体实现

  • Looper.getMainLooper().setMessageLogging();
  • 匹配>>>>>Dispatching,指定阈值时间,超过时间后执行任务(获取堆栈信息等)
  • 匹配<<<<<Finished, 如果没有超过指定的阈值时间,即没有发生卡顿,取消之前的任务

2.3第三方自动检测库AndroidPerformanceMonitor和BlockCanary

  • 非侵入式的性能监控组件,通知形式弹出卡顿信息
  • https://github.com/seiginonakama/BlockCanaryEx
  • https://github.com/markzhai/AndroidPerformanceMonitor

使用方式类比LeakCanary。

3.ANR的分析与实战

什么是ANR呢?如果应用程序有一段时间响应不够灵敏,系统会向用户显示一个Dialog对话框,这个对话框称作应用程序无响应(ANR:Application Not Responding)对话框。容易忽视的是这个对话框是由系统服务进程SystemServer的AMS弹出的,而且是在子线程弹出的。

3.1ANR的分类,也就是四大组件的ANR

KeyDispatchTimeout 点击或触摸事件5s内没有响应完成,也可以说是ActivityTimeout

BroadcastTimeout,前台10s,后台60s

ServiceTimeout,前台20s, 后台200s

ContentProviderTimeout

3.2ANR执行流程

  • 发生ANR
  • 进程接收异常终止信号,开始写入进程ANR信息(线程堆栈信息,CPU等使用情况)
  • 弹出ANR提示框,关闭还是继续等待(不同的ROM表现不一,有的手机厂商没有提示框)

3.3ANR分析思路

常规方案:adb pull data/anr/traces.txt 存储路径,即通过adb命令导出anr信息文件,然后根据信息分析是由CPU、IO、锁冲突等哪些原因导致的。

ANR-Watchdog,线上ANR监控方案

  • 非侵入式的ANR监控组件
  • com.github.anrwatchdog:anrwatchdog:1.3.0
  • https://github.com/SalomonBrys/ANR-WatchDog

这个库只有两个类,ANRError和ANRWatchdog。初始化就是通过new ANRWatchdog().start()即可。ANRWatchdog继承Thread,是一个线程类。主要看run方法即可:

public class ANRWatchDog extends Thread {

	private volatile int _tick = 0;

    //注释1
    //更改-tick的值
    private final Runnable _ticker = new Runnable() {
        @Override public void run() {
            _tick = (_tick + 1) % Integer.MAX_VALUE;
        }
    };
    ...

    @Override
    public void run() {
        setName("|ANR-WatchDog|");

        int lastTick;
        int lastIgnored = -1;
        while (!isInterrupted()) {
            lastTick = _tick;
            //注释2
            //post更改更改-tick的值的Runnable到主线程执行
            _uiHandler.post(_ticker);
            try {
                //注释3 sleep 一段时间_timeoutInterval= DEFAULT_ANR_TIMEOUT = 5000
                Thread.sleep(_timeoutInterval);
            }
            catch (InterruptedException e) {
                _interruptionListener.onInterrupted(e);
                return ;
            }
            // If the main thread has not handled _ticker, it is blocked. ANR.
            //注释4
            // 如果更改-tick的值没有发生改变,即Runnable _ticker没有执行,表明主线程发生ANR了
            if (_tick == lastTick) {
                if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
                    if (_tick != lastIgnored)
                        Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                    lastIgnored = _tick;
                    continue ;
                }

                ANRError error;
                if (_namePrefix != null)
                    error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
                else
                    error = ANRError.NewMainOnly();
                _anrListener.onAppNotResponding(error);
                return;
            }
        }
    }

}

ANR的原理也比较好理解,它主要有以下几个步骤:

ANRWatchdog线程类调用start方法,执行run方法,将注释1处更改-tick的值的Runnable _ticker通过post的方式,交给主线程执行。

然后sleep一段时间,默认是5s, 见注释2

判断tick的值是否发生了改变,即名为_ticker的Runnable是否执行,如果tick的值没有改变,代表Runnable没有执行,也就间接表明发生ANR了

可根据日志中的ANRError信息,进行分析定位

和前面的AndroidPerformanceMonitor的简单对比

AndroidPerformanceMonitor: 监控Message的执行,在执行前后加上时间戳,通过消息的执行之间来判定卡顿问题

ANR-WatchDog:sleep一段时间,看任务是否执行

毕竟每个message执行的时间相对较短,还不到ANR的级别,时间的粒度不同,也对应了卡顿和ANR不同,所以前者比较适合监控卡顿,后者适合ANR监控。

4.卡顿单点问题检测方案

自动化卡顿监测方案并不能够满足所有场景的要求,比如有很多的message要执行,但是每个message的执行时间都不到卡顿的阈值。那么此时自动化监测方案不能检测出卡顿,但此时用户却觉得卡顿。IPC是比较耗时的操作,但是一般没有引起足够的重视,经常在主线程中做IPC操作,以及频繁调用,虽然没有到达卡顿的阈值,但还是会影响体验,监控维度有IPC、IO、DB、View绘制等。下面以IPC举例进行简要说明。

4.1IPC问题监测指标

  • IPC调用类型(如PackageManager和TelephoneManager等)

  • 调用耗时、次数

  • 调用堆栈、发生线程

4.2常规方案

  • IPC调用前后添加埋点
  • 侵入性强,不优雅
  • 维护成本大

4.3IPC问题检测技巧

通过adb 命令抓取相关信息生成文件,然后进行分析

adb shell am trace-ipc start

adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt

adb pull /data/local /tmp/ipc-trace.txt

4.4优雅方案

提到埋点的优雅方案就难免ARTHook或者AspectJ,他们还是有区别的ARTHook可以Hook系统方法,而AspectJ(AOP方式),它的实现是会在编译成字节码.class文件的时候,在切面点插入添加相关代码,在运行时切面点代码执行时,也会执行添加的相关代码,达到监测的目的。但是它不能针对系统方法做这些操作。

IPC跨进程都是通过Binder调用,大致流程图如下

在这里插入图片描述

这些IPC如PackageManager和TelephoneManager等都会调用BindProxy,所以只需要Hook这个类的transact方法即可起到监听的效果,注意方法参数要与之相对应。

try {
            DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
                    int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                            LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
                                    + "\n" + Log.getStackTraceString(new Throwable()));
                            super.beforeHookedMethod(param);
                        }
                    });
        } catch(ClassNotFoundException e){
            e.printStackTrace();
        }

5.耗时盲区监控

Activity生命周期间隔,或者onResume到Feed(首页列表第一条)展示的时间间隔,就是耗时监控的盲区,在统计过程容易被忽视。比如在生命周期方法onCreate中postMessage,很可能在Feed显示之前执行,假如这个message执行耗时1秒,那么Feed的展示就要延迟1秒。更多的情况是不知道这段时间主线程具体做了什么事情,一方面是添加代码的人多,另一方面各种第三方的SDK可能在这段时间有postMessage等操作,这是很普遍的,很难通过review代码排查的,再就是线上盲区就更无从排查了。

耗时盲区监控线下方案

TraceView

  • 特别适合一段时间内的盲区监控
  • 线程具体时间做了什么,一目了然

耗时盲区监控线上方案

思考分析

  • 针对mLogging的监控,对主线程Looper来说,所有方法都是Msg,是系统回调的,没有Msg的具体堆栈信息,也就是说不知道Msg是谁抛出来的,只能监测大致的耗时范围,所以这个方案不够完美
  • AOP切Handler的sendMessage方法?可以获取是谁(哪个Handler具体post的message),但是不清楚准确的执行时间点,所以想知道onResume到Feed(列表第一条展示),执行了哪些message,以及具体耗时,AOP方案也是不可以的。

具体方案:

  • 使用统一的Handler:定制具体方法,因为发送消息,不管四send还是post的方式,最终都会调用sendMessageAtTime方法,而处理消息最终都会调用dispatchMessage方法。
  • 定制gradle插件,编译期动态替换,项目中所有使用到的Handler的父类替换成定制的Handler。这样所有sendMessage和dispatchMessage都会经过定制方法的回调。

定制Handler如下:

public class SuperHandler extends Handler {

    private long mStartTime = System.currentTimeMillis();

    public SuperHandler() {
        super(Looper.myLooper(), null);
    }

    public SuperHandler(Callback callback) {
        super(Looper.myLooper(), callback);
    }

    public SuperHandler(Looper looper, Callback callback) {
        super(looper, callback);
    }

    public SuperHandler(Looper looper) {
        super(looper);
    }
	/**
	* 注释1
	* 定制的发送消息方法
	*/
    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        // 布尔值表示消息是否发送成功
        boolean send = super.sendMessageAtTime(msg, uptimeMillis);
        if (send) {// 发送成功
            //将message对象和它的调用栈信息保存起来,后续就可以知道这个消息是由谁发送的
            GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
        }
        return send;
    }

    /**
    * 注释2
    * 定制的处理消息方法
    */
    @Override
    public void dispatchMessage(Message msg) {
        //添加开始时间戳
        mStartTime = System.currentTimeMillis();
        super.dispatchMessage(msg);

        if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
                && Looper.myLooper() == Looper.getMainLooper()) {
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
                jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));

                LogUtils.i("MsgDetail " + jsonObject.toString());
                GetDetailHandlerHelper.getMsgDetail().remove(msg);
            } catch (Exception e) {
            }
        }
    }

}

在注释1处方法,可以把要发送的msg和调用栈信息保存到GetDetailHandlerHelper类中的ConcurrentHashMap集合中,在注释2处方法,即处理消息的时候,先加上时间戳,消息处理完毕,将消息处理的耗时,以及message的调用栈信息打印出来,这样一来,就可以从日志中详细的看到message的耗时情况,以及调用栈信息(在什么地方,由谁发送和执行)了。

public class GetDetailHandlerHelper {

    private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<Message, String> getMsgDetail() {
        return sMsgDetail;
    }

}

监测到卡顿的地方,或者或者耗时较长的方法,就可以针对性的进行的调优了,具体优化可参考启动优化的相关要点,异步、延迟,根据任务的IO型或是CPU型针对性配置线程池等等。

原创文章 23 获赞 30 访问量 9568

猜你喜欢

转载自blog.csdn.net/my_csdnboke/article/details/104224412