本文我们来分析Android UI的卡顿性能监控,本方案是基于Handler机制实现的,其他方案我们将在今后文章中进行分析。
在Android中,UI线程负责执行UI视图的布局、渲染等工作,UI在更新期间,如果UI线程执行时间超过了16ms,则就会产生丢帧的现象,大量的丢帧,就会造成卡顿,影响用户体验。
Android中规定,每秒可以执行60次屏幕刷新,当我们的APP能够达到60帧/秒时,这种体验是优秀的,当帧率降低到40帧以下,甚至30帧以下,用户就可以感知到卡顿了。
UI卡顿通常产生的原因如下:
在诸多原因中,大部分的原因是我们的编码导致的,这类问题可以通过各种优化手段进行优化。我们想要优化UI的性能,避免卡顿产生,首先我们必须做到监控卡顿的发生,能准确定位到哪个模块,甚至哪个方法导致了卡顿,UI性能问题也就解决了一大半了。
本文重点分析,UI线程执行大量耗时操作产生卡顿的检测手段,当然我们可以在线下通过AndroidStudio提供的检测工具进行检测,但我们更想监控线上用户的真实使用场景中的卡顿问题。
想要监控线上用户UI线程的卡顿,也就是要把UI线程中的耗时逻辑找出来,然后进行优化开发。那么我们如何如做呢?
Android中的应用程序是消息驱动的,也就是UI线程执行的所有操作,通常都会经过消息机制来进行传递(也就是Handler通信机制)。
Handler的handleMessage负责在UI线程中处理UI相关逻辑,如果我们能在handleMessage执行之前和handleMessage执行之后,分别插入一段我们的日志代码,不就可以实现UI任务执行时间的监控了吗?
我们要直接创建一个基类放在我们的项目代码中,所有需要Handler的地方都对此进行继承,然后我们在基类中添加日志监控,这样就可以实现我们的目的了吧?
NO! NO! NO!
首先这样对项目改造的成本太高了,而且我们也监控不到系统中的消息,也监控不到第三方sdk中的消息执行时间!
怎么做呢?
还记得我们在前文 Handler线程通信机制:实战、原理、性能优化! 吗?,文中介绍了Handler的通信原理以及源码,既然所有的操作都要经过这里,我们是否可以从源码角度找到实现方案呢?
答案是肯定的!可行方案就在Handler机制的源码中。
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类型的日志呢?
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分析了。
本章我们介绍了UI线程卡顿产生的原因,以及实现方案的分析。我们使用Handler机制实现了UI卡顿的监控,并且分析了实现原理,最后使用具体Demo完成了代码方案的实现。
当然,UI卡顿监控手段多种多样,我会在后面的文章中逐一进行分享。