實現Android下的FPS實時顯示工具

FPS是圖形性能的主要指標之一,Android中的一些應用有顯示FPS的功能,如Bsplayer,Skype,Antutu等,但絕大多數應用並不提供顯示FPS的功能。而且應用提供的往往是應用本身的刷新率,並不等於最終用戶所看到的刷新率,因為屏幕上往往不止一個應用參與顯示。我們知道Android中每個應用都會繪制自己的Surface,完瞭都丟給Surfaceflinger,Surfaceflinger統一對它們進行composition,然後swap framebuffer輸出到屏幕。前文介紹瞭Android中的so註入和hook技巧(https://blog.csdn.net/ariesjzj/article/details/9900105),示例瞭如何動態hook系統中的Surfaceflinger中的eglSwapBuffers函數。那麼很自然的,我們就可以通過它來計算當前的FPS,並實時顯示在屏幕上。

 

ARM下有FPS實時顯示工具-FPS meter,不過要收費。本文中我們自己做一個功能類似的免費工具,而且x86和ARM平臺通用。整個應用分為幾個部分,首先是用於so註入的native程序和要註入的動態鏈接庫。這部分是前文(https://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拷貝到應用私有目錄:

File file_inject = new File(APP_PATH + "inject");  

File file_lib = new File(APP_PATH + "libfpsshow.so");  

if (!file_inject.exists() || !file_lib.exists()) {  

    String sysabi = getSystemProp("ro.product.cpu.abi");  

    Log.e(TAG, "System ABI is: " + sysabi + "\n");  

    if (sysabi.startsWith("armeabi")) {  

        copyFile("inject_arm", APP_PATH + "inject");  

        copyFile("libfpsshow_arm.so", APP_PATH + "libfpsshow.so");  

    } else if (sysabi.startsWith("x86")) {  

        copyFile("inject", APP_PATH + "inject");  

        copyFile("libfpsshow.so", APP_PATH + "libfpsshow.so");  

    } else {  

        Log.e(TAG, "ABI not supported\n");  

        Toast.makeText(this, "ABI not supported", Toast.LENGTH_LONG).show();  

    }  

} else {  

    Log.d(TAG, "Already copied\n");  

}  

然後等待用戶啟動service:

case R.id.buttonStart:  

    Log.d(TAG, "starting service");  

    startService(new Intent(this, FPSService.class));  

    break;  

case R.id.buttonStop:  

    Log.d(TAG, "stopping service");  

    stopService(new Intent(this, FPSService.class));  

    break;  

Service啟動時的onCreate()函數,做一坨初始化工作,包括創建pipe,顯示懸浮文字,執行註入等:

// Create the pipe file for receiving data  

createPipe();  

// Create floating textview to display FPS  

createLayout();  

  

// Inject and hook  

ArrayList<String> list = new ArrayList<String>();  

list.add("chmod 775 " + APP_PATH + "inject");  

list.add("chmod 666 " + APP_PATH + "pipe");  

list.add("chmod 775 " + APP_PATH + "libfpsshow.so");  

list.add(APP_PATH + "inject");  

  

// Execute as root  

if (execute(list)) {  

    Log.e(TAG, "OK\n");  

} else {  

    Toast.makeText(this, "Execute abnormally, please make sure it's rooted.", Toast.LENGTH_LONG).show();  

    Log.e(TAG, "Error\n");  

    this.stopSelf();  

}  

其中的執行註入和hook程序是要root權限的,所以要通過:

public final boolean execute(ArrayList<String> commands) {  

    …  

    Process suProcess = Runtime.getRuntime().exec("su");  

    DataOutputStream os = new DataOutputStream(suProcess.getOutputStream());  

    for (String currCommand : commands) {  

        os.writeBytes(currCommand + "\n");  

        os.flush();  

    }  

    os.writeBytes("exit\n");  

    os.flush();  

    int suProcessRetval = suProcess.waitFor();  

    …  

}  

加懸浮文字,其實就是加個Layout:

windowManager = (WindowManager) getApplicationContext().getSystemService("window");    

layoutParams = new WindowManager.LayoutParams();    

layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;    

layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;    

layoutParams.format = PixelFormat.RGBA_8888;    

layoutParams.gravity = Gravity.TOP | Gravity.CENTER;    

layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;    

layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;    

layoutParams.x = 0;    

layoutParams.y = 0;    

// myLayout is the customized layout which contains textview  

myLayout = new MyLayout(this);  

windowManager.addView(myLayout, layoutParams);    

Service啟動時onStartCommand()會被調用,其中會啟動線程:

@Override  

public int onStartCommand(Intent intent, int flags, int startId) {  

    Log.v(TAG, "onStartCommand");  

    Toast.makeText(this, "Service Started", Toast.LENGTH_LONG).show();  

    handleCommand(intent);  

    myhandler.postDelayed(myTasks, 1000);  

    return Service.START_STICKY;  

}  

線程每一秒執行一次run()函數,該函數從pipe讀Surfaceflinger傳來的FPS,然後顯示在屏幕上。

private Runnable myTasks = new Runnable() {  

  

    @Override  

    public void run() {   

        int fps = readFps();  

        Log.e(TAG, "Service FPS = " + fps + "\n");  

      

        myLayout.setFPS(fps);  

          

        // Do other customized computation.  

        …  

          

        myLayout.setFPSAvg(fps_avg);  

        myhandler.postDelayed(myTasks, 1000);  

    }  

};  

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *