Android(安卓)UI卡顿检测(一)——基于Handler机制的实现方案(线上方案)
本文我们来分析Android UI的卡顿性能监控,本方案是基于Handler机制实现的,其他方案我们将在今后文章中进行分析。
UI卡顿产生的原因及监控方案分析
在Android中,UI线程负责执行UI视图的布局、渲染等工作,UI在更新期间,如果UI线程执行时间超过了16ms,则就会产生丢帧的现象,大量的丢帧,就会造成卡顿,影响用户体验。
Android中规定,每秒可以执行60次屏幕刷新,当我们的APP能够达到60帧/秒时,这种体验是优秀的,当帧率降低到40帧以下,甚至30帧以下,用户就可以感知到卡顿了。
UI卡顿产生的原因
UI卡顿通常产生的原因如下:
- 系统CPU资源紧张,分配给APP主线程(UI线程)的CPU时间片减少。
- UI线程中执行了大量的耗时任务,导致了UI线程视图刷新工作的阻塞。
- Android虚拟机频繁执行GC操作导致的卡顿。由于GC会占用大量的系统资源,同时GC过程中会产生UI线程停顿,从而产生卡顿。
- 过度绘制产生卡顿。过度绘制会导致GPU执行时间变长,从而产生丢帧现象。
在诸多原因中,大部分的原因是我们的编码导致的,这类问题可以通过各种优化手段进行优化。我们想要优化UI的性能,避免卡顿产生,首先我们必须做到监控卡顿的发生,能准确定位到哪个模块,甚至哪个方法导致了卡顿,UI性能问题也就解决了一大半了。
本文重点分析,UI线程执行大量耗时操作产生卡顿的检测手段,当然我们可以在线下通过AndroidStudio提供的检测工具进行检测,但我们更想监控线上用户的真实使用场景中的卡顿问题。
方案分析
思路
想要监控线上用户UI线程的卡顿,也就是要把UI线程中的耗时逻辑找出来,然后进行优化开发。那么我们如何如做呢?
Android中的应用程序是消息驱动的,也就是UI线程执行的所有操作,通常都会经过消息机制来进行传递(也就是Handler通信机制)。
Handler的handleMessage负责在UI线程中处理UI相关逻辑,如果我们能在handleMessage执行之前和handleMessage执行之后,分别插入一段我们的日志代码,不就可以实现UI任务执行时间的监控了吗?
方案设想?
我们要直接创建一个基类放在我们的项目代码中,所有需要Handler的地方都对此进行继承,然后我们在基类中添加日志监控,这样就可以实现我们的目的了吧?
NO! NO! NO!
首先这样对项目改造的成本太高了,而且我们也监控不到系统中的消息,也监控不到第三方sdk中的消息执行时间!
怎么做呢?
可行方案
还记得我们在前文 Handler线程通信机制:实战、原理、性能优化! 吗?,文中介绍了Handler的通信原理以及源码,既然所有的操作都要经过这里,我们是否可以从源码角度找到实现方案呢?
答案是肯定的!可行方案就在Handler机制的源码中。
UI卡顿检测的实现
我们来看Looper的loop方法:
public static void loop() { …… for (;;) { …… final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } …… 消息处理相关逻辑 …… msg.target.dispatchMessage(msg); …… if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } …… } }
loop方法中有一个Printer类型的logging,它会在消息执行之前和消息执行之后,输出一行日志,用于标记消息执行的开始和结束。
我们只要记录开始日志和结束日志的时间差,就可以计算出该任务在UI线程的执行时间了,如果执行时间很长,则必然产生了卡顿。
那么,问题来了,我们如何监控这个Printer类型的日志呢?
Printer的替换
private Printer mLogging; public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; }
我们发现mLogging这个对象可以通过一个public方法进行设置!这简直太好了!我们可以通过setMessageLogging方法设置我们自己的Printer对象就可以实现卡顿的监控了!
卡顿监控代码的实现
public class HandlerBlockTask { private final static String TAG = "budaye"; public final int BLOCK_TMME = 1000; private HandlerThread mBlockThread = new HandlerThread("blockThread"); private Handler mHandler; private Runnable mBlockRunnable = new Runnable() { @Override public void run() { StringBuilder sb = new StringBuilder(); StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace(); for (StackTraceElement s : stackTrace) { sb.append(s.toString() + "\n"); } Log.d(TAG, sb.toString()); } }; public void startWork(){ mBlockThread.start(); mHandler = new Handler(mBlockThread.getLooper()); Looper.getMainLooper().setMessageLogging(new Printer() { private static final String START = ">>>>> Dispatching"; private static final String END = "<<<<< Finished"; @Override public void println(String x) { if (x.startsWith(START)) { startMonitor(); } if (x.startsWith(END)) { removeMonitor(); } } }); } private void startMonitor() { mHandler.postDelayed(mBlockRunnable, BLOCK_TMME); } private void removeMonitor() { mHandler.removeCallbacks(mBlockRunnable); }}
逻辑解析:
- Demo中,我们使用了一个工作线程mBlockThread来监控UI线程的卡顿。
- 每次Looper的loop方法对消息进行处理之前,我们添加一个定时监控器。
- 如果UI线程中的消息处理时间小于我们设定的阈值BLOCK_TMME,则取消已添加的定时器。
- 当UI线程执行耗时任务,超过我们设定的阈值时,就会执行mBlockRunnable这个Rnnable,在它的run方法中,打印出主线程卡顿时的代码堆栈。
- 我们把堆栈日志收集起来,进行归类分析,就可以定位到产生卡顿问题的具体代码行号了。
注:当然,你也可以打印出每个消息执行的具体时间,这也非常简单,不做具体Demo分析了。
总结
本章我们介绍了UI线程卡顿产生的原因,以及实现方案的分析。我们使用Handler机制实现了UI卡顿的监控,并且分析了实现原理,最后使用具体Demo完成了代码方案的实现。
当然,UI卡顿监控手段多种多样,我会在后面的文章中逐一进行分享。
更多相关文章
- [android]一个关于UDP和TCP的项目实践(二)
- Android(安卓)Handler 消息通信机制
- Android(安卓)SurfaceView 详解
- Android中线程池的使用分析
- android通过两种方法开启一个线程
- Android(安卓)实战面试题分享
- [Android] [Java] Process 创建+控制+分析 经验浅谈
- Android(安卓)SDK 1.5中文版 (Application基础—4)
- Android中EventBus介绍、使用及源码分析