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

卡顿介绍

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

优化工具

CPU Profile

目前Android Studio以及自带了CPU Profiler工具,它可以以图形化的形式展示执行的时间、调用栈等信息。收集的信息比较全面,包含了所有线程。但是由于其收集信息全面,导致了运行时内存开销严重,App函数整体运行都会变慢,可能会带偏我们的优化方向。

使用方式:
Debug.startMethodTracing();

Debug.stopMethodTracing();
最终生成的文件在sd卡:Android/data/packagename/files目录下。

Systrace

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

StrictMode

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

线程策略包括:

  • 自定义的耗时调用,detectCustimSlowCalls
  • 磁盘读取操作,detectDiskReads
  • 网络操作,detectNetwork

虚拟机策略:

  • Activity泄漏,detectActivityLeaks
  • Sqlite对象泄漏,detectLeakedSqlLiteObjects
  • 检测实例数量,setClassInstanceLimit

我们在Application中使用:

  1. private void initStrictMode() {
  2. if (BuildConfig.DEBUG) {
  3. //线程
  4. StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
  5. .detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
  6. .detectDiskReads()
  7. .detectDiskWrites()
  8. .detectNetwork()// 或者直接使用 .detectAll() 手机全部信息
  9. .penaltyLog() //在Logcat 中打印违规异常信息,还可以选择弹框提示或者直接奔溃等
  10. .build());
  11. //虚拟机
  12. StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
  13. .detectLeakedSqlLiteObjects()
  14. .setClassInstanceLimit(StrictModeTest.class, 1)
  15. .detectLeakedClosableObjects() //API等级11
  16. .penaltyDropBox()
  17. .build());
  18. }
  19. }

StrictMode本身也是耗性能的,所以我们只在debug模式下开启。当出现不符合检测策略的时候就会在控制台打印日志,输入StrictMode关键词过滤即可。

自动化检测卡顿方法

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

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

  1. public static void loop() {
  2. // ....省略开始代码...
  3. for (;;) {
  4. Message msg = queue.next(); // might block
  5. if (msg == null) {
  6. // No message indicates that the message queue is quitting.
  7. return;
  8. }
  9. // This must be in a local variable, in case a UI event sets the logger
  10. final Printer logging = me.mLogging;
  11. if (logging != null) {
  12. //重点 开始打印
  13. logging.println(">>>>> Dispatching to " + msg.target + " " +
  14. msg.callback + ": " + msg.what);
  15. }
  16. // ...省略中间代码...
  17. if (logging != null) {
  18. //重点 完成打印
  19. logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
  20. }
  21. // ...省略最后代码...
  22. }
  23. }

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

字段Printer设置给mLogging对象:

  1. Looper.getMainLooper().setMessageLogging(new Printer() {
  2. @Override
  3. public void println(String log) {
  4. Log.e("printer","==println=="+log);
  5. }
  6. });

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

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

ANR异常

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

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

ANR发生执行的流程:

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

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

我们在线下的时候可以直接通过ADB命令来查看日志:

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

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

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

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

  1. compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'

然后在Application中进行初始化:

  1. new ANRWatchDog().start();

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

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

  1. // post的操作
  2. private final Runnable _ticker = new Runnable() {
  3. @Override public void run() {
  4. _tick = 0;
  5. _reported = false;
  6. }
  7. };
  1. @Override
  2. public void run() {
  3. // 首先对线程进行重命名
  4. setName("|ANR-WatchDog|");
  5. long interval = _timeoutInterval;
  6. while (!isInterrupted()) {
  7. boolean needPost = _tick == 0;
  8. _tick += interval;
  9. if (needPost) {
  10. // 发送post
  11. _uiHandler.post(_ticker);
  12. }
  13. try {
  14. // 睡眠
  15. Thread.sleep(interval);
  16. } catch (InterruptedException e) {
  17. _interruptionListener.onInterrupted(e);
  18. return ;
  19. }
  20. // If the main thread has not handled _ticker, it is blocked. ANR.
  21. if (_tick != 0 && !_reported) {
  22. //noinspection ConstantConditions
  23. if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
  24. Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
  25. _reported = true;
  26. continue ;
  27. }
  28. interval = _anrInterceptor.intercept(_tick);
  29. if (interval > 0) {
  30. continue;
  31. }
  32. final ANRError error;
  33. if (_namePrefix != null) {
  34. error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
  35. } else {
  36. error = ANRError.NewMainOnly(_tick);
  37. }
  38. _anrListener.onAppNotResponding(error);
  39. interval = _timeoutInterval;
  40. _reported = true;
  41. }
  42. }
  43. }

使用ANRWatchDog的原因就是它是非侵入式的,并且可以弥补高版本权限问题。二者结合使用。

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