上一篇 Animator 有提到过 Animator 界面刷新逻辑主要是通过 Choreographer 来完成,本篇主角 TinyDancer 其实也是借助于Choreographer 来完成实时对帧率的检测,判断界面的卡顿情况,下面来简要分析一下 TinyDancer 工作原理。
TinyDancer的基本使用方法也比较简单,在 github 上也有很清楚是的说明,使用方法如下:
// In your DebugApplication class:
public class DebugApplication extends Application {
@Override public void onCreate() {
TinyDancer.create()
.show(context);
//alternatively
TinyDancer.create()
.redFlagPercentage(.1f) // set red indicator for 10%....different from default
.startingXPosition(200)
.startingYPosition(600)
.show(context);
//you can add a callback to get frame times and the calculated
//number of dropped frames within that window
TinyDancer.create()
.addFrameDataCallback(new FrameDataCallback() {
@Override
public void doFrame(long previousFrameNS, long currentFrameNS, int droppedFrames) {
//collect your stats here
}
})
.show(context);
}
}
在build.gradle中配置如下:
dependencies {
debugCompile "com.github.brianPlummer:tinydancer:0.1.2"
releaseCompile "com.github.brianPlummer:tinydancer-noop:0.1.2"
testCompile "com.github.brianPlummer:tinydancer-noop:0.1.2"
}
按照上述配置完成后,便可以正常的使用该功能了,在各个界面的上方,会有一个float winodw 去展示当前界面绘制的帧率情况。效果图类似下图所示:
那么上述功能具体是怎么实现的呢,那么就从在 application 初始化逻辑处开始分析吧,TinyDancer 初始化代码如下:
public static TinyDancerBuilder create(){
return new TinyDancerBuilder();
}
可以看到 create() 方法仅仅只是创建了一份TinyDancerBuilder实例出来,这个也是一种开源框架中常见的构造模式,来看看 TinyDancerBuilder 里面都做了些什么吧。
protected TinyDancerBuilder(){
fpsConfig = new FPSConfig();
}
可以看到,在 TinyDancerBuilder 构造方法中,仅仅只是创建了一份新的 FPSConfig 实例出来,可配置信息有哪些呢,看下 FPSConfig 的成员变量先:
public float redFlagPercentage = 0.2f; //
public float yellowFlagPercentage = 0.05f; //
public float refreshRate = 60; //60fps
public float deviceRefreshRateInMs = 16.6f; //value from device ex 16.6 ms
// starting coordinates
public int startingXPosition = 200;
public int startingYPosition = 600;
public int startingGravity = DEFAULT_GRAVITY;
public boolean xOrYSpecified = false;
public boolean gravitySpecified = false;
可以看到在 FPSConfig中,仅仅只是定义了悬浮窗的相关信息,如展示位置,颜色渐变区间等等。无关紧要,那么悬浮窗是怎么展示的,数据又是什么时候变化的呢,直接看 builder 的show() 方法吧。
/**
* show fps meter, this regisers the frame callback that
* collects the fps info and pushes it to the ui
* @param context
*/
public void show(Context context) {
// 首先要去申请悬浮窗权限. 在 SDK 23 以上
// 这个权限是必须的。
if (overlayPermRequest(context)) {
//once permission is granted then you must call show() again
return;
}
// set device's frame rate info into the config
setFrameRate(context);
// create the presenter that updates the view
tinyCoach = new TinyCoach((Application) context.getApplicationContext(), fpsConfig);
// create our choreographer callback and register it
fpsFrameCallback = new FPSFrameCallback(fpsConfig, tinyCoach);
Choreographer.getInstance().postFrameCallback(fpsFrameCallback);
//set activity background/foreground listener
Foreground.init((Application) context.getApplicationContext()).addListener(foregroundListener);
}
在show() 方法中,首先需要判断应用本身的 overLay 权限是否打开,否则无法展示悬浮窗。 紧接着创建了一份 TinyCoach 实例出来,看下这个构造方法吧:
public TinyCoach(Application context, FPSConfig config) {
fpsConfig = config;
//create meter view
meterView = LayoutInflater.from(context).inflate(R.layout.meter_view, null);
//set initial fps value....might change...
((TextView) meterView).setText((int) fpsConfig.refreshRate + "");
// grab window manager and add view to the window
windowManager = (WindowManager) meterView.getContext().getSystemService(Service.WINDOW_SERVICE);
addViewToWindow(meterView);
}
可以看到,在构造方法中,tinyCoach 创建了悬浮窗并且通过 windowManager 添加到了屏幕上,那么悬浮窗里面的数据是如何展示以及产生变化的呢,继续跟随 show() 方法往下看,可以看到,新生成了一份 FPSFrameCallback 实例,这个实例继承自Choreographer.FrameCallback ,因此可以想到,悬浮窗的数据刷新逻辑也是基于 callback 的回调来做的,那么直接回到回调的 doFrame() 方法来看看吧。
@Override
public void doFrame(long frameTimeNanos)
{
//if not enabled then we bail out now and don't register the callback
if (!enabled){
destroy();
return;
}
//initial case
if (startSampleTimeInNs == 0){
startSampleTimeInNs = frameTimeNanos;
}
// only invoked for callbacks....
else if (fpsConfig.frameDataCallback != null)
{
long start = dataSet.get(dataSet.size()-1);
int droppedCount = Calculation.droppedCount(start, frameTimeNanos, fpsConfig.deviceRefreshRateInMs);
fpsConfig.frameDataCallback.doFrame(start, frameTimeNanos, droppedCount);
}
//we have exceeded the sample length ~700ms worth of data...we should push results and save current
//frame time in new list
if (isFinishedWithSample(frameTimeNanos))
{
collectSampleAndSend(frameTimeNanos);
}
// add current frame time to our list
dataSet.add(frameTimeNanos);
//we need to register for the next frame callback
Choreographer.getInstance().postFrameCallback(this);
}
在方法的开头,判断该功能有没有被开启,如果被用户禁止了,那么直接返回。接下来,如果在 application 中有手动设置过 frameDataCallback 回调,那么首先计算两次刷新之间的掉帧个数,并且回调回去,供用户后续处理。简单看一下 FrameDataCallback 说明吧,代码如下:
public interface FrameDataCallback
{
/**
* this is called for every doFrame() on the choreographer callback
* use this very judiciously. Logging synchronously from here is a bad
* idea as doFrame will be called every 16-32ms.
* @param previousFrameNS previous vsync frame time in NS
* @param currentFrameNS current vsync frame time in NS
* @param droppedFrames number of dropped frames between current and previous times
*/
void doFrame(long previousFrameNS, long currentFrameNS, int droppedFrames);
}
计算掉帧方式其实也很简单,其实就是当前刷新时间和上一次的刷新时间,然后进行除法处理,代码如下:
public static int droppedCount(long start, long end, float devRefreshRate){
int count = 0;
long diffNs = end - start;
long diffMs = TimeUnit.MILLISECONDS.convert(diffNs, TimeUnit.NANOSECONDS);
long dev = Math.round(devRefreshRate);
if (diffMs > dev) {
long droppedCount = (diffMs / dev);
count = (int) droppedCount;
}
return count;
}
接下来就到了最关键的一步,界面上的数据是如何更新的,什么时候更新,继续往下看代码吧,应用会每隔 700ms 去收集一些数据然后去更新界面,通过收集到的数据来更新界面展示,计算相关的代码如下:
public static AbstractMap.SimpleEntry<Metric, Long> calculateMetric(FPSConfig fpsConfig,
List<Long> dataSet,
List<Integer> droppedSet)
{
long timeInNS = dataSet.get(dataSet.size() - 1) - dataSet.get(0);
// 计算在理想情况下应该刷新帧的总次数.
long size = getNumberOfFramesInSet(timeInNS, fpsConfig);
//metric
int runningOver = 0;
// total dropped
int dropped = 0;
for(Integer k : droppedSet){
// 统计该时间段内所有掉帧的个数
dropped+=k;
if (k >=2) {
// 统计连续掉帧的个数
// 即一次刷新期间,掉帧超过2个的次数
runningOver+=k;
}
}
// 以每秒刷新 60 次为基准,计算没有掉帧占用的比例
float multiplier = fpsConfig.refreshRate / size;
float answer = multiplier * (size - dropped);
long realAnswer = Math.round(answer);
// calculate metric
// 计算连续掉帧占用的比例
float percentOver = (float)runningOver/(float)size;
Metric metric = Metric.GOOD;
if (percentOver >= fpsConfig.redFlagPercentage) {
metric = Metric.BAD;
} else if (percentOver >= fpsConfig.yellowFlagPercentage) {
metric = Metric.MEDIUM;
}
return new AbstractMap.SimpleEntry<Metric, Long>(metric, realAnswer);
}
计算过程在上述过程中都有详细的描述,当计算完成之后,便开始执行界面刷新逻辑步骤,刷新步骤也特别简单,只是简简单单的完成背景颜色的更新以及文案的修改!
public void showData(FPSConfig fpsConfig, List<Long> dataSet) {
...
AbstractMap.SimpleEntry<Calculation.Metric, Long> answer = Calculation.calculateMetric(fpsConfig, dataSet, droppedSet);
... 分情况去改变背景色
if (answer.getKey() == Calculation.Metric.BAD) {
meterView.setBackgroundResource(R.drawable.fpsmeterring_bad);
} else if (answer.getKey() == Calculation.Metric.MEDIUM) {
meterView.setBackgroundResource(R.drawable.fpsmeterring_medium);
} else {
meterView.setBackgroundResource(R.drawable.fpsmeterring_good);
}
((TextView) meterView).setText(answer.getValue() + "");
}
自此,TinyDancer 流程就已经全部分析完毕,它在检测应用绘制方面还是起到了一定的参考作用,额外的,用户还可以在 系统 settings 界面打开 GPU 呈现模式分析,可以看出具体卡帧的原因,具体原因可以参考 Google 官方文档。
检查 GPU 渲染速度和绘制过度