摘要:描述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)param.thisObject; 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) { 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 { } } 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,那么就知道这个是同步帧的消息了。这里将相关信息打印出来,效果见原理一篇。当然信息更好是记录到文件中,再进行分析。
以下是简单分析:

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