如何更快速有效的收集Android應用的FPS

CharliChen發表於2016-12-14

FPS是什麼?

FPS(每秒傳輸幀數(Frames Per Second))是影象領域中的定義,是指畫面每秒傳輸幀數,通俗來講就是指動畫或視訊的畫面數,對應的就是APP UI介面的刷行頻率,在一個UI動畫的播放過程中,fps越大,介面表現越流暢,fps越低,介面表現越卡頓,因此,測量FPS經常用於評價一個APP的流暢度,以此來判定APP是否能帶來更好的使用者體驗。


我們是如何收集APP幀率的?

通常情況下,我們可以通過以下三種方式收集到APP的fps:

1、裝置連線usb資料線,使用adb除錯工具,輸入命令:adb shell dumpsys gfxinfo <pagekagename>,隨後對返回的資料進行適當處理便可以得到此時此刻app的fps。這種方式是最普遍也是最常用的一種,但在使用上有明顯的痛點,一是裝置需要連線usb,二是adb命令返回的資料並不是實時fps,需要經過處理才能得到,因此不能在測試app的過程中實時顯示fps,或許你可以寫一個簡單的指令碼執行在pc端,在pc端顯示fps,但對測試人員來說,一邊看手機,一邊看電腦的體驗並不好。

2、通過在root的裝置上安裝第三方效能測試工具app,目前業界存在許多類似騰訊gt的效能測試工具app,安裝這個app到裝置後,便可以在測試app的過程中監控到被測app的所有效能資料,包括fps,一般也會有懸浮窗將效能資料實時顯示在介面上,方便測試人員測試,但有個大前提:獲取fps資料,裝置必須root;

3、修改被測app原始碼,通過Choreographer的回撥FrameCallback來計算Loop被執行了幾次,從而計算出應用的流暢度。這種方式得出的fps可能是最精確的,但是成本也是最大的。


無需資料線、無需root、無需更改被測APP原始碼,更快速有效的收集APP 幀率

能否不需要usb、不需要root、不需要更改app原始碼就能獲取到app的fps呢?答案是肯定的,技術實現的關鍵點就是,開發一個app,利用無線adb除錯,在app上模擬adb傳送dumpsys命令獲取到並計算裝置fps。

首先,我們需要開啟裝置的adb命令埠,使用adb命令:adb tcpip 5555,表示裝置的5555埠用於接收adb命令而不需要通過usb資料線。

隨後,在我們的app端,通過:adb connect 127.0.0.1:5555連線到裝置,連線成功後,我們可以構造socket,模擬shell命令“dumpsys gfxinfo“傳送到5555這個埠。

最後,接收5555埠的響應資料並對資料進行處理計算,便可以得到fps,並把fps通過懸浮窗顯示在app。

這樣,便改進了我們平時在收集fps過程中遇到的痛點和影響效率的地方,讓fps變得唾手可及。



JAVA程式碼實現

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;

public class FpsCount {
//取樣頻率,單位ms
    private static int WAITTIME = 1600;
    //取樣次數
    private static int RUNTIMES = 10;
    //gfxinfo用到的command
    private static String gfxCMD = "adb shell dumpsys gfxinfo com.huajiao";
    //需要監控的層    
    private static String layerName="SurfaceView";
    private static String[] command = { "cmd", "/c", "adb shell dumpsys SurfaceFlinger --latency "+layerName};
    //清空之前取樣的資料,防止統計重複的時間
    private static String[] clearCommand = { "cmd", "/c", "adb shell dumpsys SurfaceFlinger --latency-clear"};
    private static String[] comd_getUpTime = { "cmd", "/c", "adb shell cat /proc/uptime"};

    private static double MillSecds = 1000000.0;
    private static double NANOS = 1000000000.0;


    public static void main(String[] args) throws InterruptedException, ArrayIndexOutOfBoundsException {        
        try {
            for (int i = 0; i < RUNTIMES; i++) {
                if(layerName==null || "".equals(layerName)){
                    new RuntimeException("圖層獲取失敗!");
                }else{
                    Runtime.getRuntime().exec(clearCommand);
                    Thread.sleep(WAITTIME);
                    getFps(layerName);
                }
                System.out.println("<================第"+(i+1)+"次測試完成!===============>");
            }
        } catch (Exception e) {
            // TODO: handle exception
        }       
    }

    //計算fps 通過SurfaceFlinger --latency獲取
    @SuppressWarnings("unused")
    static void getFps(String layer){
        BufferedReader br = null,br2 = null,br3 = null;
        java.text.DecimalFormat df1=new java.text.DecimalFormat("#.0");
        java.text.DecimalFormat df2=new java.text.DecimalFormat("#.00");
        java.text.DecimalFormat df3=new java.text.DecimalFormat("#.000");       
        double refreshPriod=0; //裝置重新整理週期
        //這段是使用gfxinfo統計fps,可以刪掉
        try {
            Process prt = Runtime.getRuntime().exec(gfxCMD);
            br3 = new BufferedReader(new InputStreamReader(prt.getInputStream()));
            String line;
            boolean flag=false;
            int frames2=0,jankCount=0,vsync_overtime=0;
            float countTime=0;
            while((line = br3.readLine()) != null){
                if(line.length()>0){
                    if(line.contains("Execute")){
                        flag=true;
                        continue;
                    }
                    if(line.contains("View hierarchy:")){
                        flag=false;
                        continue;
                    }
                    if(flag){
                        if(!line.contains(":") && !line.contains("@")){
                            String[] timeArray = line.trim().split("    ");
                            float oncetime=Float.parseFloat(timeArray[0])+Float.parseFloat(timeArray[1])
                                    +Float.parseFloat(timeArray[2])+Float.parseFloat(timeArray[3]);
                            frames2+=1;
                            countTime=countTime+oncetime;
                            if(oncetime > 16.67){
                                jankCount+=1;
                                if(oncetime % 16.67 == 0){
                                    vsync_overtime += oncetime/16.67 - 1;
                                }else{
                                    vsync_overtime += oncetime/16.67;
                                }
                            }                           
                        }                       
                    }

                }
            }
            if((frames2 + vsync_overtime)>0){
                float ffps = frames2 * 60 / (frames2 + vsync_overtime);
                //System.out.println("gfxinfo方式 | 總幀數:"+frames2+" fps:"+ffps+" 跳幀數:"+jankCount);
            }
            //下面程式碼是利用制定層獲取fps的程式碼            
            //get device uptime         
            String upTime="";
            Process pt = Runtime.getRuntime().exec(comd_getUpTime);
            br2 = new BufferedReader(new InputStreamReader(pt.getInputStream()));
            String uptmeLine;
            while((uptmeLine = br2.readLine()) != null){
                if(uptmeLine.length()>0){
                    upTime = uptmeLine.split(" ")[0];
                }
            }           
            Process p = Runtime.getRuntime().exec(command);
            br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String returnInfo = null;
            double b = 0;
            int frames = 0,jank=0;
            double totalCountPeriod=0;
            String beginRenderTime="0.0",endRenderTime="0.0";
            double r=0;
            int count=1;    
            while((returnInfo = br.readLine()) != null){
                if(!"".equals(returnInfo) && returnInfo.length() > 0){
                    count++;
                    int frameSize = returnInfo.split("\\s{1,}").length;
                    if(frameSize==1){
                        refreshPriod = Double.parseDouble(returnInfo)/MillSecds;
                        b = 0;
                        frames = 0;
                        r=refreshPriod;
                    }else{

                        if(frameSize==3){
                            String[] timeStamps = returnInfo.split("\\s{1,}");
                            double t0 = Double.parseDouble(timeStamps[0]);
                            double t1 = Double.parseDouble(timeStamps[1]);
                            double t2 = Double.parseDouble(timeStamps[2]);
                            if(t1 > 0 && !"9223372036854775807".equals(timeStamps[1])){
                                if(b==0){
                                    b=t1;
                                    jank=0;
                                }else{
                                    double countPeriod = (t1-b)/MillSecds; //統計週期,大於500ms重新置為0
                                    if(countPeriod>500){
                                        if(frames>0){
                                            System.out.println(totalCountPeriod/1000);
                                            System.out.println("SurfaceFlinger方式(超時了) | 開始取樣時間點:"+beginRenderTime+"s   "
                                                    + "|結束取樣時間點:"+df3.format(b/NANOS)+"s   "
                                                    + "|fps:"+df2.format(frames*1000/totalCountPeriod)
                                                    + "   |Frames:"+frames
                                                    + "   |單幀平均渲染時間:"+df2.format(totalCountPeriod/frames)+"ms");
                                        }
                                        b=t1;
                                        frames=0;
                                        totalCountPeriod=0;
                                        jank=0;
                                    }else{
                                        frames+=1;
                                        if(countPeriod>r){
                                            totalCountPeriod+=countPeriod;
                                            if((t2-t0)/MillSecds > r){
                                                jank+=1;
                                            }
                                            b=t1;
                                        }else{
                                            totalCountPeriod+=r;
                                            b=Double.parseDouble(df1.format(b+r*MillSecds));
                                        }
                                    }
                                }           
                                if(frames==0){
                                    beginRenderTime=df3.format(t1/NANOS);
                                }
                            }
                        }
                    }
                }               
            }
            if(frames>0){
                System.out.println("SurfaceFlinger方式 | 開始取樣時間點:"+beginRenderTime+"s   "
                        + "|結束取樣時間點:"+df3.format(b/NANOS)+"s   "
                        + "|fps:"+df2.format(frames*1000/totalCountPeriod)
                        + "   |Frames:"+frames
                        + "   |單幀平均渲染時間:"+df2.format(totalCountPeriod/frames)+"ms");
            }else{
                System.out.println("獲取的層不正確  or 當前沒有渲染操作,請拖動螢幕");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {           
            if (br != null) {
                try {
                    Runtime.getRuntime().exec(clearCommand);
                    br.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }           
        }
    }
}


相關文章