Android 卡顿检测方案

五陵年少金市东,银鞍白马渡春风。这篇文章主要讲述Android 卡顿检测方案相关的知识,希望能为你提供帮助。
应用的流畅度最直接的影响了 App 的用户体验,轻微的卡顿有时导致用户的界面操作需要等待一两秒钟才能生效,严重的卡顿则导致系统直接弹出 ANR 的提示窗口,让用户选择要继续等待还是关闭应用。
 

Android 卡顿检测方案

文章图片
 
所以,如果想要提升用户体验,就需要尽量避免卡顿的产生,否则用户经历几次类似场景之后,只会动动手指卸载应用,再顺手到应用商店给个差评。关于卡顿的分析方案,已经有以下两种:
  • 分析  trace  文件。通过分析系统的/data/anr/traces.txt,来找到导致 UI 线程阻塞的源头,这种方案比较适合开发过程中使用,而不适合线上环境;
  • 使用  BlockCanary  开源方案。其原理是利用  Looper  中的 loop 输出的> > > > > Dispatching to  和< < < < < Finished to这样的 log,这种方案适合开发过程和上线的时候使用,但也有个弊端,就是如果系统移除了前面两个 log,检测可能会面临失效。
下面就开始说本文要提及的卡顿检测实现方案,原理简单,代码量也不多,只有  BlockLooper  和  BlockError  两个类。
基本使用在 Application 中调用  BlockLooper.initialize  进行一些参数初始化,具体参数项可以参照 BlockLooper 中的 Configuration 静态内部类,当发生卡顿时,则会在回调(非 UI 线程中)OnBlockListener
public class androidPerformanceToolsApplication extends Application { private final static String TAG = AndroidPerformanceToolsApplication.class.getSimpleName(); @Override public void onCreate() { super.onCreate(); // 初始化相关配置信息 BlockLooper.initialize(new BlockLooper.Builder(this) .setIgnoreDebugger(true) .setReportAllThreadInfo(true) .setSaveLog(true) .setOnBlockListener(new BlockLooper.OnBlockListener() {//回调在非 UI 线程 @Override public void onBlock(BlockError blockError) { blockError.printStackTrace(); //把堆栈信息输出到控制台 } }) .build()); } }

【Android 卡顿检测方案】在选择要启动(停止)卡顿检测的时候,调用对应的 API。
BlockLooper.getBlockLooper().start(); //启动检测 BlockLooper.getBlockLooper().stop(); //停止检测

使用上很简单,接下来看一下效果演示和源码实现。
效果演示制造一个 UI 阻塞效果:
 
Android 卡顿检测方案

文章图片
 
看看 AS 控制台输出的整个堆栈信息:
 
Android 卡顿检测方案

文章图片
 
定位到对应阻塞位置的源码:
 
Android 卡顿检测方案

文章图片
 
当然,对线程的信息 BlockLooper 也不仅输出到控制台,也会帮你缓存到 SD 上对应的应用缓存目录下,在 SD 卡上的/Android/data/对应 App 包名/cache/block/下可以找到,文件名是发生卡顿的时间点,后缀是 trace。
 
Android 卡顿检测方案

文章图片
 
源码解读当 App 在 5s 内无法对用户做出的操作进行响应时,系统就会认为发生了 ANR。BlockLooper 实现上就是利用了这个定义,它继承了 Runnable 接口,通过 initialize 传入对应参数配置好后,通过 BlockLooper 的 start()创建一个 Thread 来跑起这个 Runnable,在没有 stop 之前,BlockLooper 会一直执行 run 方法中的循环,执行步骤如下:
  • Step1. 判断是否停止检测 UI 线程阻塞,未停止则进入 Step2;
  • Step2. 使用 uiHandler 不断发送 ticker 这个 Runnable,ticker 会对 tickCounter 进行累加安徽板面;
  • Step3. BlockLooper 进入指定时间的 sleep(frequency 是在 initialize 时传入,最小不能低于 5s);
  • Step4. 如果 UI 线程没有发生阻塞,则 sleep 过后,tickCounter 一定与原来的值不相等,否则一定是 UI 线程发生阻塞;
  • Step5. 发生阻塞后,还需判断是否由于 Debug 程序引起的,不是则进入 Step6;
  • Step6. 回调 OnBlockListener,以及选择保存当前进程中所有线程的堆栈状态到 SD 卡等。
public class BlockLooper implements Runnable { ... private Handler uiHandler = new Handler(Looper.getMainLooper()); private Runnable ticker = new Runnable() { @Override public void run() { tickCounter = (tickCounter + 1) % Integer.MAX_VALUE; } }; ... private void init(Configuration configuration) { this.appContext = configuration.appContext; this.frequency = configuration.frequency < DEFAULT_FREQUENCY ? DEFAULT_FREQUENCY : configuration.frequency; this.ignoreDebugger = configuration.ignoreDebugger; this.reportAllThreadInfo = configuration.reportAllThreadInfo; this.onBlockListener = configuration.onBlockListener; this.saveLog = configuration.saveLog; } @Override public void run() { int lastTickNumber; while (!isStop) { //Step1 lastTickNumber = tickCounter; uiHandler.post(ticker); //Step2 try { Thread.sleep(frequency); //Step3 } catch (InterruptedException e) { e.printStackTrace(); break; } if (lastTickNumber == tickCounter) { //Step4 if (!ignoreDebugger & & Debug.isDebuggerConnected()) { //Step5 Log.w(TAG, "当前由调试模式引起消息阻塞引起 ANR,可以通过 setIgnoreDebugger(true)来忽略调试模式造成的 ANR"); continue; } BlockError blockError; //Step6 if (!reportAllThreadInfo) { blockError = BlockError.getUiThread(); } else { blockError = BlockError.getAllThread(); } if (onBlockListener != null) { onBlockListener.onBlock(blockError); } if (saveLog) { if (StorageUtils.isMounted()) { File logDir = getLogDirectory(); saveLogToSdcard(blockError, logDir); } else { Log.w(TAG, "sdcard is unmounted"); } } } } } ... public synchronized void start() { if (isStop) { isStop = false; Thread blockThread = new Thread(this); blockThread.setName(LOOPER_NAME); blockThread.start(); } } public synchronized void stop() { if (!isStop) { isStop = true; } } ... ... }

介绍完 BlockLooper 后,再简单说一下 BlockError 的代码,主要有 getUiThread 和 getAllThread 两个方法,分别用户获取 UI 线程和进程中所有线程的堆栈状态信息,当捕获到 BlockError 时,会在 OnBlockListener 中以参数的形式传递回去。
public class BlockError extends Error { private BlockError(ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo) { super("BlockLooper Catch BlockError", threadStackInfo); } public static BlockError getUiThread() { Thread uiThread = Looper.getMainLooper().getThread(); StackTraceElement[] stackTraceElements = uiThread.getStackTrace(); ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo = new ThreadStackInfoWrapper(getThreadNameAndState(uiThread), stackTraceElements) .new ThreadStackInfo(null); return new BlockError(threadStackInfo); } public static BlockError getAllThread() { final Thread uiThread = Looper.getMainLooper().getThread(); Map< Thread, StackTraceElement[]> stackTraceElementMap = new TreeMap< Thread, StackTraceElement[]> (new Comparator< Thread> () { @Override public int compare(Thread lhs, Thread rhs) { if (lhs == rhs) { return 0; } else if (lhs == uiThread) { return 1; } else if (rhs == uiThread) { return -1; } return rhs.getName().compareTo(lhs.getName()); } }); for (Map.Entry< Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) { Thread key = entry.getKey(); StackTraceElement[] value = https://www.songbingjia.com/android/entry.getValue(); if (value.length > 0) { stackTraceElementMap.put(key, value); } } //Fix 有时候 Thread.getAllStackTraces()不包含 UI 线程的问题 if (!stackTraceElementMap.containsKey(uiThread)) { stackTraceElementMap.put(uiThread, uiThread.getStackTrace()); } ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo = null; for (Map.Entry< Thread, StackTraceElement[]> entry : stackTraceElementMap.entrySet()) { Thread key = entry.getKey(); StackTraceElement[] value = entry.getValue(); threadStackInfo = new ThreadStackInfoWrapper(getThreadNameAndState(key), value). new ThreadStackInfo(threadStackInfo); } return new BlockError(threadStackInfo); } ... }

总结以上就是 BlockLooper 的实现,非常简单,相信大家都看得懂。

    推荐阅读