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

摘要:描述Android基于Hook机制检测并统计应用FPS的一种方法,前文描述了“卡”的原因以及检测的基本原理,接下来将描述一种基于Xposed的具体实现。

1.基本流程

  当应用主线程启动时,需要做两件事:一是创建一个依附与这个应用的统计分析子进程,它有一个阻塞队列等待接收需要处理的消息;二是hook掉Handler.dispatchMessage,并对消息进行封装发送到到统计分析子进程。

2.Xposed Hook过程

2.1 Hook Application构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void hookApplicationConstructor() {
Set<XC_MethodHook.Unhook> unhookApplicationConstructor = XposedBridge.hookAllConstructors(Application.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(
MethodHookParam param)
throws Throwable {
// 记录context
Context context = (Context)param.thisObject;
// hook FPS相关
hookFPS(context);
}
});
if (unhookApplicationConstructor != null) {
mUnhookList.addAll(unhookApplicationConstructor);
}
}

这么做的目的是为了当应用程序进程创建时才hook,并且能够获取一个Application Context,这样后续可以调用一些系统服务。

2.2 Hook Handler.dispatchMessage

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
private void hookFPS(final Context context) {
Unhook unhook = XposedHelpers.findAndHookMethod(Handler.class,
"dispatchMessage", Message.class, new XC_MethodHook() {
private long lastTime = 0;
@Override
protected void beforeHookedMethod(
MethodHookParam param)
throws Throwable {
lastTime = System.currentTimeMillis();
}
@Override
protected void afterHookedMethod(
MethodHookParam param)
throws Throwable {
Message message = (Message) param.args[0];
if (message == null) {
return;
}
long usedTime = System.currentTimeMillis() - lastTime;
mCurrentActivity = getCurrentActivityName(context);
addHandlerMessage(message,usedTime);
}
});
mUnhookList.add(unhook);
}

原理篇中已经说明了为什么需要hook dispatchMessage,这里记录了dispatchMessage耗时,并且获取当前顶层的Activity用作分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private synchronized void addHandlerMessage(Message message, long usedTime) {
AnalyzerMessage analyzerMessage = new AnalyzerMessage();
analyzerMessage.MessageType = AnalyzerMessage.ANALYZER_MESSAGE_TYPE.ANALYZER_MESSAGE_TYPE_HANDLER;
analyzerMessage.PackageName = AndroidAppHelper.currentPackageName();
analyzerMessage.ActivityName = mCurrentActivity;
analyzerMessage.MessageTarget = message.getTarget().toString();
analyzerMessage.MessageWhat = message.what;
analyzerMessage.UsedTime = usedTime;
analyzerMessage.MessageObject = message.obj;
addMessage(analyzerMessage);
}
private synchronized void addMessage(AnalyzerMessage analyzerMessage) {
mMessageQueue.add(analyzerMessage);
}

这里将系统Message类型转换成自定义的消息类型AnalyzerMessage,因为在异步分析线程中无法获取之前应用的相关信息,例如顶层Acitivity的名称,因此需要转换记录。

1
private final LinkedBlockingQueue<AnalyzerMessage> mMessageQueue = new LinkedBlockingQueue<AnalyzerMessage>();

mMessageQueue为阻塞队列,分析线程需要读取它并进行分析。

2.3 GET_TASKS权限问题

前面我们需要获取当前Activity的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
private String getCurrentActivityName(Context context) {
try {
ActivityManager activityManager=(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> runningTaskInfo = activityManager.getRunningTasks(1);
if (runningTaskInfo != null && runningTaskInfo.size() >= 1) {
return runningTaskInfo.get(0).topActivity.getClassName();
}
} catch(Exception ex) {
ex.printStackTrace();
}
return "UNKNOWN";
}

获取当前任务栈需要android.permission.GET_TASKS权限,很明显只有很少一部分应用会使用到,因此在在这些应用进程中获取栈信息会抛异常导致无法获取到。这时需要hook掉ActivityManagerService,跳过权限检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
XposedHelpers.findAndHookMethod(ActivityManagerService.class,
"checkCallingPermission", String.class,new XC_MethodHook() {
@Override
protected void beforeHookedMethod(
MethodHookParam param)
throws Throwable {
param.setResult(0);
}
});
/*
XposedHelpers.findAndHookMethod(ActivityManagerService.class,
"isGetTasksAllowed", String.class, int.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(
MethodHookParam param)
throws Throwable {
param.setResult(true);
}
});
*/

系统版本不一样判断也不一样,例如4.2上是ActivityManagerService.checkCallingPermission,6.0是isGetTasksAllowed,不过效果都一样。这里为了方便跳过了所有权限检查,囧rz,可以判断一下。

3. 分析子线程

3.1. 线程构造函数

1
2
3
4
5
private final BlockingQueue<AnalyzerMessage> mMessageQueue ;
public AsynAnalyzer(BlockingQueue<AnalyzerMessage> messageQueue) {
mMessageQueue = messageQueue;
}

这里需要将主线程中创建的消息队列传入,这样子线程就可以等待处理了。

3.2 线程run函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void run() {
while(true) {
try {
AnalyzerMessage message = mMessageQueue.take();
if (message == null) {
break;
}
if(message.MessageType == AnalyzerMessage.ANALYZER_MESSAGE_TYPE.ANALYZER_MESSAGE_TYPE_ACTIVITY_IN) {
processActivityMessage(message);
} else if (message.MessageType == AnalyzerMessage.ANALYZER_MESSAGE_TYPE.ANALYZER_MESSAGE_TYPE_HANDLER) {
processHandlerMessage(message);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
Log.e("FPSAnalyzer","FPSAnalyzer 中断退出");
break;
}
}
}

子线程中通过MessageQueue.take()阻塞等待主线程发来的消息。

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
private void processHandlerMessage(AnalyzerMessage message) {
if (message.PackageName == null || message.PackageName.equals("android")) {
return;
}
if (message.MessageTarget.contains("android.view.Choreographer")) {
processChoreographer(message);
return;
} else if (message.MessageTarget.contains("android.app.ActivityThread$H")) {
prorocessActivityThread(message);
return;
} else {
// Other Handler
}
}
private void processChoreographer(AnalyzerMessage message) {
String dumpInfo = message.MessageTarget + " : " + ChoreographerMessageWhat.codeToString(message.MessageWhat) + ":" + message.PackageName + ":"+ message.ActivityName;
if (message.UsedTime > 40) {
dumpInfo = "FPSAnalyzer DISPATCH FRAME(>40):"+message.UsedTime+" INFO:" + dumpInfo;
Log.e("FPSAnalyzer",dumpInfo);
} else {
dumpInfo = "FPSAnalyzer DISPATCH FRAME:"+message.UsedTime+" INFO:" + dumpInfo;
Log.i("FPSAnalyzer",dumpInfo);
}
}

消息中发送的目标为android.view.Choreographer,那么就知道这个是同步帧的消息了。这里将相关信息打印出来,效果见原理一篇。当然信息更好是记录到文件中,再进行分析。
以下是简单分析:
FPS统计图表

4. 总结

  这里描述了如何通过xposed hook方式,获取到相关的信息,并通过简单的消息处理异步框架,在不影响应用主线程的情况进行记录。