Android性能优化-基于Hook机制的FPS检测与统计原理

摘要:描述Android基于Hook机制检测并统计应用FPS的一种方法,通过该方法能够量化应用“不流畅”、“卡”等问题描述。

1.问题

  描述Android基于Hook机制检测并统计应用FPS的一种方法,通过该方法能够量化应用“不流畅”、“卡”等问题描述。

2.原理

  每个Android应用都存在一个主线程,每个主线程关联了一个Looper对象。Looper中存在一个消息队列,用来处理主线程消息,如果无法及时处理主线程消息,那么可以认为“卡”(UI界面处理一般在主线程中)。
  每个线程存在一个Choreographer,用来同步显示,通过主线程的Choreographer处理耗时便可以大致计算出当前应用的FPS。

2.1 Looper.loop等待消息

  当应用启动后,会进入Looper.loop()一直等待消息,当消息到来将通过消息目标Message.target对象处理消息,Message.target类型为Handler。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void loop() {
final Looper me = myLooper();
...
for (;;) {
Message msg = queue.next(); // might block
...
msg.target.dispatchMessage(msg);
...
}
}

2.2 Handler.dispatchMessage处理消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
/**
* Subclasses must implement this to receive messages.
*/
public void handleMessage(Message msg) {
}

  具体处理是在子类中实现,我们需要跟踪的是Choreographer消息处理。

2.3 FrameHandler帧同步消息处理

  子类android.view.Choreographer$FrameHandler继承Handler用来处理帧同步消息:

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;
}
}
}

  因此计算出doFrame的耗时就能计算出当前的大致帧率。

3. doFrame与帧率

  首先直观了解下doFrame耗时与主线程加载数据之间的关系,以今日头条为例:
今日头条耗时1
今日头条耗时2
  “位置1”:今日头条首先加载欢迎界面做广告,加载数据等原因导致耗时比较长。
  “位置2”、“位置3”、“位置4”:加载主界面时,仍然有很长的耗时。
  “位置5”:当主线程数据加载完成,耗时恢复正常。

  现编写ListView测试,将5000条数据添加到,每次获取数据都直接去数据库查询,结果如下:
ListView帧率测试
  “位置6”:Activity刚创建完需要显示时数据加载时的帧率,已经达到了2000ms的耗时,测试10000条数据时,耗时为12000ms,此时界面会黑屏一段时间才显示。
  “位置7”:为加载完后帧率正常情况。
  “位置8”:当尝试滑动ListView时,由于获取数据时间过长,表现上已经明显卡顿,此时看到耗时已经几百毫秒,如果按帧率计算不到10帧。

4. Hook实现简单描述

  经过以上分析,我们可以首先hook掉Handler.dispatchMessage函数,然后对Message.target发送目标进行判断,如果为android.view.Choreographer$FrameHandler,那么就可以记录并统计出耗时。

5. 总结

  这里简单描述了主线程中Looper、Choreographer之间的关系以及卡顿的原因,接下来将描述通过Hook方式检测FPS的具体实现。