如何更快速有效的收集Android應用的FPS
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();
}
}
}
}
}
相關文章
- 技術乾貨 | 反外掛技術的革新:如何有效應對 FPS 外掛的威脅
- 在 Android 12 中構建更現代的應用 WidgetAndroid
- Android O 讓安裝應用更安全Android
- Android動態更換應用圖示Android
- 親測有效 | 如何更高效的管理原生微服務應用微服務
- 快速上手系列--Android應用開發模板Android
- excel應用技巧:如何高效應用資料有效性功能Excel
- [譯] 如何優化您的 Android 應用(Go 版)優化AndroidGo
- 如何快速部署容器化應用
- 更相減損術的應用
- Android 開發實用程式碼收集Android
- Android應用開發中如何使用隱藏的APIAndroidAPI
- 「Eolink Apikit 」如何快速建立有效的API監控任務?API
- 影響FMEA有效應用的因素是什麼?
- 教你如何在快應用中跳轉到Android的appAndroidAPP
- 如何快速查詢網站有效子域名網站
- 如何快速開發一款應用
- Flutter應用如何快速增加雲同步功能Flutter
- 探究 lua 在 Android 中的應用Android
- .NET for Android/iOS應用的如何在各自的系統執行AndroidiOS
- [譯] 更為詳細的地圖、導航和助手功能 —— Google I/O 2018 的 Android 應用更新地圖GoAndroid
- 如何在 Android 專案中應用 OpenCV?AndroidOpenCV
- 談談如何構建有效的資料供應鏈
- MQMQ的快速入門+應用場景MQ
- 在資料庫繁忙時如何快速有效的關閉MySQL服務資料庫MySql
- Laravel wallet ,如何快速開發出一個錢包功能的應用?Laravel
- Android Native Crash 收集Android
- 爬蟲技術解析:如何有效地收集網路資料爬蟲
- 如何快速應用機器學習技術?機器學習
- 如何快速開發一款應用程式?
- [譯] 如何用 Android vitals 解決應用程式的質量問題Android
- 你釋出的Android 應用安全嗎?Android
- Android基礎及應用 Intent的呼叫AndroidIntent
- Android基礎及應用 Service的使用Android
- 我的第一個Android應用程式Android
- [譯]PostgreSQL中更安全的應用使用者SQL
- 愛立信:更智慧、更快速、更安全的“員工”的崛起
- Win10日曆應用怎麼更換主題_Win10日曆應用如何更換背景Win10
- 什麼是銀行資料治理?如何進行有效的銀行領域的實際應用?