如何监测Android应用卡顿?这篇就够了

本文首发于微信公众号「Android开发之旅」,欢迎关注git

卡顿介绍

用户在使用咱们应用的时候,不少问题是很难被及时的发现的好比内存占用高,耗费流量等,可是一旦发生卡顿就会被用户直观的感觉到。因此应用卡顿是很影响用户体验的。另一方面,对于开发者来讲,卡顿的问题很难定位,发生问题的缘由错综复杂,好比:代码问题、内存问题、绘制问题以及IO操做等等。并且线上发生的卡顿问题在线下咱们很难复现,由于这和用户当时的系统环境有很大的关系,所以咱们须要在用户发送卡顿的时候记录下用户使用的场景等。好比:内存消耗,磁盘空间,用户行为路径等等。github

优化工具

CPU Profile

目前Android Studio以及自带了CPU Profiler工具,它能够以图形化的形式展现执行的时间、调用栈等信息。收集的信息比较全面,包含了全部线程。可是因为其收集信息全面,致使了运行时内存开销严重,App函数总体运行都会变慢,可能会带偏咱们的优化方向。后端

使用方式: Debug.startMethodTracing(); ... Debug.stopMethodTracing(); 最终生成的文件在sd卡:Android/data/packagename/files目录下。性能优化

Systrace

Systrace以前文章已经讲解过,它是轻量级的框架,并且开销小,能够直观反映CPU的利用率并且右侧alter能够针对一些问题给出相关的建议。 好比绘制慢或者GC频繁等。bash

StrictMode

Android2.3引入的一个工具类:严苛模式。是一种运行时检测机制。能够帮助开发人员检测代码当中不规范的问题。StrictMode主要检测线程策略和虚拟机策略。微信

线程策略包括:网络

  • 自定义的耗时调用,detectCustimSlowCalls框架

  • 磁盘读取操做,detectDiskReadside

  • 网络操做,detectNetwork函数

虚拟机策略:

  • Activity泄漏,detectActivityLeaks

  • Sqlite对象泄漏,detectLeakedSqlLiteObjects

  • 检测实例数量,setClassInstanceLimit

咱们在Application中使用:

private void initStrictMode() {
        if (BuildConfig.DEBUG) {
            //线程
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()// 或者直接使用 .detectAll() 手机所有信息
                    .penaltyLog() //在Logcat 中打印违规异常信息,还能够选择弹框提示或者直接奔溃等
                    .build());
            //虚拟机
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .setClassInstanceLimit(StrictModeTest.class, 1)
                    .detectLeakedClosableObjects() //API等级11
                   .penaltyDropBox()
                    .build());
        }
    }
复制代码

StrictMode自己也是耗性能的,因此咱们只在debug模式下开启。当出现不符合检测策略的时候就会在控制台打印日志,输入StrictMode关键词过滤便可。

自动化检测卡顿方法

CPU Profiler 和 Systrace 都是适合线下使用的,没法带到线上。那咱们如何作到线上监测卡顿呢?

咱们都知道一个进程中只有个Looper对象,咱们经过查看Looper源码发现,在其loop方法中的死循环中有个mLogging对象,在执行的时候打印了一个Dispatching to日志,执行完成的时候有打印了一个Finished to日志。如:

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);
            }
            
            // ...省略中间代码...
            
            if (logging != null) {
                //重点 完成打印
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
​
            // ...省略最后代码...
        }
    }
复制代码

因此咱们能够自定义Printer对象,让Handler的日志都经过咱们自定义的Printer进行打印,而后收集日志信息,匹配Dispatching to和Finished to字段,若是在设定的某个时间内只有Dispatching to字段而没有Finished to字段,那么就说明发生了卡顿。发生卡顿后咱们就收集此时的调用栈信息。相反若是两个字段都存在则说明应用运行的很流畅。

字段Printer设置给mLogging对象:

Looper.getMainLooper().setMessageLogging(new Printer() {
    @Override
    public void println(String log) {
          Log.e("printer","==println=="+log);
    }
});
复制代码

代码中的log字段就是咱们须要的Dispatch和Finished字段,咱们监测这两个字段并收集调用栈信息将其发送到后端进行分析使用。

那么这里其实还存在一个问题就是可能咱们收集的信息不够准确,为何呢?就是咱们收集的调用栈信息是最后收集的,那么这个时候有可能卡顿已经执行完成了,此刻搜集到的信息有可能不是卡顿发生的关键信息。就像OOM同样,它是一个随时都有可能发生的。因此咱们须要高频率的收集日志信息,高频率的收集对后端有必定的压力,而咱们高频收集的信息有很大一部分也是重复的,因此就须要日志去重操做。

ANR异常

ANR异常全称 Application Not Responding,即应用无响应。若是你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称做应用程序无响应对话框,用户能够选择“等待”而让程序继续运行,也能够选择“强制关闭”。因此一个流畅的合理的应用程序中不能出现anr。由于这很影响用户的使用体验,固然因为厂商深度定制系统的缘由,在某些手机上发生ANR也不会弹框的。

发生ANR到弹框在不一样的组件之间时间定义是不同的,按键是5秒。前台广播10秒,后台广播60秒。前台服务20秒,后台服务200秒。这些数据都定义在AMS中,读者能够去看看。

ANR发生执行的流程:

ANR的日志在data/anr/traces.txt目录下。

咱们在线下的时候能够直接经过ADB命令来查看日志:

adb pull data/anr/traces.txt 你的目录 这样能够详细分析CPU、IO、锁等操做的问题所在。

线上咱们可使用FileObserver监控文件变化,可是这种方法在高版本系统中有权限问题。另一种就是使用AnrWatchDog框架。这也是一个开源框架,地址:github.com/SalomonBrys… 它的原理就是经过修改值的方式判断UI线程是否发生卡顿。

这个库使用也很是简单,首先在gradle中配置:

compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'
复制代码

而后在Application中进行初始化:

new ANRWatchDog().start();
复制代码

这样就能够了。默认检测到卡顿就直接抛ANRError异常将应用奔溃,咱们能够复写Listener接口来抓取堆栈信息。

ANRWatchDog是继承之Thread线程的,那么咱们就看下核心方法run方法中的代码逻辑。

// post的操做
    private final Runnable _ticker = new Runnable() {
        @Override public void run() {
            _tick = 0;
            _reported = false;
        }
    };
复制代码
@Override
    public void run() {
        // 首先对线程进行重命名
        setName("|ANR-WatchDog|");
​
        long interval = _timeoutInterval;
        while (!isInterrupted()) {
            boolean needPost = _tick == 0;
            _tick += interval;
            if (needPost) {
                // 发送post
                _uiHandler.post(_ticker);
            }
​
            try {
                // 睡眠
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                _interruptionListener.onInterrupted(e);
                return ;
            }
​
            // If the main thread has not handled _ticker, it is blocked. ANR.
            if (_tick != 0 && !_reported) {
                //noinspection ConstantConditions
                if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
                    Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                    _reported = true;
                    continue ;
                }
​
                interval = _anrInterceptor.intercept(_tick);
                if (interval > 0) {
                    continue;
                }
​
                final ANRError error;
                if (_namePrefix != null) {
                    error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
                } else {
                    error = ANRError.NewMainOnly(_tick);
                }
                _anrListener.onAppNotResponding(error);
                interval = _timeoutInterval;
                _reported = true;
            }
        }
    }

复制代码

使用ANRWatchDog的缘由就是它是非侵入式的,而且能够弥补高版本权限问题。两者结合使用。

以上就是对应用卡顿检测的方法。那么具体如何规避卡顿,这就要求咱们在平时的开发中养成良好的代码习惯。书写高质量代码。具体请看下面往期回顾中的布局优化实战。

往期推荐

Android性能优化之布局优化实战

Android性能优化之启动优化实战

扫描下方二维码关注公众号,及时获取文章推送。

二维码
相关文章
相关标签/搜索