TinyDancer 分析

  上一篇 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 去展示当前界面绘制的帧率情况。效果图类似下图所示:


TinyDancer

  那么上述功能具体是怎么实现的呢,那么就从在 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 calculateMetric(FPSConfig fpsConfig,
                                                                        List dataSet,
                                                                        List 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, realAnswer);
    }

  计算过程在上述过程中都有详细的描述,当计算完成之后,便开始执行界面刷新逻辑步骤,刷新步骤也特别简单,只是简简单单的完成背景颜色的更新以及文案的修改!

public void showData(FPSConfig fpsConfig, List dataSet) {
        ...

        AbstractMap.SimpleEntry 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 渲染速度和绘制过度

你可能感兴趣的:(TinyDancer 分析)