Android 騰訊 Matrix 原理分析(四):TracePlugin 卡頓分析之丟幀展現(xiàn)

前言

前文分析了 TracePlugin 幀率分析的數(shù)據(jù)來源,本文將分析這些數(shù)據(jù)是如何計(jì)算和展示到 View 上的。

一、效果預(yù)覽

先來看一下官方 Demo 里面的效果:


Demo[圖片上傳中...(2.png-4ee64a-1612682706410-0)]

1.1 注意的點(diǎn)

從上面的 Demo 中可以看出:

  • 右上角展示幀率、統(tǒng)計(jì)柱狀圖。
    其實(shí)展示的是一個(gè)自定義
    View,將接收到的數(shù)據(jù)經(jīng)過計(jì)算得出幀率。

  • 滑動(dòng)一段時(shí)間之后,跳轉(zhuǎn)到結(jié)果頁面。
    搜集夠一定數(shù)量的數(shù)據(jù),報(bào)告 Issus 告知開發(fā)者。

  • 頁面靜止時(shí)幀率為 60,滑動(dòng)時(shí)幀率發(fā)生變化。
    當(dāng) View 沒有發(fā)生變化時(shí),不會(huì)請(qǐng)求刷新,展示的是系統(tǒng)幀率。
    當(dāng) View 滑動(dòng)時(shí),請(qǐng)求接收垂直同步信號(hào),再經(jīng)過計(jì)算得出幀率。

1.2 使用步驟

  1. 準(zhǔn)備需要檢測的幀率的 Activity 或者任何地方,通常是一些 View 比較復(fù)雜、涉及計(jì)算較多的展示頁面。

Demo 中展示的是一個(gè)持有 ListView 的 Activity,為了模擬卡頓效果,在每次觸摸 ListView 的時(shí)候主線程休眠一段時(shí)間。

mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        MatrixLog.i(TAG, "onTouch=" + motionEvent);
        SystemClock.sleep(80);
        return false;
    }
});

  1. 展示幀率 View,類 FrameDecorator 負(fù)責(zé)接收數(shù)據(jù)和展示幀率圖,通過 FrameDecorator 展示右上角的 View。
FrameDecorator decorator = FrameDecorator.getInstance(this);
// 檢測浮窗權(quán)限
if (!canDrawOverlays()) {
    requestWindowPermission();
} else {
    decorator.show();
}

請(qǐng)求浮窗權(quán)限: 幀率統(tǒng)計(jì)圖是一個(gè)自定義 View,并且由 WindowManager 添加,所以需要在 Android M(6.0) 以上的設(shè)備打開浮窗權(quán)限。

private boolean canDrawOverlays() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        return Settings.canDrawOverlays(this);
    } else {
        return true;
    }
}
  1. 經(jīng)過上面的步驟已經(jīng)展示出幀率 View,但是目前只是展示出幀率并沒有搜集數(shù)據(jù),接下來可以在需要的地方開啟幀率分析和報(bào)告。
Matrix.with().getPluginByClass(TracePlugin.class).getFrameTracer().onStartTrace();

開始收集數(shù)據(jù)需要先獲取 TracePlugin 卡頓分析插件,接著獲取 FrameTracer 并調(diào)用它的 onStartTrace() 方法開始統(tǒng)計(jì)。

  1. 幀率上報(bào)。

當(dāng)搜集到的幀率數(shù)據(jù)超過設(shè)置的時(shí)間后(Demo 中設(shè)置的是 10s,開發(fā)者可自行設(shè)置),便進(jìn)行上報(bào),這樣我們就可以通過某個(gè)時(shí)段的幀數(shù)來確定該頁面是否需要進(jìn)行優(yōu)化。

幀率上報(bào)

仔細(xì)觀察幀率統(tǒng)計(jì)圖,發(fā)現(xiàn)滑動(dòng)時(shí)幀數(shù)大幅下降。說明該頁面在主線程做了太多事情,需要考慮進(jìn)行優(yōu)化。

二、幀率數(shù)據(jù)從哪來?

先說結(jié)論,幀率數(shù)據(jù)從 UIThreadMonitor 來。

回調(diào)過程.png

整體邏輯也比較簡單:UIThreadMonitor 會(huì)監(jiān)聽系統(tǒng)垂直同步信號(hào)并維護(hù)監(jiān)聽者列表 observers,F(xiàn)rameTracer 開始工作后添加監(jiān)聽到列表 observers,等 UIThreadMonitor 接收到信號(hào)后遍歷回調(diào)列表告知即可。

接下來我們根據(jù)上圖把該順序的代碼剖析一下。

2.1 UIThreadMonitor 監(jiān)聽垂直同步信號(hào)

UIThreadMonitor 的工作原理在上篇文章中已經(jīng)詳細(xì)分析過,在此簡單記錄:

  1. UIThreadMonitor 內(nèi)部維護(hù) Choreographer 實(shí)例,該對(duì)象用來接收系統(tǒng) VSync 信號(hào);
  2. UIThreadMonitor 向 Choreographer 添加幀率回調(diào)監(jiān)聽,這樣 Choreographer 接收到系統(tǒng)信號(hào)時(shí)會(huì)通知 UIThreadMonitor;
  3. UIThreadMonitor 再遍歷回調(diào)給自己內(nèi)部維護(hù)的列表。

實(shí)際的邏輯要比這三步復(fù)雜的多,感興趣的可以回去看之前的文章:

Android 騰訊 Matrix 原理分析(三):TracePlugin 卡頓分析之幀率監(jiān)聽

2.2 FrameTracer 啟動(dòng)和監(jiān)聽

  1. FrameTracer 屬于 卡頓分析插件 TracePlugin 的一部分,所以也是由 TracePlugin 啟動(dòng)的:

TracePlugin # start()

@Override
public void start() {
    super.start();
    ...
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            ...
            frameTracer.onStartTrace();
            ...
        }
    };
    // 主線程啟動(dòng)
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        runnable.run();
    } else {
        MatrixLog.w(TAG, "start TracePlugin in Thread[%s] but not in mainThread!", Thread.currentThread().getId());
        MatrixHandlerThread.getDefaultMainHandler().post(runnable);
    }

}
  1. FrameTracer 的 onStartTrace() 方法會(huì)調(diào)用自己的 onAlive() 方法,而后會(huì)添加監(jiān)聽到 UIThreadMonitor。

FrameTracer # onAlive()

@Override
public void onAlive() {
    super.onAlive();
    UIThreadMonitor.getMonitor().addObserver(this);
}

需要注意的是添加的監(jiān)聽類型是 LooperObserver,也就是說只有繼承了 LooperObserver 類才能被添加到 UIThreadMonitor 維護(hù)的監(jiān)聽者列表。

2.3 UIThreadMonitor 回調(diào)監(jiān)聽

UIThreadMonitor 在監(jiān)聽主線程幀率事件后遍歷回調(diào):

UIThreadMonitor # dispatchEnd()

private void dispatchEnd() {
    ...
    synchronized (observers) {
        for (LooperObserver observer : observers) {
            if (observer.isDispatchBegin()) {
                observer.doFrame(AppMethodBeat.getVisibleScene(), startNs, endNs, isVsyncFrame, intendedFrameTimeNs, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
            }
        }
    }
    ...
}

可以看到最終執(zhí)行了 doFrame() 方法,而 FrameTracer 的 doFrame() 方法會(huì)將數(shù)據(jù)設(shè)置給幀率 View。

三、設(shè)置數(shù)據(jù)給幀率 View

還是先來看一下數(shù)據(jù)是如何設(shè)置到幀率 View 的:

數(shù)據(jù)傳遞流程

整個(gè)過程與上一章節(jié)類似,無法就是添加監(jiān)聽等待回調(diào),然后將數(shù)據(jù)給到 View 更新 UI。接下來逐步分析:

3.1 FrameTracer 監(jiān)聽列表

  1. FrameTracer 從 UIThreadMonitor 中得知幀率信息,這個(gè)過程不再贅述。
  2. FrameTracer 內(nèi)部維護(hù)一個(gè) IDoFrameListener 類型的列表,用來存儲(chǔ)監(jiān)聽者列表:
private final HashSet<IDoFrameListener> listeners = new HashSet<>();

IDoFrameListener 雖然叫 Listener 但其實(shí)是一個(gè)類,內(nèi)部用 LinkedList 添加每幀數(shù)據(jù)、使用 doFrameAsync() 方法執(zhí)行監(jiān)聽回調(diào)。

3.2 FrameDecorator 創(chuàng)建和添加監(jiān)聽

  1. FrameDecorator 由開發(fā)者手動(dòng)創(chuàng)建,它是一個(gè)單例:
FrameDecorator decorator = FrameDecorator.getInstance(this);
  1. FrameDecorator 的獲取單例方法中會(huì)創(chuàng)建幀率 View FloatFrameView,并且在構(gòu)造函數(shù)中添加監(jiān)聽到 FrameTracer。

FrameDecorator # getInstance()

public static FrameDecorator getInstance(final Context context) {
    if (instance == null) {
        // 主線程直接創(chuàng)建
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            // 這里創(chuàng)建的 FloatFrameView
            instance = new FrameDecorator(context, new FloatFrameView(context));
        } else {
            try {
                // 子線程同步鎖創(chuàng)建
                synchronized (lock) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                             // 這里創(chuàng)建的 FloatFrameView
                            instance = new FrameDecorator(context, new FloatFrameView(context));
                            synchronized (lock) {
                                lock.notifyAll();
                            }
                        }
                    });
                    lock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    return instance;
}
  • 單例為 null 且在主線程則直接創(chuàng)建;
  • 有意思的是子線程創(chuàng)建的邏輯,這里同時(shí)用到了同步鎖 synchronized 和對(duì)象鎖,鑒于篇幅在此不作分析,感興趣的朋友可以解析并分享一下。
  1. FrameDecorator 構(gòu)造器中,會(huì)在 FloatFrameView attach 到 Window 的時(shí)候?qū)?FrameDecorator 添加到 FrameTracer 的監(jiān)聽列表中:
private FrameDecorator(Context context, final FloatFrameView view) {
    ...
    view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {
            MatrixLog.i(TAG, "onViewAttachedToWindow");
            if (Matrix.isInstalled()) {
                TracePlugin tracePlugin = Matrix.with().getPluginByClass(TracePlugin.class);
                if (null != tracePlugin) { // 添加監(jiān)聽
                    FrameTracer tracer = tracePlugin.getFrameTracer();
                    tracer.addListener(FrameDecorator.this);
                }
            }
        }
...
    });
    ...
}

3.3 FrameTracer 收到幀率回調(diào)

  1. FrameTracer 用 doFrame() 方法接收 UIThreadMonitor 發(fā)來的幀率數(shù)據(jù),然后再遍歷自己維護(hù)的 listeners 列表,上面步驟提到過 FrameDecorator 實(shí)例也在這個(gè)列表中。

FrameTracer # doFrame

private final HashSet<IDoFrameListener> listeners = new HashSet<>();

@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    if (isForeground()) {
        notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    }
}
  1. notifyListener() 中遍歷通知所有監(jiān)聽。

FrameTracer # notifyListener()

private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
                              final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
      long traceBegin = System.currentTimeMillis();
      try {
          ...
          synchronized (listeners) {
              for (final IDoFrameListener listener : listeners) {
                  ...
                  listener.getExecutor().execute(new Runnable() {
                          @Override
                          public void run() {
                              // 執(zhí)行這里
                              listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                      intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                          }
                      });
               ...
              }
          }
      }
      ...
  }

可以看到主要調(diào)用監(jiān)聽者們的 doFrameAsync() 方法,這樣就又回到了 FrameDecorator 中。

3.4 FrameDecorator 更新 View

上一小節(jié)中來到了 FrameDecorator 的 doFrameAsync() 方法,該方法負(fù)責(zé)將數(shù)據(jù)傳遞給幀率 View。

  1. 經(jīng)過一番計(jì)算,調(diào)用 updateView() 方法:

FrameDecorator # doFrameAsync()

@Override
public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    ...
    long collectFrame = sumFrames - lastFrames[0];
    if (duration >= 200) {
        final float fps = Math.min(maxFps, 1000.f * collectFrame / duration);
        // 使用該方法更新 View
        updateView(view, fps, belongColor,
                dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index],
                dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index],
                dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index],
                dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]);
         ...
    }
}

dropLevelsumDropLevel 數(shù)組暫時(shí)先不看,把調(diào)用鏈搞清楚再去分析。

  1. 接下來到了 updateView() 方法:

FrameDecorator # updateView

private void updateView(final FloatFrameView view, final float fps, final int belongColor,
                          final int normal, final int middle, final int high, final int frozen,
                          final int sumNormal, final int sumMiddle, final int sumHigh, final int sumFrozen) {

      ...
      // 切換到主線程
      mainHandler.post(new Runnable() {
          @Override
          public void run() {
               // 展示數(shù)據(jù)
              view.chartView.addFps((int) fps, belongColor);
              view.fpsView.setText(fpsStr);
              view.fpsView.setTextColor(belongColor);

              view.qiWangView.setText(qiWangStr);
              ...
              view.sumQiWangView.setText(sumQiWangStr);
              ...
          }
      });
  }

注意兩條注釋:

  • 切換到主線程:因?yàn)樾枰M(jìn)行 ui 的更新,所以到主線程執(zhí)行;
  • 展示數(shù)據(jù):將傳遞來的數(shù)據(jù)設(shè)置給幀率 View FloatFrameView。

到這里數(shù)據(jù)傳遞的流程已經(jīng)基本理清了,接下來分析幀率的數(shù)值是如何計(jì)算出來的。

3.5 丟幀報(bào)告

回過頭來看插件報(bào)告捕捉到的一段時(shí)間內(nèi)的數(shù)據(jù):

幀率報(bào)告

主要看報(bào)告 json 中的部分內(nèi)容:

  • machine:設(shè)備名稱,因?yàn)橛玫哪M器所以沒能獲取到;
  • scene:場景,也就是在哪個(gè)地方捕捉的數(shù)據(jù),這里是一個(gè) Activity;
  • dropLevel: 丟幀等級(jí),Matrix 把丟幀分為四個(gè)等級(jí):
    • DROPPED_FROZEN: 丟幀嚴(yán)重;
    • DROPPED_HIGH: 高度丟幀;
    • DROPPED_MIDDLE: 中度丟幀;
    • DROPPED_NORMAL: 普通丟幀;
    • DROPPED_BEST: 低丟幀,最佳狀態(tài);
public enum DropStatus {
    DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
    public int index;

    DropStatus(int index) {
        this.index = index;
    }
}

丟幀數(shù)量屬于的等級(jí):

Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)
  • dropLevel: 掉幀統(tǒng)計(jì);

關(guān)于卡頓官方文檔是這么解釋的:

FPS 低并不意味著卡頓發(fā)生,而卡頓發(fā)生 FPS 一定不高。 FPS 可以衡量一個(gè)界面的流程性,但往往不能很直觀的衡量卡頓的發(fā)生,這里有另一個(gè)指標(biāo)(掉幀程度)可以更直觀地衡量卡頓。

所以 Matrix 使用 dropLevel 來統(tǒng)計(jì)一段時(shí)間內(nèi)的丟幀程度。打個(gè)比方,如果這段時(shí)間丟幀等級(jí)基本在 DROPPED_BEST(發(fā)生了丟幀,但是丟的數(shù)量在 3 以下),那么屬于比較完美的情況無需優(yōu)化。

而 Demo 中 :

  • DROPPED_MIDDLE(中度丟幀) 發(fā)生 24 次,所丟幀數(shù) 281;
    按照每秒 60 幀來計(jì)算,中度丟幀發(fā)生了將近 3s。
  • DROPPED_NORMAL(普通丟幀) 發(fā)生 31 次,所丟幀數(shù) 146;
    普通丟幀發(fā)生了 2s 多。
  • DROPPED_BEST(低丟幀)發(fā)生 237 次,所丟幀數(shù) 14。

所以在這 10s 中有將近 5s 發(fā)生了丟幀,說明當(dāng)前頁面存在問題需要優(yōu)化,需要檢查有沒有在主線程或 View 的更新上面執(zhí)行了復(fù)雜的邏輯。

  • fpx:幀率。計(jì)算出的平均幀數(shù)。

丟幀數(shù)量的計(jì)算

如何得知某一時(shí)間段丟幀的值呢?我們來看一下 Matrix 是怎么做的。

  1. 首先需要獲取設(shè)備的刷新率,嘗試反射獲取系統(tǒng)的值。獲取不到則使用默認(rèn)值:
private long frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION);
public static final long DEFAULT_FRAME_DURATION = 16666667L;

假設(shè)這臺(tái)設(shè)備刷新率 60,那么每 16ms 刷新一次,也就是 166666.... 納秒刷新一次。

  1. 獲取 VSync 垂直同步信號(hào)處理的時(shí)間。

接收到信號(hào)記錄當(dāng)前時(shí)間:

token = dispatchTimeMs[0] = System.nanoTime();

一次刷新處理完畢記錄時(shí)間:

long endNs = System.nanoTime();
  1. 計(jì)算所丟幀數(shù):
// 一次刷新處理的時(shí)間
final long jiter = endNs - intendedFrameTimeNs;
// 除以刷新率
final int dropFrame = (int) (jiter / frameIntervalNs);

如果一次刷新耗時(shí) 16ms,這臺(tái)設(shè)備 16ms 刷新一次,得出剛好丟失 1 幀。但是如果耗時(shí)不足 16ms,得出 0 說明不會(huì)丟幀。

總結(jié)

最后簡單總結(jié)下:

  1. 幀率數(shù)據(jù)從 UIThreadMonitor 來,通過監(jiān)聽和回調(diào)的方式告知 FrameTracer;
  2. FrameDecorator 負(fù)責(zé)接收數(shù)據(jù)和管理幀率 View,通過設(shè)置監(jiān)聽給 FrameTracer 接收幀率信息;
  3. 丟幀分為五個(gè)等級(jí),F(xiàn)rameTracer 會(huì)統(tǒng)計(jì)丟幀的次數(shù)和所丟的幀數(shù);
  4. 丟幀信息由 FrameTracer 的內(nèi)部類 FPSCollector 統(tǒng)計(jì)并報(bào)告給開發(fā)者。

到此本文結(jié)束,感謝閱讀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容