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();
}
}
}
}
}