实现Android下的FPS实时显示工具

来源:互联网 发布:radius协议认证端口 编辑:程序博客网 时间:2024/05/16 04:16

http://blog.csdn.net/jinzhuojun/article/details/10428435

原文地址:http://blog.csdn.NET/ariesjzj/article/details/10428435

FPS是图形性能的主要指标之一,Android中的一些应用有显示FPS的功能,如Bsplayer,Skype,Antutu等,但绝大多数应用并不提供显示FPS的功能。而且应用提供的往往是应用本身的刷新率,并不等于最终用户所看到的刷新率,因为屏幕上往往不止一个应用参与显示。我们知道Android中每个应用都会绘制自己的Surface,完了都丢给Surfaceflinger,Surfaceflinger统一对它们进行composition,然后swap framebuffer输出到屏幕。前文介绍了Android中的so注入和hook技巧(http://blog.csdn.net/ariesjzj/article/details/9900105),示例了如何动态hook系统中的Surfaceflinger中的eglSwapBuffers函数。那么很自然的,我们就可以通过它来计算当前的FPS,并实时显示在屏幕上。


ARM下有FPS实时显示工具-FPS meter,不过要收费。本文中我们自己做一个功能类似的免费工具,而且x86和ARM平台通用。整个应用分为几个部分,首先是用于so注入的native程序和要注入的动态链接库。这部分是前文(http://blog.csdn.net/ariesjzj/article/details/9900105)中主要涉及的内容,不再累述。基本思想是在要注入的so中定义自己的eglSwapBuffers函数,然后在初始化时将之替换got表中老的eglSwapBuffers函数地址。这样,当Surfaceflinger需要刷新屏幕时,就会先调用我们定义的eglSwapBuffers函数,在这个函数被调用时,它会记录和统计调用次数,并写入一个专用的pipe文件,然后调用系统自己的eglSwapBuffers。在应用端我们需要以下几个部分:一个Activity用于显示界面与用户交互,一个Service用于主要工作,即从pipe读取FPS信息并且实时显示在屏幕上,最后是一个native的程序,用于在service启动时完成so的注入。


大体流程如下:

Activity启动时根据平台ABI将相应版本的用于注入的native程序和要注入的so拷贝到应用私有目录:

[java] view plain copy
 print?
  1. File file_inject = new File(APP_PATH + "inject");  
  2. File file_lib = new File(APP_PATH + "libfpsshow.so");  
  3. if (!file_inject.exists() || !file_lib.exists()) {  
  4.     String sysabi = getSystemProp("ro.product.cpu.abi");  
  5.     Log.e(TAG, "System ABI is: " + sysabi + "\n");  
  6.     if (sysabi.startsWith("armeabi")) {  
  7.         copyFile("inject_arm", APP_PATH + "inject");  
  8.         copyFile("libfpsshow_arm.so", APP_PATH + "libfpsshow.so");  
  9.     } else if (sysabi.startsWith("x86")) {  
  10.         copyFile("inject", APP_PATH + "inject");  
  11.         copyFile("libfpsshow.so", APP_PATH + "libfpsshow.so");  
  12.     } else {  
  13.         Log.e(TAG, "ABI not supported\n");  
  14.         Toast.makeText(this"ABI not supported", Toast.LENGTH_LONG).show();  
  15.     }  
  16. else {  
  17.     Log.d(TAG, "Already copied\n");  
  18. }  
然后等待用户启动service:
[java] view plain copy
 print?
  1. case R.id.buttonStart:  
  2.     Log.d(TAG, "starting service");  
  3.     startService(new Intent(this, FPSService.class));  
  4.     break;  
  5. case R.id.buttonStop:  
  6.     Log.d(TAG, "stopping service");  
  7.     stopService(new Intent(this, FPSService.class));  
  8.     break;  
Service启动时的onCreate()函数,做一坨初始化工作,包括创建pipe,显示悬浮文字,执行注入等:
[java] view plain copy
 print?
  1. // Create the pipe file for receiving data  
  2. createPipe();  
  3. // Create floating textview to display FPS  
  4. createLayout();  
  5.   
  6. // Inject and hook  
  7. ArrayList<String> list = new ArrayList<String>();  
  8. list.add("chmod 775 " + APP_PATH + "inject");  
  9. list.add("chmod 666 " + APP_PATH + "pipe");  
  10. list.add("chmod 775 " + APP_PATH + "libfpsshow.so");  
  11. list.add(APP_PATH + "inject");  
  12.   
  13. // Execute as root  
  14. if (execute(list)) {  
  15.     Log.e(TAG, "OK\n");  
  16. else {  
  17.     Toast.makeText(this"Execute abnormally, please make sure it's rooted.", Toast.LENGTH_LONG).show();  
  18.     Log.e(TAG, "Error\n");  
  19.     this.stopSelf();  
  20. }  
其中的执行注入和hook程序是要root权限的,所以要通过:
[java] view plain copy
 print?
  1. public final boolean execute(ArrayList<String> commands) {  
  2.     ...  
  3.     Process suProcess = Runtime.getRuntime().exec("su");  
  4.     DataOutputStream os = new DataOutputStream(suProcess.getOutputStream());  
  5.     for (String currCommand : commands) {  
  6.         os.writeBytes(currCommand + "\n");  
  7.         os.flush();  
  8.     }  
  9.     os.writeBytes("exit\n");  
  10.     os.flush();  
  11.     int suProcessRetval = suProcess.waitFor();  
  12.     ...  
  13. }  
加悬浮文字,其实就是加个Layout:
[java] view plain copy
 print?
  1. windowManager = (WindowManager) getApplicationContext().getSystemService("window");    
  2. layoutParams = new WindowManager.LayoutParams();    
  3. layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;    
  4. layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;    
  5. layoutParams.format = PixelFormat.RGBA_8888;    
  6. layoutParams.gravity = Gravity.TOP | Gravity.CENTER;    
  7. layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;    
  8. layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;    
  9. layoutParams.x = 0;    
  10. layoutParams.y = 0;    
  11. // myLayout is the customized layout which contains textview  
  12. myLayout = new MyLayout(this);  
  13. windowManager.addView(myLayout, layoutParams);    

Service启动时onStartCommand()会被调用,其中会启动线程:

[java] view plain copy
 print?
  1. @Override  
  2. public int onStartCommand(Intent intent, int flags, int startId) {  
  3.     Log.v(TAG, "onStartCommand");  
  4.     Toast.makeText(this"Service Started", Toast.LENGTH_LONG).show();  
  5.     handleCommand(intent);  
  6.     myhandler.postDelayed(myTasks, 1000);  
  7.     return Service.START_STICKY;  
  8. }  
线程每一秒执行一次run()函数,该函数从pipe读Surfaceflinger传来的FPS,然后显示在屏幕上。
[java] view plain copy
 print?
  1. private Runnable myTasks = new Runnable() {  
  2.   
  3.     @Override  
  4.     public void run() {   
  5.         int fps = readFps();  
  6.         Log.e(TAG, "Service FPS = " + fps + "\n");  
  7.       
  8.         myLayout.setFPS(fps);  
  9.           
  10.         // Do other customized computation.  
  11.         ...  
  12.           
  13.         myLayout.setFPSAvg(fps_avg);  
  14.         myhandler.postDelayed(myTasks, 1000);  
  15.     }  
  16. };  

实现的时候还有些细节需要考虑,比如当启动多个应用后,后台非关键Service会被Android杀掉。为了解决这个问题,可以仿照Android官方例子中ApiDemo中的ForegroundService例子将Service设为前台进程,这样就不容易被杀了。另外在屏幕显示FPS的话,如果一秒更新一次,那显示FPS本身也会影响FPS(尽管只有一帧),如果觉得有影响可以把它关掉,让FPS只输出到log。

效果截图:



2


0 0
原创粉丝点击