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

前言

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

一、效果預覽

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


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

1.1 注意的點

從上面的 Demo 中可以看出:

  • 右上角展示幀率、統計柱狀圖。
    其實展示的是一個自定義
    View,將接收到的數據經過計算得出幀率。

  • 滑動一段時間之后,跳轉到結果頁面。
    搜集夠一定數量的數據,報告 Issus 告知開發者。

  • 頁面靜止時幀率為 60,滑動時幀率發生變化。
    當 View 沒有發生變化時,不會請求刷新,展示的是系統幀率。
    當 View 滑動時,請求接收垂直同步信號,再經過計算得出幀率。

1.2 使用步驟

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

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

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 負責接收數據和展示幀率圖,通過 FrameDecorator 展示右上角的 View。
FrameDecorator decorator = FrameDecorator.getInstance(this);
// 檢測浮窗權限
if (!canDrawOverlays()) {
    requestWindowPermission();
} else {
    decorator.show();
}

請求浮窗權限: 幀率統計圖是一個自定義 View,并且由 WindowManager 添加,所以需要在 Android M(6.0) 以上的設備打開浮窗權限。

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

開始收集數據需要先獲取 TracePlugin 卡頓分析插件,接著獲取 FrameTracer 并調用它的 onStartTrace() 方法開始統計。

  1. 幀率上報。

當搜集到的幀率數據超過設置的時間后(Demo 中設置的是 10s,開發者可自行設置),便進行上報,這樣我們就可以通過某個時段的幀數來確定該頁面是否需要進行優化。

幀率上報

仔細觀察幀率統計圖,發現滑動時幀數大幅下降。說明該頁面在主線程做了太多事情,需要考慮進行優化。

二、幀率數據從哪來?

先說結論,幀率數據從 UIThreadMonitor 來。

回調過程.png

整體邏輯也比較簡單:UIThreadMonitor 會監聽系統垂直同步信號并維護監聽者列表 observers,FrameTracer 開始工作后添加監聽到列表 observers,等 UIThreadMonitor 接收到信號后遍歷回調列表告知即可。

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

2.1 UIThreadMonitor 監聽垂直同步信號

UIThreadMonitor 的工作原理在上篇文章中已經詳細分析過,在此簡單記錄:

  1. UIThreadMonitor 內部維護 Choreographer 實例,該對象用來接收系統 VSync 信號;
  2. UIThreadMonitor 向 Choreographer 添加幀率回調監聽,這樣 Choreographer 接收到系統信號時會通知 UIThreadMonitor;
  3. UIThreadMonitor 再遍歷回調給自己內部維護的列表。

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

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

2.2 FrameTracer 啟動和監聽

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

TracePlugin # start()

@Override
public void start() {
    super.start();
    ...
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            ...
            frameTracer.onStartTrace();
            ...
        }
    };
    // 主線程啟動
    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() 方法會調用自己的 onAlive() 方法,而后會添加監聽到 UIThreadMonitor。

FrameTracer # onAlive()

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

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

2.3 UIThreadMonitor 回調監聽

UIThreadMonitor 在監聽主線程幀率事件后遍歷回調:

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]);
            }
        }
    }
    ...
}

可以看到最終執行了 doFrame() 方法,而 FrameTracer 的 doFrame() 方法會將數據設置給幀率 View。

三、設置數據給幀率 View

還是先來看一下數據是如何設置到幀率 View 的:

數據傳遞流程

整個過程與上一章節類似,無法就是添加監聽等待回調,然后將數據給到 View 更新 UI。接下來逐步分析:

3.1 FrameTracer 監聽列表

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

IDoFrameListener 雖然叫 Listener 但其實是一個類,內部用 LinkedList 添加每幀數據、使用 doFrameAsync() 方法執行監聽回調。

3.2 FrameDecorator 創建和添加監聽

  1. FrameDecorator 由開發者手動創建,它是一個單例:
FrameDecorator decorator = FrameDecorator.getInstance(this);
  1. FrameDecorator 的獲取單例方法中會創建幀率 View FloatFrameView,并且在構造函數中添加監聽到 FrameTracer。

FrameDecorator # getInstance()

public static FrameDecorator getInstance(final Context context) {
    if (instance == null) {
        // 主線程直接創建
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            // 這里創建的 FloatFrameView
            instance = new FrameDecorator(context, new FloatFrameView(context));
        } else {
            try {
                // 子線程同步鎖創建
                synchronized (lock) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                             // 這里創建的 FloatFrameView
                            instance = new FrameDecorator(context, new FloatFrameView(context));
                            synchronized (lock) {
                                lock.notifyAll();
                            }
                        }
                    });
                    lock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    return instance;
}
  • 單例為 null 且在主線程則直接創建;
  • 有意思的是子線程創建的邏輯,這里同時用到了同步鎖 synchronized 和對象鎖,鑒于篇幅在此不作分析,感興趣的朋友可以解析并分享一下。
  1. FrameDecorator 構造器中,會在 FloatFrameView attach 到 Window 的時候將 FrameDecorator 添加到 FrameTracer 的監聽列表中:
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) { // 添加監聽
                    FrameTracer tracer = tracePlugin.getFrameTracer();
                    tracer.addListener(FrameDecorator.this);
                }
            }
        }
...
    });
    ...
}

3.3 FrameTracer 收到幀率回調

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

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() 中遍歷通知所有監聽。

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() {
                              // 執行這里
                              listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                      intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                          }
                      });
               ...
              }
          }
      }
      ...
  }

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

3.4 FrameDecorator 更新 View

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

  1. 經過一番計算,調用 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 數組暫時先不看,把調用鏈搞清楚再去分析。

  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() {
               // 展示數據
              view.chartView.addFps((int) fps, belongColor);
              view.fpsView.setText(fpsStr);
              view.fpsView.setTextColor(belongColor);

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

注意兩條注釋:

  • 切換到主線程:因為需要進行 ui 的更新,所以到主線程執行;
  • 展示數據:將傳遞來的數據設置給幀率 View FloatFrameView。

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

3.5 丟幀報告

回過頭來看插件報告捕捉到的一段時間內的數據:

幀率報告

主要看報告 json 中的部分內容:

  • machine:設備名稱,因為用的模擬器所以沒能獲取到;
  • scene:場景,也就是在哪個地方捕捉的數據,這里是一個 Activity;
  • dropLevel: 丟幀等級,Matrix 把丟幀分為四個等級:
    • DROPPED_FROZEN: 丟幀嚴重;
    • DROPPED_HIGH: 高度丟幀;
    • DROPPED_MIDDLE: 中度丟幀;
    • DROPPED_NORMAL: 普通丟幀;
    • DROPPED_BEST: 低丟幀,最佳狀態;
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;
    }
}

丟幀數量屬于的等級:

Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)
  • dropLevel: 掉幀統計;

關于卡頓官方文檔是這么解釋的:

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

所以 Matrix 使用 dropLevel 來統計一段時間內的丟幀程度。打個比方,如果這段時間丟幀等級基本在 DROPPED_BEST(發生了丟幀,但是丟的數量在 3 以下),那么屬于比較完美的情況無需優化。

而 Demo 中 :

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

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

  • fpx:幀率。計算出的平均幀數。

丟幀數量的計算

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

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

假設這臺設備刷新率 60,那么每 16ms 刷新一次,也就是 166666.... 納秒刷新一次。

  1. 獲取 VSync 垂直同步信號處理的時間。

接收到信號記錄當前時間:

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

一次刷新處理完畢記錄時間:

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

如果一次刷新耗時 16ms,這臺設備 16ms 刷新一次,得出剛好丟失 1 幀。但是如果耗時不足 16ms,得出 0 說明不會丟幀。

總結

最后簡單總結下:

  1. 幀率數據從 UIThreadMonitor 來,通過監聽和回調的方式告知 FrameTracer;
  2. FrameDecorator 負責接收數據和管理幀率 View,通過設置監聽給 FrameTracer 接收幀率信息;
  3. 丟幀分為五個等級,FrameTracer 會統計丟幀的次數和所丟的幀數;
  4. 丟幀信息由 FrameTracer 的內部類 FPSCollector 統計并報告給開發者。

到此本文結束,感謝閱讀。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372