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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容