1.界面绘制的层级过高
2.UI线程存在耗时操作
3.GC频繁导致线程频繁挂起
框架的检测原理:利用主线程Looper日志打点检测
众所周知在应用启动时,在ActivityThread的main方法中会在主线程启动一个MainHandler
Handler源码每次执行MessageQueue之前会打印日志,执行完之后会打印日志。BlockCanary利用这个原理 Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
并在mainLooperPrinter中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒,该阀值是可以自定义)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。
`public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
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);
}
...
}
}`
框架架构图:
根据BlockCanary的原理,相信大家都能看出问题,其实这样检测卡顿的方式并不严谨。
BlockCanary是通过检测MainHandler处理每一个任务所耗费的时间,如果超过了阀价那么就提示卡顿。但是在根据Android的UI渲染原理,大家可以知道,屏幕是以60HZ的频率发出刷新信号,但是如果当当前界面没有发生任何的UI变动时,ViewRootImpl是收不到刷新的信号的。
那么在界面没有发生UI变化时(也就是不需要执行刷新界面的操作)主线程执行了耗时的操作而且超过了设定的时间阀值,那么这种情况其实并没有发生卡顿的现象,但是BlockCanary认为界面已经卡顿了。
在控制UI渲染流程中提供了一个监听屏幕刷新类Choreographer,在这个类中可以通过 Choreographer.getInstance().postFrameCallback(BlockDetectByChoregrapher.getInstance());
在屏幕绘制每一帧之前会回调 那么通过回调的时间可以判断界面的掉帧情况。如下:
public class BlockDetectByChoregrapher implements Choreographer.FrameCallback {
public static BlockDetectByChoregrapher sInstance;
private String TAG = "BlockDetectByChoregrapher";
public static final float deviceRefreshRateMs = 16.6f;
public static long lastFrameTimeNanos = 0;//纳秒为单位
public static long currentFrameTimeNanos = 0;
public void start() {
Choreographer.getInstance().postFrameCallback(BlockDetectByChoregrapher.getInstance());
}
public static BlockDetectByChoregrapher getInstance() {
if (sInstance == null) {
sInstance = new BlockDetectByChoregrapher();
}
return sInstance;
}
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
currentFrameTimeNanos = frameTimeNanos;
float value = (currentFrameTimeNanos - lastFrameTimeNanos) / 1000000.0f;
final int skipFrameCount = skipFrameCount(lastFrameTimeNanos, currentFrameTimeNanos, deviceRefreshRateMs);
if (skipFrameCount > 1) {
Log.e(TAG, "两次绘制时间间隔value=" + value
+ " frameTimeNanos=" + frameTimeNanos + " currentFrameTimeNanos=" + currentFrameTimeNanos + " skipFrameCount=" + skipFrameCount + "");
}
lastFrameTimeNanos = currentFrameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
/**
* 计算跳过多少帧
*
* @param start
* @param end
* @param devicefreshRate
* @return
*/
private int skipFrameCount(long start, long end, float devicefreshRate) {
int count = 0;
long diffNs = end - start;
long diffMs = Math.round(diffNs / 1000000.0f);
long dev = Math.round(devicefreshRate);
if (diffMs > dev) {
long skipCount = diffMs / dev;
count = (int) skipCount;
}
return count;
}
}
利用Choreographer虽然可以更准确的判断出是否发生卡顿,但是不能够看出发生卡顿的地方。所以可以通过和BlockCanary配合使用,这样能够精准的找到卡顿发生的位置,而且主线程做过多的耗时的操作,不管是否造成卡顿,总归是不好的。
如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。喜欢的小伙伴可以关注一下哦。谢谢!