天天看點

他把閑魚APP長清單流暢度翻了倍(良心教程)

作者:閑魚技術-雲從

1 整體思路

閑魚在業務的快速疊代過程中,app 的長清單滑動流暢度逐漸惡化,對使用者浏覽内容體驗産生傷害。閑魚作為國内 flutter 應用的先驅,APP 以 flutter 和原生 Native 的混合工程存在。這裡分别就 Android 原生、flutter 頁面和大家分享我們的優化思路。

本文分為三個部分:

  • 流暢度名額和檢測工具建構
  • 原生 Android 長清單優化
  • flutter 長清單優化

流暢度優化整體思路圖如下:

他把閑魚APP長清單流暢度翻了倍(良心教程)

2 流暢度名額和檢測工具建構

2.1 現狀和難點

檢測工具現狀:以 Android 為例,現有流暢度工具可分為:

  • 侵入式
    • 內建 sdk,通過注冊幀回調計算流暢度。Android 見 Choreographer 類
    • profile 模式
  • 無侵入式
    • 執行系統指令,如

      adb shell dumpsys gfxinfo ${packageName}

    • 騰訊 GT APP,底層執行

      service call SurfaceFlinger 1013

      ,高版本 Android 已不支援

流暢度名額現狀有:

  • FPS (Frames Per Second)
  • SF(SkippedFrame,跳幀)
  1. 在機關時間 1 秒内,跳過執行 Choreographer 中 doFrame() 的次數 (見 《移動App性能評測與優化》
  • SM(Smooth,流暢度)
  1. 在機關時間 1 秒内,實際執行 Choreographer 中 doFrame() 的次數。其中 SM=60-SF。(見
  • 幀耗時資料

    使用 adb 指令得到幾個關鍵分位的幀平均耗時:

Total frames rendered: 2245
Janky frames: 31 (1.38%)
50th percentile: 5ms
90th percentile: 10ms
95th percentile: 14ms
99th percentile: 18ms           

然而以上工具和名額定義在 app 的複雜場景下,尚存在問題

  1. 多平台問題

    現 APP 技術有原生、h5、小程式、RN、weex、flutter 等。暫無一款無侵入的流暢度檢測工具能同時支援多個平台、多種機型和多個名額資料,而侵入式的檢測工具無法檢測競品 APP。

  2. 名額選擇和使用者體驗一緻性

    我們期望能有少量的幾個名額資料,準确的表達使用者流暢度體感。

平均 FPS(SM 和 SF 類似),不足以反映使用者體驗。如相同 30 FPS,可以是 1s 内 30 個 33.3 ms的畫面,也可以是 29 個 16.6ms 的畫面再加 1 個 516.9 ms 的畫面,但使用者體驗并不相同。

  1. 流暢度資料影響因素多

    滑動速度和滑動狀态:idle(停止)、drag(手指拖拽)、fling(自由滑動)都是影響流暢度資料的重要因素。

2.2 流暢度名額制定

維基百科

動畫定義:一種通過定時拍攝一系列多個靜止的固态圖像(幀)以一定頻率連續變化、運動(播放)的速度(如每秒16張)而導緻肉眼的視覺殘象産生的錯覺——而誤以為圖畫或物體(畫面)活動的作品及其影片技術。

清單滑動同理,是 APP 以一定頻率(60hz下16.6ms)和不同 offset 計算出一系列靜止畫面,讓肉眼看到滑動動畫。

當我們說清單滑動不流暢,是因為頻率過低無法讓肉眼産生視覺殘留,或在時間(畫面停留時長)和空間(畫面内容)産生跳變,讓使用者感覺到變化的不自然。以此我們可以定義名額如下:

  • 時間角度
    • 定義平均 FPS:定義一次檢測的平均幀率。反應畫面平均停留時長。
    • 定義 1s 大卡頓次數:平均 1s 内出現占用 3 幀及以上的畫面次數。反應畫面停留時長跳變
  • 空間角度
    • offset 跳變值:在畫面不掉幀的情況,若其中一個畫面出現跳變,甚至花屏或者綠屏會讓使用者體驗到不流暢。在 APP 滑動過程中,畫面内容由 offset 決定,而 offset 跳變,和卡頓時長、內插補點器實作均有關聯,現有內插補點器實作基本基于 D/T 曲線(距離/時間),為此平均 FPS 和 1s 大卡頓次數很大程度上展現了畫面跳變,同時考慮到無侵入式檢測 offset 的難度問題,暫不考慮 offset 跳變值。

綜上,我們定義流暢度名額為平均 FPS 值和 1s 大卡頓次數。

2.3 流暢度檢測工具實作

我們從 APP 錄屏畫面入手,計算流暢度名額值。當我們得到 APP 滑動過程中的錄屏資料,可通過每 16.6ms 檢測錄屏畫面是否發生變化,當連續畫面未發生變化,則表示發生了卡頓。無變化的連續畫面數則表示了卡頓的時長。

他把閑魚APP長清單流暢度翻了倍(良心教程)

為得到目标 APP 錄屏資料,檢測工具 APP 向系統注冊錄屏服務,然後在檢測工具 APP 的幀回調中不停讀取錄屏畫面,并和上次檢測畫面 hash 值進行比對。

  • 檢測工具 APP 和目标 APP 程序隔離,為此目标 APP 發生卡頓并不影響檢測工具 APP 的幀回調
  • 為保證每次錄屏畫面讀取和 hash 值計算在 16.6ms 内完成,需根據高低端機型調整畫面寬高壓縮比。
他把閑魚APP長清單流暢度翻了倍(良心教程)

為排除滑動操作對流暢度數值的幹擾,我們使用腳本操作檢測工具 APP 和目标 APP 的滑動。自動化腳本原理為使用 adb 指令操作手機

點選:adb shell input tap $x $y
滑動:adb shell input swipe $x1 $y1 $x2 $y2 $duration           

2.4 檢測工具示範

流暢度檢測工具 APP 以懸浮框的方式顯示,下面為目标檢測 APP:

流暢度檢測工具界面

2.5 小結和展望

在流暢度名額方面,我們定義了平均 FPS 和 1s 大卡頓次數作為名額,更好的反應了使用者體驗。

在流暢度檢測工具方面,我們實作了無侵入檢測工具,支援以下特性:

  • 無侵入
  • 支援檢測第三方 app
  • 支援多平台:native,flutter,h5,小程式
  • 多元度資料:平均 FPS,平均 1s 大卡幀次數,幀分布直方圖,幀分布均方差
  • 自動操作,避免人為操作差異

此外,流暢度檢測工具還有一些不足之處

  • 清單中有視訊卡片
    • 停止滑動時,若清單中有視訊播放,由于畫面一直在變化,檢測工具無法判斷是滑動停止;同時,由于視訊 fps 值為 30 左右,會導緻流暢度資料偏低
    • 如何避免:檢測過程中,需保證清單滑動不停止
  • 低端機(y67)真實 fps 計算存在偏差
    • 為保證低端機上(如 vivo y67)上計算大圖像 hash 值在 16ms 以内,錄屏畫面壓縮較大(寬度壓縮 100,高度壓縮 10),為此在大量空白或者大色塊的場景下,無法檢測到畫面的細微變化,fps 計算存在偏低。
    • 如何避免:避免低端機上檢測大量空白或大色塊的場景

3 原生 Android 長清單優化

Android 原生長清單優化已經非常成熟了,在工具方面有 traceview、blockcanary、DDMS、Android Profile 等。常見優化手段也很多:布局層級優化,過度渲染優化,頻繁measure、layout優化,UI 線程耗時方法優化、備援資源資源加載優化等,這裡不再贅述。

除此之外閑魚使用以下 2 點優化首頁

3.1 異步建構視圖緩存池

通過工具檢測或耗時列印,發現清單初始滑動和 loadmore 時觸發 item 視圖建構耗時嚴重(RecyclerView.onCreateViewHolder)

他把閑魚APP長清單流暢度翻了倍(良心教程)

檢視首頁顯示和初始滑動流程,可以發現流程中其他 UI 操作過程和等待使用者操作過程均有優化空間。

他把閑魚APP長清單流暢度翻了倍(良心教程)

利用 AsyncLayoutInflater 原理異步建構視圖緩存池,優化首頁清單流程如下:

他把閑魚APP長清單流暢度翻了倍(良心教程)

其中視圖緩存池建構完成的時機在不同機型下不同,可能在清單首屏多卡片建構之前,或建構中,或在使用者滑動操作之前完成,或一開始建構就抛出錯誤停止建構

注意:不能直接使用 AsyncLayoutInflater,AsyncLayoutInflater 在異步建構失敗後有一個降級到 UI 線程建構的邏輯,為避免降級邏輯發生導緻緩存池在 UI 線程建構,導緻頁面更加卡頓,需要移除這個降級邏輯:出現異步 inflater 失敗,停止緩存池建構。

3.2 ViewDataUnbinder 快速抽離 UI 操作

在卡片資料綁定階段(RecyclerView.onBindViewHolder),在低端機上耗時較為嚴重,原因是在卡片資料綁定方法中,而 UI 和非 UI 操作糅合在一起,由于 UI 邏輯必須在 UI 線程執行,最終導緻全部邏輯隻能在 UI 線程執行。

能想到定義視圖資料層,将 UI 和非 UI 操作分離開,然而實際編碼發現業務代碼改動量大且容易出錯,AB 測試邏輯難以實作。那有沒有更好的方案,用最少量代碼抽離 UI 操作呢?

他把閑魚APP長清單流暢度翻了倍(良心教程)

核心思路:編譯期根據視圖類自動生成 ViewData 類,并替換視圖類執行個體。ViewData 類和視圖類擁有相同的關鍵方法簽名,方法執行時記錄視圖操作,統一切換到 UI 線程執行視圖操作。

他把閑魚APP長清單流暢度翻了倍(良心教程)

具體使用代碼樣例如下

  1. 注解視圖類

    使用 ViewDataAnno 注解視圖類,UIMethodAnno 注解 UI 操作方法。

他把閑魚APP長清單流暢度翻了倍(良心教程)

其中注解說明

他把閑魚APP長清單流暢度翻了倍(良心教程)
  1. 生成 ViewData 類
他把閑魚APP長清單流暢度翻了倍(良心教程)
  1. 業務代碼修改
    • 修改視圖變量為 ViewData 類型
    • 原視圖資料綁定邏輯放置背景線程

3.3 優化結果

他把閑魚APP長清單流暢度翻了倍(良心教程)
閑魚首頁,在恢複内容上屏速度(流暢度降低)後提升流暢度

4 Flutter 複雜長清單優化

flutter 一直以高性能被大家所認知,參考

Flutter 是如何做到性能直逼 native 的?

,這也是閑魚當初選擇 flutter 的一個重要原因。而在閑魚的實際 flutter 頁面,如商品詳情頁和搜尋結果頁,長清單滑動流暢度體驗卻不盡人意。

4.1 工具使用和常見優化

做性能優化前,需要了解 flutter 的渲染原理,如 Widget、Element、RenderObject 三棵樹結構、Widget 到螢幕顯示過程等,可參考

超詳解析Flutter渲染引擎

複雜業務如何保證Flutter的高性能高流暢度?

針對性能問題,首推官方性能分析工具并結合使用 profile 模式檢視性能問題,參考

Flutter Performance 分析工具簡介

Profile 模式隻能在真機上運作,不能在模拟器上運作:基本和 Release 模式一緻,除了啟用了服務擴充和 tracing,以及一些為了最低限度支援 tracing 運作的東西(比如可以連接配接 observatory 到程序)。指令 flutter run --profile 就是以這種模式運作的,通過 sky/tools/gn --android --runtime-mode=profile 或者 sky/tools/gn --ios --runtime-mode=profile 來 build。因為模拟器不能代表真實場景,是以不能在模拟器上運作

引自:

Flutter性能調優、複雜業務保證Flutter的高性能高流暢

4.1.1 檢查 widget rebuild 情況

Android Studio 上

View

Tool Windows

Flutter Performance

打開檢測 Widget rebuild 情況,可以發現 FDButtonBar 被頻繁重建,然而檢視視圖内容并沒有發生變化。檢視代碼定位到

reducer.dart

中會根據滑動事件更新 state 中的

scrollPercent

,進而産生重建。而在詳情頁中,

scrollPercent

在 Widget 建構中并未參與使用。

閑魚頁面中使用了 fish-redux,在 reducer.dart 的方法中傳回不同的 state 對象則表示需要重建 widget
// reducer.dart
// 滑動事件監聽
  static BottomBarState onScroll(BottomBarState state, Action action) {
    ...
    return state.clone()..scrollPercent = scrollPercent;
    ...
  }           
他把閑魚APP長清單流暢度翻了倍(良心教程)

4.1.2 使用 fish-redux 性能日志

fish-redux

是閑魚研發一套在 flutter 上的 redux 架構,閑魚 APP 中有廣泛應用。fish-redux 中自帶性能日志,源碼檢視

performance.dart

,若需要列印 profile 或 release 模式下的性能日志,可自行修改源碼。

閑魚詳情頁滑動時,檢視 adb 日志,可以發現大量的滑動廣播通知,且存在耗時 1ms 以上事件處理。

11-15 15:03:43.684 27076 27271 I flutter : CommonBuyDetailPage performance: ItemBodyAction.onScrollBroadcast 261
11-15 15:03:43.701 27076 27271 I flutter : CommonBuyDetailPage performance: ItemBodyAction.onScrollBroadcast 1933
11-15 15:03:43.716 27076 27271 I flutter : CommonBuyDetailPage performance: ItemBodyAction.onScrollBroadcast 371           
profile 模式下時間日志
他把閑魚APP長清單流暢度翻了倍(良心教程)

因為詳情頁中存在視圖間關聯,如标題欄的顯示隐藏漸變,

問賣家

的顯示消失均需要根據滑動事件做判斷。結合業務邏輯,可以發現,除了

問賣家

外,其他視圖在滑動超出 600 之後,收到滑動事件後不會發生視圖内容變化;而

問賣家

在滑動超出更大的一個值後會永遠消失不顯示,在一開始未超出這個值時,僅需要判斷滑動方向即可。基于以上業務背景,在滑動超出 600 後,若

問賣家

是不再顯示狀态,則不發送滑動事件;否則僅在開始滑動的 30 距離内發送事件。

此外,可以利用 fish-redux 的特性:若 reducer.dart 中傳回新的 state 對象則表示 widget 重建,檢查全部的 reducer.dart 檔案内方法實作,排查可能發生的無效 widget 重建。

4.1.3 優化 ClipPath 和 ClipRPath

使用 Timeline 檢視渲染線程性能消耗,可以發現有多個

ClipRectLayer

ClipRRectLayer

他把閑魚APP長清單流暢度翻了倍(良心教程)
他把閑魚APP長清單流暢度翻了倍(良心教程)

打開 Debug flag

debugDisableClipLayers

debugDisablePhysicalShapeLayers

重新檢查視圖,可以發現部分 ClipRectLayer 是因為圖檔内容超出視圖邊界産生,部分 ClipRRectLayer 是因為卡片 Widget 圓角設定以及基于外接紋理的圖檔控件裡設定了 ClipRRect 設定(即便 radius 為0也會設定)

他把閑魚APP長清單流暢度翻了倍(良心教程)

了解原理後,我們對閑魚圖檔控件新增參數,支援圖檔内容圓角設定和圖檔内容寬高裁剪,使 native 層生成的 Bitmap 已經滿足圓角和寬高比要求。同時修複 radius 為0也會設定 ClipRRect 的問題。優化後的 Timeline 圖如下:

他把閑魚APP長清單流暢度翻了倍(良心教程)

4.1.4 其他優化建議

flutter 性能優化相關的優秀文章很多,本文不再對類似的排查和優化手段做贅述,這裡做下簡單彙總:

  • widget build 優化
    1. setState 狀态重新整理位置盡量放置于視圖樹的低層級
    2. Provider 中擷取 Model 的方式會影響重新整理範圍。推薦使用 Selector 或 Consumer 來擷取祖先 Model,以維持最小重新整理範圍
    3. 對于長清單,避免使用 ListView() 構造函數,推薦使用 ListView.builder 構造函數
    4. reducer 中,state 對象中的視圖資料真正發生變化的時候,建立 state 對象
  • 主 isolate 優化
    1. 減少或延遲 widget build 中非視圖邏輯,如曝光埋點延遲到滑動停止聚合觸發
    2. 清單 Item 高度可知的情況下,推薦設定 itemExtent,減少滑動中頻繁計算清單高度
    3. 使用 const 修飾無需變更的 widget 或普通對象
    4. 使用 AnimatedBuilder 時,避免在不依賴于動畫的 widget 的構造方法中建構 widget 樹。動畫的每次變動都會重建這個 widget 樹。而應該建構子樹的那一部分,并将其作為 child 傳遞給 AnimatedBuilder
    5. 避免在動畫中剪裁。如果可能,請在動畫開始之前預先剪切圖像
  • Render 線程優化
    1. 對于頻繁更新的控件(如動畫),使用 RepaintBoundary 隔離它,建立單獨 layer 減少重繪區域
    2. 使用圖檔替換半透明效果
    3. 減少 saveLayer(ShaderMask、ColorFilter、Text Overflow)、clipPath的使用,提升 render 線程性能
    4. 避免使用 Opacity widget,尤其是在動畫中避免使用。請用 AnimatedOpacity 或 FadeInImage 進行代替
    5. 避免使用帶換行符的長文本
  • 工具推薦
    1. 官方 DevTools 工具
    2. 利用 Debug flags 排查問題(推薦
    3. 善于利用架構日志,如 fish-redux 性能日志

4.2 清單 element 複用優化

flutter 清單控件劃分為可視區域和 Cache 區域,往下滑動時 element 從底部被建立進入底部 Cache 區域後,再進入可視區域,再進去頂部 Cache 區域,最後被銷毀。往上滑動邏輯類似。在不使用 keepAlive 的情況下,來回滑動,曾經建立過的 element 需要重新建立。而在我們的業務中,清單 item Widget 結構是接近的,此時如果能根據類型複用 element,就能一定程度的提升性能。

清單控件源碼見 sliver_list.dart 中 RenderSliverList.performLayout()

element 緩存在 _childElements 數組中,以 index 為索引。源碼見 sliver.dart

若 item Widget 結構差異很大,即便複用了 element,Element.updateChild 方法内部最終還是執行了 inflateWidget 方法,對于性能提升就沒什麼價值了

他把閑魚APP長清單流暢度翻了倍(良心教程)

我們建構

index

${widget.key}

List<element>

的映射關系:在 widget 建立處建立

index

${widget.key}

映射,在 element 應該被銷毀移除的邏輯處,将 element 緩存至

${widget.key}

映射的

List<element>

處(注意 renderObject 對象需要從父節點移除)。清單滑動過程中,優先根據映射關系找到緩存中的 element 并使用(注意更新 element.renderObject.parentData 中的 index 值)

4.3 複雜 Widget 分幀上屏

以上全部優化手段嘗試後,在閑魚的詳情頁和搜尋頁上還是遠沒有達到預期。原因是猜你喜歡卡片和搜尋頁卡片本身就足夠複雜,另外由于我們引入 DX 技術讓 Widget 進一步變得巨大,最終導緻的結果是:即便高端機,也無法在一幀時間内完成渲染。

然而抛開技術視角,從業務視角看,卡片展現内容和 DX 的動态能力都是必需的。那如何在滿足業務訴求的情況下,實作超大 Widget 的高性能呢?

他把閑魚APP長清單流暢度翻了倍(良心教程)
業務側僅需 Text,但在 DX 技術中使用的是 DXTextWidget
他把閑魚APP長清單流暢度翻了倍(良心教程)
猜你喜歡卡片在 紅米 K30Pro(CPU 骁龍 865)的 Timeline 圖
他把閑魚APP長清單流暢度翻了倍(良心教程)
搜尋結果卡片 Timeline 圖,補充了 performLayout、updateChild、Widget build

在已知常見優化手段無法滿足的情況下,我們回歸 GUI 系統性能優化的起點去思考問題。流暢度優化思路,大體可以分為 3 個方向:

  1. 多線程方案

    在 Android 原生開發中很常見。但在 dart 世界中,不同線程(isolate)的記憶體是隔離的,此外由于 flutter 渲染流程三棵樹,我們不好直接操作 RenderObject,多線程方案在 flutter 中較難實施(排除 IO 更新資料後顯示等正常場景)

  2. 優化每個任務,擠壓 CPU 運算量,保證一幀時間(16.6 ms)完成任務
  3. 中的主流優化思路,前面的優化手段都是這個思路
  4. 快速響應使用者,讓使用者覺得夠快,不阻塞使用者的互動。即一幀時間内還有任務沒有完成,則停止執行,保證清單先執行滑動,未執行任務在後續幀時間片上執行

    參考 React Fiber 架構,基于時間分片的思路,協調階段将一顆任務樹轉為一條任務鍊(parent 節點 → child 節點 → sibling 節點 → parent 節點),滿足了任務鍊可中斷執行,提前送出渲染,最後實作了将一條任務鍊拆解到多幀時間分片中消化。

他把閑魚APP長清單流暢度翻了倍(良心教程)

排除方向 1、2 後,隻剩下方向 3。再結合猜你喜歡卡片 Timeline 圖可以發現,在卡片 Widget 建立的一幀發生時間不足,而後面的幾幀内時間消耗都遠沒到 16.6 ms,可以想到方向 3 是正确的。那剩下的關鍵問題僅有以下 2 點:

  1. 能否将一個大 Widget build 任務為拆分多個小 Widget build 任務并大緻平均的配置設定到多個時間分片上?
  2. 一個大 widget 分時間片上屏是否會影響體驗?
他把閑魚APP長清單流暢度翻了倍(良心教程)
Timeline 上任務耗時圖
他把閑魚APP長清單流暢度翻了倍(良心教程)
Flutter widget 拆分和分幀上屏

基于時間分片的大方向,我們把一個大 widget 拆分為一個空白架構和 2 個卡片 widget,再将卡片 widget 拆分為一個卡片架構和多個 FXImage Widget,Widget 架構中不立馬顯示的部分使用占位 Widget 臨時代替。

由此建構一個高優大任務隊列和一個低優小任務隊列,高優大任務隊列中的任務高優執行且獨占一幀時間,低優小任務隊列低優執行且一幀時間最多能執行 12 個任務。再利用 flutter 逐漸标髒,将 build 任務延遲到後續時間分片上。

他把閑魚APP長清單流暢度翻了倍(良心教程)

以上最終将一個超大 widget 建構從 1 幀時間分散到 4 幀時間内消化,優化了卡頓。

他把閑魚APP長清單流暢度翻了倍(良心教程)
優化後猜你喜歡卡片 Timeline 圖(紅米 K30Pro,CPU 骁龍 865)

在體驗方面,前面講清單控件結構時已知有一個不可見的 Cache 區域,是以分幀上屏大部分是在這個不可見區域完成的,為此在高端機或正常滑動情況下使用者并無感覺。而在低端機上快速滑動能明顯看到卡片空白情況,但整體相比嚴重頓挫體感要好。

4.4 優化資料

基于上面的優化手段,閑魚詳情頁和搜尋頁流暢度 FPS 提升了 3 個點,低端機大卡頓次數降低一半,中高端機型上流暢度提升到 57 或以上,大卡頓次數接近 0。

詳情頁

線上高可用 fps 資料如下:

他把閑魚APP長清單流暢度翻了倍(良心教程)

線上低端機 fps 曲線。綠色為優化版本

曲線分布越靠右,流暢度越好

他把閑魚APP長清單流暢度翻了倍(良心教程)
線上高端機 fps 曲線。綠色為優化版本

搜尋頁

他把閑魚APP長清單流暢度翻了倍(良心教程)
他把閑魚APP長清單流暢度翻了倍(良心教程)

4.5 滑動內插補點器優化

完成上面優化後,線下自建流暢度檢測工具資料和線上 fps 資料曲線都有很大的提升,且資料名額接近原生 APP 流暢度。在中高端機型上,閑魚詳情頁 FPS 已經被我們優化到了 57 及以上了,1s 大卡頓次數接近 0。在原生 APP 流暢度 FPS 數值達到 57 及以上時,滑動過程中基本上不會感受到卡頓,然而,flutter 頁面的實際滑動操作中,還是能感受到卡頓。

回顧自建流暢度檢測工具原理:基于每幀畫面比對、無侵入,相同的自動化腳本,是以相信我們線下測試的資料(平均 FPS 和 1s 大卡頓次數)是準确的。性能資料接近,而體感有差異,且性能資料準确可信,是以可以确認流暢度名額(平均 FPS 和 1s 大卡頓次數)還不能完全反應體感。

再回顧 2.2 流暢度名額制定,可以發現我們并沒有對空間次元的 offset 跳變(畫面内容跳變)做檢測。基于此,我們可以對比 Android 原生 RecyclerView 和 Flutter SliverList 在卡頓情況下 offset 變化情況

他把閑魚APP長清單流暢度翻了倍(良心教程)
Android 原生 RecyclerView 和 Flutter SliverList fling 階段 offset/time 曲線圖

由上可以得到,同樣在 FPS 值達到 57,Android RecyclerView 在使用者體感上比 flutter 清單控件更好的原因:在小卡頓時,offset 偏移值并沒有發生翻倍跳變。

檢視 flutter 滑動算法,可以發現是基于一條 D/T 曲線計算滑動距離,是以發生卡頓時,輸入 timeOffset 值發生翻倍,最終計算出來的 offset 值發生近乎翻倍。

他把閑魚APP長清單流暢度翻了倍(良心教程)
flutter ClampingScrollSimulation D/T 曲線

為消除在發生小卡頓時,offset 跳變的情況,我們自定義了 physics 和 simulation,在 time 發生發生小跳變時,修改滑動距離算法,采用 V/T 曲線算法,distance 通過累加的方式計算,優化了 time offset 發生翻倍而導緻曲線跳變的情況

distance = velocity(time) * 16.6ms + distance           
注意:需要适配系統頻率大于 60 hz 的機型(如 90hz,120hz),在一幀時間内有可能計算多次 distance

以 V/T 曲線為基礎,我們提供了以下滑動內插補點器:

  • SmoothClampingScrollPhysics

    無回彈內插補點器,停頓後偏移值不跳變。 結束滑動的效果同 ClampingScrollSimulation

他把閑魚APP長清單流暢度翻了倍(良心教程)
  • SmoothBouncingScrollPhysics

    回彈內插補點器,停頓後偏移值不跳變

他把閑魚APP長清單流暢度翻了倍(良心教程)

5 總結和展望

經過上述優化,在原生 Android 方面,閑魚首頁流暢度和内容上屏得到明顯提升;在 Flutter 方面,閑魚詳情頁和搜尋頁流暢度 FPS 提升了 3 個點,低端機大卡頓次數降低一半,中高端機型上流暢度提升到 57 或以上,大卡頓次數接近 0,相同小卡頓在體驗上得到了提升。

流暢度優化是每一個 GUI 系統都一直在努力的事情,有很多優秀的工具介紹、官方和非官方的優化文章。這次優化過程中,我們也借鑒了很多别人的文章,發現和優化了一些問題,但本文盡量不去重複描述,推薦讀者閱讀相關優化文章或官方文檔。

在以上優化手段尚無法實作最終目标時,我們也做了一些不一樣的優化,期望能抛磚引玉,對讀者有所幫助和啟發:

  • 基于使用者體驗為導向建構了流暢度名額:平均 FPS,1s 大卡頓次數
  • 針對名額,自建了流暢度檢測工具,支援無侵入、跨平台、自動化
  • [Android] 顯示 ViewDataUnbinder 元件在複雜業務邏輯中快速抽離 UI 操作
  • [Flutter] 修改 Flutter engine 源碼,支援清單 element 複用
  • [Flutter] 實作大 Widget 分幀上屏元件
  • [Flutter] 內插補點器算法優化

後續我們會繼續思考以下内容:

  • 如何将流暢度檢測工具内部産品化,支援非研發同僚使用?
  • 如何使用已有的經驗、工具、元件快速優化其他業務頁面?
  • 如何在研發階段及時發現和防止無效 rebuild 等問題?
  • 如何在 CI 平台及時發現頁面流暢度惡化情況?
  • 如何以業務無侵入的方式實作業務大 Widget 自動且合理地分幀上屏?

繼續閱讀