Android 卡顿检测方案

Android卡顿检测方案目前有两种:

1,基于Looper的检测方案。
2,基于Choreographer的帧率检测方案

分别简单介绍下这两种方案的基本实现原理。

  

基于Looper检测方案

实际上这种方案已经有比较成熟的实现,例如AndroidPerformanceMonitor(原来也叫BlockCanary),作者在自己的博客上也有实现原理的介绍BlockCanary — 轻松找出Android App界面卡顿元凶, 这里稍微总结一下。

Looper.java有这么一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}

...
}
}

即Looper的dispatchMessaeg方法的前后都用了一个Printer打印了log,而且这个printer是支持开发者自定义的,我们可以自己实现一个Printer,并通过
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);设置进去,这个时候就可以检测dispatchMessage的市长了,另外我们可以通过定时去dump线程堆栈,且通过上面的自定义Printer标记可以判断两次打 tag 的间隔是否超过设定的阈值,如果超过阈值,则dump当前所有的 Java 线程堆栈,然后进行数据上报。

基于Choreographer的帧率检测方案

Android 4.1中为了改善Android的卡顿问题,Google提出了project butter, 其中非常有名的vsync 16ms机制,就是在这个项目中提出的。 详情可以看Google在方法放出的Android性能优化典范,实际上这里也有很多相关的中文文章。Choreographer就是整个vsnyc中很重要的一个类,它会根据vsync信号,来统筹消息,动画以及绘图等相关操作。网上有一些介绍深入分析UI 上层事件处理核心机制 Choreographer

以下是Choreographer的部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}

startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
if (DEBUG) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}
frameTimeNanos = startNanos - lastFrameOffset;
}

if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
scheduleVsyncLocked();
return;
}

mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}

doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

if (DEBUG) {
final long endNanos = System.nanoTime();
Log.d(TAG, "Frame " + frame + ": Finished, took "
+ (endNanos - startNanos) * 0.000001f + " ms, latency "
+ (startNanos - frameTimeNanos) * 0.000001f + " ms.");
}
}

github上同样也有一个开源的帧率检测小工具TinyDancer, 可以参考其中的FPSFrameCallback.java的实现。我们可以在每一帧被渲染的时候记录下它开始渲染的时间,这样在下一帧被处理时,我们就可以判断上一帧在渲染过程中是否出现掉帧,而整个过程都是实时处理的。

总结

对于这两种方案,第一种方案比较适合在发布前进行测试或者小范围灰度测试然后定位问题,第二种方案适合监控线上环境的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。