本文我们来分析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卡顿监控手段多种多样,我会在后面的文章中逐一进行分享。
PS:性能优化专栏:《Android性能》持续更新中……
今天的文章Android UI卡顿检测(一)——基于Handler机制的实现方案(线上方案)分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/29751.html