當(dāng)我們討論流暢度的時(shí)候,我們究竟在說(shuō)什么?

前言:那些年我們用過(guò)的顯示性能指標(biāo)

注:Google 在自己文章中用了 Display Performance 來(lái)描述我們常說(shuō)的流暢度,為了顯得有文化,本文主要用“顯示性能”一詞來(lái)代指“流暢度”(雖然兩者在概念上有細(xì)微差別)。

從 Android 誕生的那一刻起,流暢度就為眾人所關(guān)注。一時(shí)之間,似乎所有人都在討論 Android 和 iOS 誰(shuí)的流暢度更好。但是,毫不夸張的說(shuō),流暢度絕對(duì)是 Android 眾多性能維度中最為奇葩的一個(gè)。因?yàn)椋瑸榱丝坍嬤@一性能維度,業(yè)界設(shè)計(jì)了各式各樣的指標(biāo)來(lái)對(duì)其進(jìn)行衡量。可以說(shuō)弄清了這些指標(biāo)我們就明白了什么是流暢度,可是這似乎并不太容易。

筆者簡(jiǎn)單搜集了一些業(yè)界中提及的顯示性能指標(biāo),大家可以來(lái)品評(píng)一下:

指標(biāo)名稱:FPS
相關(guān)資料Android性能測(cè)試之fps獲取

指標(biāo)名稱:Aggregate frame stats(N 多個(gè)指標(biāo))
相關(guān)資料Testing Display Performance

指標(biāo)名稱:Jankiness count、Max accumulated frames、Frame rate
相關(guān)資料JankTestBase.java

指標(biāo)名稱:SM、Skipped frames
相關(guān)資料Android應(yīng)用性能評(píng)測(cè)調(diào)優(yōu)

面對(duì)如此之多的顯示性能指標(biāo),想必大家也會(huì)跟筆者一樣,心中難免疑惑叢生。其實(shí),我們只需要依次弄清楚以下三個(gè)哲學(xué)問(wèn)題,所有的問(wèn)題也許就會(huì)迎刃而解:

  • 你是誰(shuí)——這些指標(biāo)具體反映了什么問(wèn)題
  • 你從哪兒來(lái)——這些指標(biāo)數(shù)值是怎么得到的
  • 你要到哪兒去——這些指標(biāo)如何落地來(lái)指導(dǎo)優(yōu)化

因此,本文將嘗試依次從上訴三個(gè)問(wèn)題來(lái)逐步分析和探討各個(gè)顯示性能指標(biāo)。

Step 1:你是誰(shuí)——這些指標(biāo)具體反映了什么問(wèn)題

總所周知,脫離了具體的應(yīng)用背景,所有的指標(biāo)都是沒(méi)有意義的。所以,為了徹底弄清楚各個(gè)顯示性能指標(biāo)的具體身份,我們勢(shì)必得從 Android 的圖像渲染流程說(shuō)起。

具體展開之前,首先需要說(shuō)明的是,為了降低復(fù)雜程度和本章篇幅,在這個(gè)環(huán)節(jié)之中,我們只討論圖像渲染流程中的各個(gè)具體環(huán)節(jié)所對(duì)應(yīng)的指標(biāo)有哪些。而指標(biāo)的具體定義,由第二章《你從哪兒來(lái)——這些指標(biāo)數(shù)值是怎么得到的》進(jìn)行討論。

Android 圖像渲染流程

下圖是筆者結(jié)合各類資料(主要是是源碼及官方文檔),在根據(jù)自己的理解梳理出的幾種常見場(chǎng)景下的圖像渲染流程:

PS 1:筆者個(gè)人技術(shù)水平有限,若存在理解有誤的地方還望指正。
PS 2:本文主要討論的 Android 源碼為 Android 6.0

Android Graphics Pipeline

備注:基于 OpenGL 的應(yīng)用可以使用 Choreographer 中的 VSYNC 信號(hào)來(lái)進(jìn)行圖像渲染工作的安排。

上面這幅圖涉及的概念較多,要完全吃透估計(jì)得費(fèi)不少時(shí)間。不過(guò)好在我們只是想弄明白顯示性能各種指標(biāo)的含義,所以我們只需要理清下面兩大關(guān)系即可:

  • SurfaceFlinger、HWComposer與Surface的關(guān)系

    • Surface:可以理解為Android系統(tǒng)中的一個(gè)基本顯示單元。只要使用Android任意一種API繪圖,繪制的結(jié)果都將反映在Surface上。
  • SurfaceFlinger:服務(wù)運(yùn)行在System進(jìn)程中,用來(lái)統(tǒng)一管理系統(tǒng)的幀緩沖區(qū)設(shè)備,其主要作用是將系統(tǒng)中的大部分Surface進(jìn)行合成。SurfaceFlinger主要使用GPU進(jìn)行Surface的合成,合成的結(jié)果將形成一個(gè)FrameBuffer。

  • HWComposer:即Hardware Composer HAL,其作用是將SurfaceFlinger通過(guò)GPU合成的結(jié)果與其他Surface一起最終形成BufferQueue中的一個(gè)Buffer。此外,HWComposer可以協(xié)助SurfaceFlinger進(jìn)行Surface的合成,但是否進(jìn)行協(xié)助是由HWComposer決定的。

  • 值得注意的是,有的Surface不由WindowManager管理,將直接作為HWComposer的輸入之一與SurfaceFlinger的輸出做最后的合成。

  • Choreographer、SurfaceFlinger、HWComposer與VSYNC的關(guān)系

    • VSYNC:Vertical Synchronization的縮寫,它的作用是使GPU的渲染頻率與顯示器的刷新頻率(一般為固定值)同步從而避免出現(xiàn)畫面撕裂的現(xiàn)象。
    • HWComposer:VSYNC信號(hào)主要由HWComposer通過(guò)硬件觸發(fā)。
    • Choreographer:當(dāng)收到VSYNC信號(hào)時(shí),Choreographer將按優(yōu)先級(jí)高低依次去調(diào)用使用者通過(guò)postCallback提前設(shè)置的回調(diào)函數(shù),它們分別是:優(yōu)先級(jí)最高的CALLBACK_INPUT、優(yōu)先級(jí)次高的CALLBACK_ANIMATION以及優(yōu)先級(jí)最低的CALLBACK_TRAVERSAL。
    • SurfaceFlinger:Surface的合成操作也時(shí)基于VSYNC信號(hào)進(jìn)行的。

簡(jiǎn)單來(lái)說(shuō),Android 圖像渲染流程主要由以下特征:

  1. 我們可以簡(jiǎn)單把 Android 圖像渲染架構(gòu)分為應(yīng)用(Surface)、系統(tǒng)(SurfaceFlinger)、硬件(Screen)三個(gè)層級(jí),其中繪制在應(yīng)用層,合成及提交上屏在系統(tǒng)層,顯示在硬件層;
  2. 無(wú)論應(yīng)用(Surface)、系統(tǒng)(SurfaceFlinger)、硬件(Screen)都是當(dāng)且僅當(dāng)繪制內(nèi)容發(fā)生改變,才會(huì)對(duì)繪制內(nèi)容進(jìn)行處理;
  3. 系統(tǒng)中的 SurfaceFlinger 以及絕大部分 Surface 都是按照 VSYNC 信號(hào)的節(jié)奏來(lái)安排自己的任務(wù);
  4. 目前,絕大部分 Surface 都屬于 Hardware Rendering。

各個(gè)指標(biāo)在 Android 圖像渲染流程所代表的意義

大致梳理了 Android 的圖像渲染流程之后,我們需要做的一件事情,就是看看上面提到的指標(biāo),都對(duì)應(yīng)了渲染流程的哪些階段,這樣對(duì)于我們了解各個(gè)指標(biāo)所反映的具體物理意義及其優(yōu)勢(shì)劣勢(shì)都有極大幫助。再次強(qiáng)調(diào),在這個(gè)環(huán)節(jié)之中,我們的討論僅限于只討論指標(biāo)所對(duì)應(yīng)的渲染流程的具體階段,各指標(biāo)的具體定義由第二章具體展開。

系統(tǒng)層級(jí)(SurfaceFlinger)的顯示性能指標(biāo)

  • 基礎(chǔ)數(shù)據(jù):SurfaceFlinger 合成次數(shù)
  • 指標(biāo)意義:
    • 系統(tǒng)合成幀率:FPS
  • 特別說(shuō)明:
    • SurfaceFlinger 僅在顯示區(qū)域內(nèi)的 Surface 有提交內(nèi)容更新時(shí)才會(huì)進(jìn)行合成(上屏),因此,系統(tǒng)合成幀率低并不一定意味著圖像顯示性能差,有可能是因?yàn)楫?dāng)前并沒(méi)有任何的內(nèi)容更新所導(dǎo)致。
    • 若顯示區(qū)域內(nèi)的某個(gè)待測(cè) Surface 持續(xù)進(jìn)行更新時(shí), SurfaceFlinger的合成(上屏)的頻率可以在某種程度上反映該 Surface 的顯示性能,但從理論上分析該指標(biāo)并不一定準(zhǔn)確。這是因?yàn)椋麸@示區(qū)域內(nèi)尚存在其他 Surface,它們也會(huì)影響 SurfaceFlinger 的合成(上屏)的行為,從而干擾結(jié)果。
    • 若某個(gè) Surface 的合成不在 SurfaceFlinger 中進(jìn)行(如 Camera Preview),則該 Surface 的顯示性能無(wú)法用這類指標(biāo)進(jìn)行衡量。

應(yīng)用層級(jí)(Surface)的顯示性能指標(biāo)

  • 基礎(chǔ)數(shù)據(jù):繪制過(guò)程中每一幀的關(guān)鍵時(shí)間點(diǎn)(如開始繪制時(shí)間、結(jié)束繪制時(shí)間等)
  • 指標(biāo)意義:
    • 應(yīng)用繪制幀率:Frame rate
    • 應(yīng)用繪制輪詢頻率:SM
    • 應(yīng)用繪制超時(shí)(跳幀)的次數(shù):Aggregate frame stats、Jankiness count、Skipped frames
    • 應(yīng)用繪制超時(shí)(跳幀)的幅度:Aggregate frame stats、Max accumulated frames、Skipped frames
  • 特別說(shuō)明:
    • 與 SurfaceFlinger 類似, Surface也僅在有內(nèi)容更新時(shí)才會(huì)進(jìn)行繪制,因此,繪制頻率低并不一定意味著圖像顯示性能差,有可能是因?yàn)楫?dāng)前并沒(méi)有任何的內(nèi)容更新所導(dǎo)致。
    • Aggregate frame stats 由于其基礎(chǔ)數(shù)據(jù)僅在硬件繪制(Hardware Rendering)過(guò)程中進(jìn)行統(tǒng)計(jì),屬于 HWUI 的功能,所以非硬件繪制的 Surface 自然無(wú)法使用這類指標(biāo)進(jìn)行衡量;
    • Jankiness count、Max accumulated frames、Frame rate 這類指標(biāo)的基礎(chǔ)數(shù)據(jù)來(lái)自于 FrameTracker ,適用范圍最廣。
    • SM、Skipped frames 這類指標(biāo),由于其基礎(chǔ)數(shù)據(jù)取自 Choreographer,若 某些 Surface 的繪制不依賴于 Choreographer ,則這些指標(biāo)無(wú)法衡量該 Surface 的顯示性能。

小結(jié)

評(píng)價(jià)顯示性能的各個(gè)指標(biāo),可以按其在圖像渲染流程中的作用,分為以下兩類:

  1. 系統(tǒng)層級(jí)的指標(biāo)僅有 FPS 一根獨(dú)苗,它的限制是 Surface 的和合成需要在 SurfaceFlinger中進(jìn)行;
  2. 應(yīng)用層級(jí)的指標(biāo)較多,它們之中又可以分為三類:
    1) Aggregate frame stats 屬于 HWUI 的功能, 需要 Surface 的繪制由 HWUI 進(jìn)行才能進(jìn)行分析;
    2) Jankiness count、Max accumulated frames、Frame rate 看上去適用范圍最廣。
    3) SM、Skipped frames 需要 Surface 依賴 Choreographer 進(jìn)行繪制,才能正常工作;

Step 2:你從哪兒來(lái)——這些指標(biāo)數(shù)值是怎么得到的

第一章的內(nèi)容僅僅是站在整個(gè)圖像繪制流程的高度來(lái)簡(jiǎn)單分析各個(gè)指標(biāo)的,本章將進(jìn)一步分析各個(gè)指標(biāo)的基礎(chǔ)數(shù)據(jù)來(lái)源以及具體計(jì)算方式。

基礎(chǔ)數(shù)據(jù):系統(tǒng)層級(jí)(SurfaceFlinger)的合成(上屏)的次數(shù)

前面說(shuō)到,在 Android 系統(tǒng)中,SurfaceFlinger 扮演了系統(tǒng)中所有 Surface 的管理者的角色,當(dāng)應(yīng)用程序所對(duì)應(yīng)的 Surface 更新之后,絕大多數(shù)的 Surface 都將在 SurfaceFlinger 之中完成了合并的工作之后,最終才會(huì)在 Screen 上顯示出來(lái)。

當(dāng)然, SurfaceFlinger 的執(zhí)行也是由 VSYNC 信號(hào)驅(qū)動(dòng)的,這也決定了每秒鐘合成次數(shù)的上限就是 60 次。當(dāng) SurfaceFlinger 接收到 Surface 更新通知的時(shí)候,將會(huì)由 SurfaceFlinger::handleMessageRefresh 函數(shù)進(jìn)行處理,其中包含重建可見區(qū)域、初始化、合成等步驟。這里,我們主要關(guān)注 SurfaceFlinger::doComposition() 這個(gè)方法。

void SurfaceFlinger::handleMessageRefresh() {
  ...
  if (CC_UNLIKELY(mDropMissedFrames && frameMissed)) {
    // Latch buffers, but don't send anything to HWC, then signal another
    // wakeup for the next vsync
    preComposition();
    repaintEverything();
  } else {
    preComposition();
    rebuildLayerStacks();
    setUpHWComposer();
    doDebugFlashRegions();
    doComposition(); //重點(diǎn)關(guān)注對(duì)象
    postComposition();
  }
  ...
}

在 doComposition 中,完成 Surface 的合成之后,都會(huì)調(diào)用 DisplayDevice::flip(),它會(huì)使用變量 mPageFlipCount 統(tǒng)計(jì)我們進(jìn)行合成的次數(shù),這個(gè)變量就是我們統(tǒng)計(jì) FPS 的核心原始數(shù)據(jù)。mPageFlipCount 記錄了 SurfaceFlinger 一共進(jìn)行了多少次合成,也可以簡(jiǎn)單理解為,SurfaceFlinger 向屏幕提交了多少幀的數(shù)據(jù)。

void SurfaceFlinger::doComposition() {
  ATRACE_CALL();
  const bool repaintEverything = android_atomic_and(0, &mRepaintEverything);
  for (size_t dpy=0 ; dpy<mDisplays.size() ; dpy++) {
    const sp<DisplayDevice>& hw(mDisplays[dpy]);
    if (hw->isDisplayOn()) {
      ...
      hw->flip(hw->swapRegion);//重點(diǎn)關(guān)注對(duì)象
      ...
    }
    // inform the h/w that we're done compositing
    hw->compositionComplete();
  }
  postFramebuffer();
}
void DisplayDevice::flip(const Region& dirty) const {
  ...
  mPageFlipCount++;
}

不僅如此, Android 還為我們獲取這個(gè)基礎(chǔ)數(shù)據(jù)提供了比較方便的方法。通過(guò)執(zhí)行 adb 命令:service call SurfaceFlinger 1013,我們就可以得出當(dāng)前的 mPageFlipCount。

C:\Users\xiaosongluo>adb shell
shell@cancro:/ $ su
su
root@cancro:/ # service call SurfaceFlinger 1013
service call SurfaceFlinger 1013
Result: Parcel(00aea4f4    '....')

FPS 的計(jì)算方法

根據(jù) FPS 的定義,我們不難逆推得出 FPS 的計(jì)算方法:

在 t1 時(shí)刻獲取 mPageFlipCount 的數(shù)值 v1,在在 t2時(shí)刻獲取 mPageFlipCount 的數(shù)值 v2,F(xiàn)PS 的計(jì)算公式:

FPS = (v2 - v1) / (t2 - t1);

需要注意的是:mPageFlipCount 的原始數(shù)據(jù)是 16 進(jìn)制的,一般而言計(jì)算之前需要先進(jìn)行進(jìn)制轉(zhuǎn)換。

基礎(chǔ)數(shù)據(jù):應(yīng)用層級(jí)(Surface)的繪制過(guò)程中每一幀的關(guān)鍵時(shí)間點(diǎn)(FrameInfo)

請(qǐng)大家先注意 FrameInfo 是由 Android 6.0(具體來(lái)講是 Android M Preview) 引入到 HWUI 模塊中的統(tǒng)計(jì)功能。 因此,目前來(lái)講絕大多數(shù)系統(tǒng)上的大多數(shù)應(yīng)用都暫時(shí)無(wú)法獲取這一基礎(chǔ)數(shù)據(jù)。不過(guò) This IsTheFuture。

我們?cè)賮?lái)仔細(xì)瞧瞧 Google 給出的顯示性能測(cè)試的十全大補(bǔ)丸 《Testing Display Performance : Aggregate frame stats》 。其中,特別值得關(guān)注的是 adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats 這一條命令。通過(guò)這條命令,我們獲取每一幀繪制過(guò)程中每個(gè)關(guān)鍵節(jié)點(diǎn)的耗時(shí)情況,從而仔細(xì)的分析潛在的性能問(wèn)題。

不得不說(shuō),按照 Google 給出的這種測(cè)試方法進(jìn)行測(cè)試得到的顯示性能數(shù)據(jù)是非常全面的。

這些基礎(chǔ)數(shù)據(jù)都是記錄在 FrameInfo 之中,由 CanvasContext 在doFrame()時(shí)進(jìn)行記錄。相關(guān)的主要源碼如下:

//源碼:FrameInfo.cpp
#include "FrameInfo.h"
#include <cstring>

namespace android {
  namespace uirenderer {
    const std::string FrameInfoNames[] = {
      "Flags",
      "IntendedVsync",
      "Vsync",
      "OldestInputEvent",
      "NewestInputEvent",
      "HandleInputStart",
      "AnimationStart",
      "PerformTraversalsStart",
      "DrawStart",
      "SyncQueued",
      "SyncStart",
      "IssueDrawCommandsStart",
      "SwapBuffers",
      "FrameCompleted",
    };

    void FrameInfo::importUiThreadInfo(int64_t* info) {
      memcpy(mFrameInfo, info, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));
    }
  } /* namespace uirenderer */
} /* namespace android */

Aggregate frame stats 指標(biāo)的計(jì)算方法

首先需要說(shuō)明的是 Aggregate frame stats 不是一個(gè)指標(biāo),而是一系列指標(biāo)集合。我們來(lái)看一個(gè)具體的 Aggregate frame stats 的例子:

Stats since: 752958278148ns
Total frames rendered: 82189
Janky frames: 35335 (42.99%)
90th percentile: 34ms
95th percentile: 42ms
99th percentile: 69ms
Number Missed Vsync: 4706
Number High input latency: 142
Number Slow UI thread: 17270
Number Slow bitmap uploads: 1542
Number Slow draw: 23342

以上統(tǒng)計(jì)信息的實(shí)現(xiàn)可以詳見源碼:GfxMonitorImpl.java

在 Android M 以上的系統(tǒng)上,上述信息的獲取十分方便(事實(shí)上也只有這些系統(tǒng)能夠獲取這些信息)。僅需要執(zhí)行以下命令即可:

adb shell dumpsys gfxinfo <PACKAGE_NAME>

基礎(chǔ)數(shù)據(jù):應(yīng)用層級(jí)(Surface)的繪制過(guò)程中每一幀的關(guān)鍵時(shí)間點(diǎn)(FrameTracker)

我們先來(lái)看一段 FrameTracker 的自我介紹:

FrameTracker tracks information about the most recently rendered frames. It uses a circular buffer of frame records, and is NOT thread-safe-mutexing must be done at a higher level if multi-threaded access is possible.

其實(shí) FrameTracker 是按照?qǐng)D層(Layer / Surface)統(tǒng)計(jì)的,而針對(duì)每個(gè) Layer ,F(xiàn)rameTracker 都記錄了它最近 128 幀的信息。

獲取 FrameTracker 的信息相當(dāng)方便,僅需要執(zhí)行以下命令即可::

adb shell dumpsys SurfaceFlinger --latency <LAYER_NAME>

該條命令最終會(huì)調(diào)用如下源碼,打印出繪制過(guò)程中的關(guān)鍵時(shí)間點(diǎn):

void FrameTracker::dumpStats(String8& result) const {
    Mutex::Autolock lock(mMutex);
    processFencesLocked();
    const size_t o = mOffset;
    for (size_t i = 1; i < NUM_FRAME_RECORDS; i++) {
        const size_t index = (o+i) % NUM_FRAME_RECORDS;
        result.appendFormat("%" PRId64 "\t%" PRId64 "\t%" PRId64 "\n",
            mFrameRecords[index].desiredPresentTime,
            mFrameRecords[index].actualPresentTime,
            mFrameRecords[index].frameReadyTime);
    }
    result.append("\n");
}

其中 desiredPresentTime、actualPresentTime 以及frameReadyTime 的含義已經(jīng)非常清晰,這里就不畫蛇添足了。

Jankiness count、Max accumulated frames、Frame rate 指標(biāo)的計(jì)算方法

這里需要特別指出的是, FrameTracker 只保存了 Surface 最近渲染的 128 幀的信息,因此,Jankiness count、Max accumulated frames、Frame rate 也僅僅是針對(duì)這 128 幀數(shù)據(jù)所計(jì)算出來(lái)的結(jié)果,它們的具體含義分別是:

  • Jankiness count:根據(jù)相鄰兩幀繪制時(shí)間的差值,“估計(jì)”是否存在跳幀并進(jìn)行跳幀次數(shù)的統(tǒng)計(jì);
  • Max accumulated frames: 根據(jù)相鄰兩幀繪制時(shí)間的差值,“估計(jì)”這 128 幀繪制過(guò)程中可能形成的最大連續(xù)跳幀數(shù);
  • Frame rate:計(jì)算所得平均(繪制)幀率。

注:以上數(shù)據(jù)主要依據(jù) actualPresentTime 數(shù)據(jù)來(lái)計(jì)算,desiredPresentTime 以及 frameReadyTime 實(shí)際并未參與上述指標(biāo)的計(jì)算過(guò)程。

其實(shí) Jankiness count、Max accumulated frames、Frame rate 指標(biāo)的基礎(chǔ)數(shù)據(jù)適用范圍很是廣泛,而且也繞過(guò)了 FPS 無(wú)法統(tǒng)計(jì)具體應(yīng)用顯示性能的缺點(diǎn)。但可惜的是, FPS 無(wú)法反映靜態(tài)應(yīng)用的顯示性能的缺點(diǎn),這類指標(biāo)也無(wú)法完全避免,而且由于 Jankiness count、Max accumulated frames 的數(shù)值來(lái)源于“估計(jì)”,這也限制了這一指標(biāo)的使用。

如果你對(duì)具體的計(jì)算過(guò)程感興趣,可以參考詳見源碼:JankTestBase

基礎(chǔ)數(shù)據(jù):應(yīng)用層級(jí)(Surface)的繪制過(guò)程中每一幀的關(guān)鍵時(shí)間點(diǎn)(Choreographer)

先說(shuō)一句有點(diǎn)繞口的話: Choreographer 是依據(jù) Choreographer 繪制的 Surface 在 UI 繪制過(guò)程中最為核心的機(jī)制。

Choreographer 的工作機(jī)制簡(jiǎn)單來(lái)說(shuō)就是,使用者首先通過(guò) postCallback 在 Choreographer 中設(shè)置的自己回調(diào)函數(shù):

  • CALLBACK_INPUT:優(yōu)先級(jí)最高,和輸入事件處理有關(guān)。
  • CALLBACK_ANIMATION:優(yōu)先級(jí)其次,和Animation的處理有關(guān)。
  • CALLBACK_TRAVERSAL:優(yōu)先級(jí)最低,和UI等控件繪制有關(guān)。

那么,當(dāng) Choreographer 接收到 VSYNC 信號(hào)時(shí),Choreographer 會(huì)調(diào)用 doFrame 函數(shù)依次對(duì)上述借口進(jìn)行回調(diào),從而進(jìn)行渲染。

那么顯然,doFrame 的執(zhí)行效率(次數(shù)、頻率)也就是我們需要的顯示性能數(shù)據(jù)。而這樣的基礎(chǔ)數(shù)據(jù),Choreographer 自身也進(jìn)行了記錄。如下面代碼中, jitterNanos 記錄了繪制前后兩幀所間隔的時(shí)間差, 而 skippedFrames 則記錄了 jitterNanos 這段時(shí)間 doFrame 錯(cuò)過(guò)了多少個(gè) VSYNC 信號(hào),即跳過(guò)了多少幀。


// Set a limit to warn about skipped frames.
// Skipped frames imply jank.
private static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt("debug.choreographer.skipwarning", 30);

void doFrame(long frameTimeNanos, int frame) {
  ...
  final long jitterNanos = startNanos - frameTimeNanos;
  if (jitterNanos >= mFrameIntervalNanos) {
    final long skippedFrames = jitterNanos / mFrameIntervalNanos;
    if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
      Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread.");
    }
    ...
    final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
    ...
    frameTimeNanos = startNanos - lastFrameOffset;
  }
  ...
}

上述數(shù)據(jù)的獲取并不是那么的直接,所以需要一定的手段。方法一共有三種,都不難:

  • Logcat 方案

缺點(diǎn):該方案需要系統(tǒng)授權(quán) “Adb Root” 權(quán)限,用于修改系統(tǒng)屬性;對(duì)于丟幀信息只能統(tǒng)計(jì)分析,無(wú)法進(jìn)行實(shí)時(shí)處理。
優(yōu)點(diǎn):設(shè)置完成后,可以獲取系統(tǒng)中所有應(yīng)用各自的繪制丟幀情況(丟幀發(fā)生的時(shí)間以及連續(xù)丟幀的數(shù)量)。

其實(shí),仔細(xì)觀察代碼,我們就可以注意到 Choreographer 源碼中本身就有輸出的方案:

if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
  Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread.");
}

唯一阻礙我們獲取數(shù)值的是:skippedFrames 的數(shù)值只有大于 SKIPPED_FRAME_WARNING_LIMIT 才會(huì)輸出相關(guān)的警告。而 SKIPPED_FRAME_WARNING_LIMIT 的數(shù)值可以由系統(tǒng)參數(shù) debug.choreographer.skipwarning 來(lái)設(shè)定。

注意:初始條件下,系統(tǒng)中不存在 debug.choreographer.skipwarning 參數(shù),因此 SKIPPED_FRAME_WARNING_LIMIT 將取默認(rèn)值 30。因此,正常情況下,我們能夠看見上訴 Log 出現(xiàn)的機(jī)會(huì)極少。

因此,如果我們修改(設(shè)定)系統(tǒng)屬性 debug.choreographer.skipwarning 為 1,Logcat 中將打印出每一次丟幀的Log。需要說(shuō)明的是,由于為 SKIPPED_FRAME_WARNING_LIMIT 賦值的代碼段由 Zygote 在系統(tǒng)啟動(dòng)階段加載,而其他應(yīng)用都是在拷貝復(fù)用 Zygote 中的設(shè)定,因此設(shè)定系統(tǒng)屬性后需要重啟 Zygote 才能使得上述設(shè)定生效。

具體的設(shè)置方法如下:

setprop debug.choreographer.skipwarning 1
setprop ctl.restart surfaceflinger; setprop ctl.restart zygote

設(shè)定完成以后,我們可以直接通過(guò) Logcat 中的信息得到系統(tǒng)中所有應(yīng)用的繪制丟幀信息,包括丟幀發(fā)生的時(shí)間以及連續(xù)丟幀的數(shù)量。不過(guò)由于 Logcat 信息的滯后性,以上信息我們幾乎只能進(jìn)行在測(cè)試完成后進(jìn)行統(tǒng)計(jì)分析,而無(wú)法進(jìn)行實(shí)時(shí)處理。

  • Choreographer.FrameCallback 方案

缺點(diǎn):該方案需要將測(cè)試代碼與待測(cè)應(yīng)用打包在一起,因此理論上僅能測(cè)試自己開發(fā)的應(yīng)用。
優(yōu)點(diǎn):可以對(duì)丟幀信息進(jìn)行實(shí)時(shí)處理

我們先來(lái)看看 Choreographer.FrameCallback 的定義。

Implement this interface to receive a callback when a new display frame is being rendered. The callback is invoked on theLooper thread to which the Choreographeris attached.

通過(guò)這個(gè)接口,我們可以在每一幀被渲染的時(shí)候記錄下它開始渲染的時(shí)間,這樣在下一幀被處理是,我們不僅可以判斷上一幀在渲染過(guò)程中是否出現(xiàn)掉幀,而整個(gè)過(guò)程都是實(shí)時(shí)處理的,這為我們可以及時(shí)獲取相關(guān)的調(diào)用棧信息來(lái)輔助定位潛在的性能缺陷有極大的幫助。

  • 代碼注入方案

缺點(diǎn):該方案需要通過(guò)注入程序?yàn)橹付☉?yīng)用注入測(cè)試代碼,因此需要系統(tǒng)為注入程序授權(quán) "應(yīng)用Root” 權(quán)限。
優(yōu)點(diǎn):與 Choreographer.FrameCallback 方案一致。

該方案可以簡(jiǎn)單理解為通過(guò)注入的方式來(lái)實(shí)現(xiàn)與 Choreographer.FrameCallback 方案一樣的目的。因此,這里我們主要討論兩者在實(shí)現(xiàn)方式上的區(qū)別。

顯而易見,我們需要注入的對(duì)象是 Choreographer ,因此理論上任何第三方應(yīng)用都是可以被注入的。但是隨著 Android 系統(tǒng)對(duì)"應(yīng)用Root” 權(quán)限管理越來(lái)越嚴(yán)格,所以該方案可用的范圍越來(lái)越小。

SM 指標(biāo)的計(jì)算方法

根據(jù)定義,SM 其實(shí)類似于 FPS,它被設(shè)計(jì)為可以衡量應(yīng)用平均每秒執(zhí)行 doFrame() 的次數(shù)。我們可以認(rèn)為它是在衡量 Surface 渲染輪詢的次數(shù)。

針對(duì) Logcat 方案,我們只需統(tǒng)計(jì)測(cè)試過(guò)程中目標(biāo)進(jìn)程一共掉了多少幀,由于對(duì)于絕大多數(shù)應(yīng)用在沒(méi)有丟幀的情況下會(huì)針對(duì)每一次 VSYNC 信號(hào)執(zhí)行一次 doFrame(),而 VSYNC 絕大多數(shù)情況下每秒會(huì)觸發(fā) 60 次,因此我們可以反向計(jì)算得出 SM 的數(shù)值:

SM = (60* totalSeconds - totalSkippedFrames) / totalSeconds;

針對(duì) Choreographer.FrameCallback 方案 以及 代碼注入方案,我們需要在代碼中自己進(jìn)行統(tǒng)計(jì)輸出(可以是設(shè)計(jì)成實(shí)時(shí)的,也可以設(shè)計(jì)成測(cè)試結(jié)束后進(jìn)行統(tǒng)計(jì)計(jì)算的)。

Skipped frames 指標(biāo)的計(jì)算方法

這個(gè)指標(biāo)的就是指當(dāng)前應(yīng)用在丟幀發(fā)生時(shí)的丟幀幀數(shù)。

針對(duì) Logcat 方案, 該數(shù)值直接在 Logcat 中輸出,并且?guī)в袝r(shí)間信息。

04-18 16:31:24.957 I/Choreographer(24164): Skipped 4 frames!  The application may be doing too much work on its main thread.
04-18 16:31:25.009 I/Choreographer(24164): Skipped 2 frames!  The application may be doing too much work on its main thread.

針對(duì) Choreographer.FrameCallback 方案 以及 代碼注入方案,我們可能很方便的通過(guò)計(jì)算前后兩幀開始渲染的時(shí)間差獲得這一數(shù)值,同樣方便。同樣與 Logcat 方案 不同的是,它也是可以設(shè)計(jì)成實(shí)時(shí)計(jì)算的。

小結(jié)

通過(guò)對(duì)各個(gè)顯示性能指標(biāo)的分析,我們可以知道,雖然目前指標(biāo)眾多,但其實(shí)有本質(zhì)區(qū)別的指標(biāo)確很少:

  • 系統(tǒng)層面:
    • 合成(上屏)幀率:FPS
  • 應(yīng)用層面:
    • 跳幀次數(shù):Aggregate frame stats、Jankiness count、Skipped frames
    • 跳幀幅度:Aggregate frame stats、Max accumulated frames、Skipped frames
    • 繪制幀率:Frame rate
    • 繪制輪詢頻率:SM

更為重要的是,我們從上述的分析中知道了各個(gè)指標(biāo)都有著自己的優(yōu)勢(shì)和不足,這也從根本上決定了它們各自有各自的用法。

Step 3:你要到哪兒去——這些指標(biāo)如何落地來(lái)指導(dǎo)優(yōu)化

其實(shí)指標(biāo)的用法也是多種多樣的,為了方便討論,我們僅從日常監(jiān)控、缺陷定位以及數(shù)據(jù)上報(bào)三個(gè)方面來(lái)討論各個(gè)顯示性能指標(biāo)是如何落地的。

日常監(jiān)控

  • FPS:數(shù)據(jù)形式最為直觀(FPS 是最早的顯示性能指標(biāo),而且在多個(gè)平臺(tái)中都有著類似的定義),且對(duì)系統(tǒng)平臺(tái)的要求最低(API level 1),游戲、視頻等連續(xù)繪制的應(yīng)用可以考慮選用,但不適用于絕大多數(shù)非連續(xù)繪制的應(yīng)用;
  • SM:數(shù)據(jù)形式與 FPS 類似,可以很好的彌補(bǔ) FPS 無(wú)法準(zhǔn)確刻畫非連續(xù)繪制的應(yīng)用顯示性能的缺陷;
  • Aggregate frame stats:除了對(duì)系統(tǒng)平臺(tái)有較高的要求以外,其采集方式最為簡(jiǎn)單(系統(tǒng)自帶功能);
  • Skipped frames:與 Aggregate frame stats 類似, 信息量相對(duì)較少,但可適用范圍更廣

特別說(shuō)明:Jankiness count、Max accumulated frames、Frame rate 只統(tǒng)計(jì)了128幀的信息(約2~3秒),而且 Jankiness count、Max accumulated frames對(duì)于掉幀情況的計(jì)算并非是一個(gè)準(zhǔn)確值,因此這些指標(biāo)都不太適用于日常監(jiān)控

舉個(gè)栗子,筆者服務(wù)的某個(gè)產(chǎn)品使用如下的一些指標(biāo)來(lái)監(jiān)控產(chǎn)品與競(jìng)品的性能變化情況:

| 測(cè)試指標(biāo) | 場(chǎng)景1 | 場(chǎng)景2 | 場(chǎng)景3 | 場(chǎng)景4|
| :----:|:----:|:----:|:----:|:----:|:----:|
| FPS | 58 | 58 | 58 | 58 |
| SM | 59 | 59 | 59 | 59 |
| Num of 6+ Skipped Frames | 0 | 0 | 0 | 0 |
| Num of 3+ Skipped Frames | 0 | 0 | 2 | 0 |

備注:

  1. Num of x+ Skipped Frames 代表測(cè)試過(guò)程中發(fā)生連續(xù)丟 x 幀(及以上)的次數(shù);
  2. 至于為什么我們選擇關(guān)注連續(xù)丟 3 幀以及連續(xù)丟 6 幀的的次數(shù),在【缺陷定位】部分有相關(guān)的分析討論;

缺陷定位

  • Skipped frames:基于 Choreographer.FrameCallback 方案實(shí)現(xiàn)的 Skipped frames 指標(biāo),可以在卡頓出現(xiàn)的時(shí)刻獲取應(yīng)用堆棧信息,可以在一定程度上進(jìn)行缺陷定位

特別說(shuō)明:

  1. FrameInfo 相關(guān)指標(biāo)無(wú)法直接進(jìn)行缺陷定位,但 FrameInfo 當(dāng)中包含了大量詳盡的繪制基礎(chǔ)數(shù)據(jù),對(duì)于缺陷定位也有較大幫助;
  2. 關(guān)于缺陷定位過(guò)程中連續(xù)掉幀閾值的選取,可參考維基百科中提到幾個(gè)重要的幀率數(shù)值:
  • 12 fps:由于人類眼睛的特殊生理結(jié)構(gòu),如果所看畫面之幀率高于每秒約10-12幀的時(shí)候,就會(huì)認(rèn)為是連貫的
  • 24 fps:有聲電影的拍攝及播放幀率均為每秒24幀,對(duì)一般人而言已算可接受
  • 30 fps:早期的高動(dòng)態(tài)電子游戲,幀率少于每秒30幀的話就會(huì)顯得不連貫,這是因?yàn)闆](méi)有動(dòng)態(tài)模糊使流暢度降低
  • 60 fps:在實(shí)際體驗(yàn)中,60幀相對(duì)于30幀有著更好的體驗(yàn)

以上各數(shù)據(jù)分別對(duì)應(yīng): 0 幀、1幀、2.5幀、5~6幀。(這就是為啥選擇3/6的原因)

舉個(gè)栗子, 筆者的同事萬(wàn)大師(yuwan)基于上述原理自研了一款性能分析工具。該工具在集成于待測(cè)應(yīng)用之后,可以自動(dòng)保存如下的性能缺陷信息:

Frame lost:... (連續(xù)丟幀數(shù)量,一般我們會(huì)設(shè)定一個(gè)閾值,例如 6 幀以上我們才會(huì)進(jìn)行記錄)
User action:...(當(dāng)前用戶操作)
Stack trace: ...(當(dāng)前堆棧信息)

有了這個(gè)工具之后,我們可以收集應(yīng)用的各個(gè)潛在“卡頓”點(diǎn),用于進(jìn)一步的分析和優(yōu)化。

數(shù)據(jù)上報(bào)

  • Aggregate frame stats:除了對(duì)系統(tǒng)平臺(tái)有較高的要求以外,其采集方式最為簡(jiǎn)單(系統(tǒng)自帶功能)、數(shù)據(jù)也比較清晰,相信基于這類指標(biāo)實(shí)現(xiàn)性能數(shù)據(jù)上報(bào)是特別方便的
  • Skipped frames :基于 Choreographer.FrameCallback 方案實(shí)現(xiàn)的 Skipped frames 指標(biāo),采集方式簡(jiǎn)單,實(shí)現(xiàn)基礎(chǔ)性能數(shù)據(jù)上報(bào)、卡頓數(shù)據(jù)上報(bào)也是很方便的

這方面應(yīng)用,筆者所服務(wù)的產(chǎn)品暫時(shí)沒(méi)有涉及,就不舉例子了。如果各位觀眾感興趣,建議參考 Android ANR 的設(shè)計(jì)理念。

小結(jié)

發(fā)現(xiàn)了沒(méi)有 Skipped frames 的用處很大有沒(méi)有? 而且通讀全篇,你會(huì)發(fā)現(xiàn) Aggregate frame stats、Jankiness count、Max accumulated frames 這些指標(biāo)都有提供類似的功能。

至于為什么,這就不是本文需要討論的內(nèi)容了,如果大家比較感興趣,筆者這里給出兩份相關(guān)的鏈接以供各位參考:

Fps Versus Frame Time
量化和優(yōu)化用戶與 Android 設(shè)備之間的交互

友情附贈(zèng): 現(xiàn)有顯示性能指標(biāo)對(duì)比

本來(lái)寫到這里本文的主要內(nèi)容就應(yīng)該結(jié)束了。但是如果不對(duì)比一下顯示性能指標(biāo)神馬的,總會(huì)讓人覺得缺少了一些什么。

友情提示:下述內(nèi)容相對(duì)主觀,建議各位讀者依據(jù)項(xiàng)目情況自行進(jìn)行選擇。

|指標(biāo)名稱 | 指標(biāo)意義 | 基礎(chǔ)數(shù)據(jù)來(lái)源 |采集方式 |適用系統(tǒng)|適用應(yīng)用|用途|
| :----:|:----:|:----:|:----:|:----:|:----:|:----:|:----:|
| FPS | 系統(tǒng)合成幀率 | SurfaceFlinger|adb shell|1.0+|略|監(jiān)控|
| Aggregate frame stats | 應(yīng)用跳幀次數(shù)、幅度 | FrameInfo|adb shell|6.0+|HW Rendering|監(jiān)控/上報(bào)|
|Jankiness count|(估算)應(yīng)用跳幀次數(shù)|FrameTracker|adb shell|1.0+|Almost All|定位|
|Max accumulated frames|(估算)應(yīng)用跳幀幅度|FrameTracker|adb shell|1.0+|Almost All|定位|
|Frame rate|應(yīng)用繪制幀率|FrameTracker|adb shell|1.0+|Almost All|定位|
|SM|應(yīng)用繪制輪詢頻率|Choreographer|多種方式|4.2+|SW/HW Rendering 及 部分 OpenGL Rendering|監(jiān)控|
|Skipped frames|應(yīng)用跳幀次數(shù)、幅度|Choreographer|多種方式|4.2+|SW/HW Rendering 及 部分 OpenGL Rendering|監(jiān)控/定位/上報(bào)|

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容