一、前言
淘特在很多業務場景都使用了 Flutter,加上業務場景本身具有一定的複雜性,使得 Flutter 在低端機流式場景的滑動浏覽過程中卡頓、跳幀對比使用原生(Android/iOS)開發明顯。通過分析業務層在 Flutter 渲染流程中的每個階段存在的性能問題進行了一系列的深度優化後,平均幀率已經達到50幀之上超越了原生的表現, 但卡頓率依然達不到最佳的體驗效果,遇到了難以突破的瓶頸和技術挑戰,需要進行技術嘗試和突破。
本文會從底層原理、優化思路、實際場景的優化政策、核心技術實作、優化成果、總結和參考資料進行講述,期望可以為大家帶來一定的啟發和幫助,也歡迎多多交流與指正,共建美好的 Flutter 技術社群。
二、渲染機制
原生 vs Flutter
Flutter 本身是基于原生系統之上的,是以渲染機制和 Native 是非常接近的,引用 Google Flutter 團隊 Xiao Yu分享[1],如下圖所示:

渲染流程
如圖左中,Flutter 從接收到 VSync 信号之後整體經曆 8 個階段,其中 Compositing 階段後會将資料送出給 GPU。
Semantics 階段會将
RenderObject
marked 需要做語義化更新的資訊傳遞給系統,實作輔助功能,通過語義化接口可以幫助有視力障礙的使用者來了解UI内容,和整體繪制流程關聯不大。
Finalize Tree 階段會将所有添加到
_inactiveElements
的不活躍
Element
全部 unmount 掉,和整體繪制流程關聯不大。
是以,Flutter 整體渲染流程主要關注 圖右 中的階段:
GPU Vsync
- Flutter Engine 在收到垂直同步信号後,會通知 Flutter Framework 進行 beginFrame,進入 Animation 階段。
Animation
- 主要執行了
回調。transientCallbacks
Flutter Engine 會通知 Flutter Framework 進行 drawFrame,進入 Build 階段
Build
- 建構要呈現的UI元件樹的資料結構,即建立對應的
以及對應的Widget
。Element
Layout
- 目的是要計算出每個節點所占空間的真實大小進行布局;
- 然後更新所有 dirty render objects 的布局資訊。
Compositing Bits
- 對需要更新的
進行 update 操作;RenderObject
Paint
- 生成 Layer Tree,生成 Layer Tree 并不能直接使用,還需要 Compositing 合成為一個 Scene 并進行 Rasterize 光栅化處理。層級合并的原因是因為一般 Flutter 的層級很多,直接把每一層傳遞給 GPU 效率很低,是以會先做 Composite 提高效率。光栅化之後才會交給 Flutter Engine 處理。
Compositing
- 将 Layout Tree 合成為 Scene,并建立場景目前狀态的栅格圖像,即進行 Rasterize 光栅化處理,然後送出給 Flutter Engine,最後 Skia 通過 Open GL or Vulkan 接口送出資料給 GPU, GPU經過處理後進行顯示。
核心渲染階段
-
Widget
我們平時在寫的大都是
,Widget
其實可以了解為是一個元件樹的資料結構,是 Build 階段的主要部分。其中 Widget Tree 的深度、Widget
的StatefulWidget
合理性、build 函數中是否有不合理邏輯以及使用了調用setState
的相關Widget往往會成為性能問題。saveLayer
-
Element
關聯
和Widget
,生成RenderObject
對應的Widget
存放上下文資訊,Flutter 通過周遊Element
來生成Element
視圖樹支撐UI結構;RenderObject
-
RenderObject
RenderObject 在 Layout 階段确定布局資訊,Paint 階段生成為對應的 Layer,可見其重要程度。是以 Flutter 中大部分的繪圖性能優化發生在這裡。RenderObject 樹建構的資料會被加入到 Engine 所需的 LayerTree 中。
三、性能優化思路
了解底層渲染機制和核心渲染階段,可以将優化分為三層:
這裡不具體展開講每一層的優化細節,本文主要從實際的場景來講述。
四、流式場景
流式元件原理
在原生開發下,通常使用
RecyclerView/UICollectionView
進行清單場景的開發;在Flutter開發下,Flutter Framework 也提供了ListView的元件,它的實質其實是
SliverList
核心源碼
我們從
SliverList
的核心源碼來進行分析:
class SliverList extends SliverMultiBoxAdaptorWidget {
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
return RenderSliverList(childManager: element);
}
}
abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget {
final SliverChildDelegate delegate;
@override
SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this);
@override
RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
}
通過檢視
SliverList
的源代碼可知,
SliverList
是一個
RenderObjectWidget
,結構如下:
我們首先看它的
RenderObject
的核心源碼:
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
RenderSliverList({
@required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
@override
void performLayout(){
...
//父節點對子節點的布局限制
final SliverConstraints constraints = this.constraints;
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
final double remainingExtent = constraints.remainingCacheExtent;
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
...
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
...
insertAndLayoutChild(childConstraints,after: trailingChildWithLayout,parentUsesSize: true);
...
collectGarbage(leadingGarbage, trailingGarbage);
...
}
}
abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...{
@protected
RenderBox insertAndLayoutChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {
_createOrObtainChild(index, after: after);
...
}
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {@required RenderBox after,...}) {
_createOrObtainChild(index, after: after);
...
}
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
_destroyOrCacheChild(firstChild);
...
}
void _createOrObtainChild(int index, { RenderBox after }) {
_childManager.createChild(index, after: after);
...
}
void _destroyOrCacheChild(RenderBox child) {
if (childParentData.keepAlive) {
//為了更好的性能表現不會進行keepAlive,走else邏輯.
...
} else {
_childManager.removeChild(child);
...
}
}
}
檢視
RenderSliverList
的源碼發現,對于 child 的建立和移除都是通過其父類
RenderSliverMultiBoxAdaptor
進行。而
RenderSliverMultiBoxAdaptor
是通過
_childManager
即
SliverMultiBoxAdaptorElement
進行的,整個
SliverList
繪制過程中布局大小由父節點給出了限制。
在流式場景下:
- 在滑動過程中是通過
進行對進入可視區新的 child 的建立;(即業務場景的每一個item卡片)SliverMultiBoxAdaptorElement.createChild
-
進行對不在可視區舊的 child 的移除;SliverMultiBoxAdaptorElement.removeChild
我們來看下
SliverMultiBoxAdaptorElement
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
final SplayTreeMap<int, Element> _childElements = SplayTreeMap<int, Element>();
@override
void createChild(int index, { @required RenderBox after }) {
...
Element newChild = updateChild(_childElements[index], _build(index), index);
...
}
@override
void removeChild(RenderBox child) {
...
final Element result = updateChild(_childElements[index], null, index);
...
}
@override
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
final Element newChild = super.updateChild(child, newWidget, newSlot);
...
}
}
SliverMultiBoxAdaptorElement
的源碼可以發現,對于 child 的操作其實都是通過父類
Element
updateChild
進行的。
接下來,我們來看下
Element
的核心代碼:
abstract class Element extends DiagnosticableTree implements BuildContext {
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
...
bool hasSameSuperclass = oldElementClass == newWidgetClass;;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
newChild = inflateWidget(newWidget, newSlot);
}
...
return newChild;
}
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
...
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
...
return newChild;
}
@protected
void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject();
owner._inactiveElements.add(child); // this eventually calls child.deactivate() & child.unmount()
...
}
}
可以看到主要調用
Element
mount
detachRenderObject
,這裡我們來看下
RenderObjectElement
的 這兩個方法的源碼:
abstract class RenderObjectElement extends Element {
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
...
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
...
}
@override
void attachRenderObject(dynamic newSlot) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
...
}
@override
void detachRenderObject() {
if (_ancestorRenderObjectElement != null) {
_ancestorRenderObjectElement.removeChildRenderObject(renderObject);
_ancestorRenderObjectElement = null;
}
...
}
}
通過檢視上面源碼的追溯,可知:
- 在滑動過程中進入可視區新的 child 的建立,是通過建立全新的
并 mount 挂載到 Element Tree;然後建立對應的Element
,調用了RenderObject
_ancestorRenderObjectElement?.insertChildRenderObject
- 在滑動過程中不在可視區舊的 child 的移除,将對應的
從 Element Tree unmount 移除挂載;然後調用了Element
_ancestorRenderObjectElement.removeChildRenderObject
其實這個
_ancestorRenderObjectElement
就是
SliverMultiBoxAdaptorElement
,我們再來看下
SliverMultiBoxAdaptorElement
:
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
@override
void insertChildRenderObject(covariant RenderObject child, int slot) {
...
renderObject.insert(child as RenderBox, after: _currentBeforeChild);
...
}
@override
void removeChildRenderObject(covariant RenderObject child) {
...
renderObject.remove(child as RenderBox);
}
}
其實調用的都是
ContainerRenderObjectMixin
的方法,我們再來看下
ContainerRenderObjectMixin
mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ... {
void insert(ChildType child, { ChildType after }) {
...
adoptChild(child);// attach render object
_insertIntoChildList(child, after: after);
}
void remove(ChildType child) {
_removeFromChildList(child);
dropChild(child);// detach render object
}
}
ContainerRenderObjectMixin
維護了一個雙向連結清單來持有目前 children
RenderObject
,是以在滑動過程中建立和移除都會同步在
ContainerRenderObjectMixin
的雙向連結清單中進行添加和移除。
最後總結下來:
-
Element
, 通過調用RenderObject
attach 到 Render Tree,并同步将SliverMultiBoxAdaptorElement.insertChildRenderObject
添加到RenderObject
所 mixin 的雙連結清單中;SliverMultiBoxAdaptorElement
-
從 Element Tree unmount 移除挂載;然後通過用Element
将對應的SliverMultiBoxAdaptorElement.removeChildRenderObject
從所 mixin 的雙連結清單中移除并同步将RenderObject
從 Render Tree detach 掉。RenderObject
渲染原理
通過核心源碼的分析,我們可以對流式場景的
Element
做如下分類:
下面我們來看使用者向上滑動檢視更多商品卡片并觸發加載下一頁資料進行展示時,整體的渲染流程和機制:
- 向上滑動時,頂部 0 和 1 的卡片移出 Viewport 區域(Visible Area + Cache Area),我們定義它為進入 Detach Area,進入 Detach Area 後将對應的
從 Render Tree detach 掉,并且将對應的RenderObject
從 Element Tree unmount 移除挂載,并同步從雙向連結清單中移除;Element
- 通過監聽
的滑動計算位置來判斷是否需要開始加載下一頁資料,然後底部 Loading Footer 元件會進入可視區 or 緩存區,需要對ScrollController
的 childCount +1,最後一個 child 傳回 Loading Footer 元件,同時調用SliverChildBuilderDelegate
對整個setState
重新整理。SliverList
會調用update
進行重建構,中間部分在使用者可視區會全部進行 update 操作;然後建立 Loading Footer 元件對應新的performRebuild
Element
,并同步添加到雙向連結清單中;RenderObject
- 當 loading 結束資料傳回後,會再次調用
setState
重新整理,SliverList
update
進行重建構,中間部分在使用者可視區會全部進行 update 操作;然後将 Loading Footer 元件将對應的performRebuild
RenderObject
Element
- 底部新的 item 會進入可視區 or 緩存區,需要建立對應新的
Element
RenderObject
優化政策
上面使用者向上滑動檢視更多商品卡片并觸發加載下一頁資料進行展示的場景,可以從五個方向進行優化:
- Load More
的滑動不斷進行計算,最好無需判斷,自動識别到需要加載下一頁資料然後發起 loadMore() 回調。建立ScrollController
增加 loadMore 以及和 item Builder 同級的 footerBuilder,并預設包含 Loading Footer 元件,在ReuseSliverChildBuilderDelegate
判斷是否需要動态回調 loadMore() 并自動建構 footer 元件。SliverMultiBoxAdaptorElement.createChild(int index,...)
-
局部重新整理
參考了閑魚之前在長清單的流暢度優化[2],在下一頁資料回來之後調用
setState
重新整理,導緻中間部分在使用者可視區會全部進行 update 操作,實際隻需重新整理新建立的部分,優化SliverList
的部分實作局部重新整理,如下圖:SliverMultiBoxAdaptorElement.update(SliverMultiBoxAdaptorWidget newWidget)
-
Element & RenderObject 複用
參考了閑魚之前在長清單的流暢度優化[2] 和 Google Android RecyclerView ViewHolder 複用設計[3],在有新的 item 建立時,可以做類似 Android
RecyclerView
對元件進行持有并複用。基于對渲染機制原理分析,在 Flutter 中ViewHolder
其實可以了解為是一個元件樹的資料結構,即更多是元件結構的資料表達。我們需要對移除的 item 的Widget
Element
分元件類型進行緩存持有,在建立新的 item 的時候優先從緩存持有中取出進行複用。同時不破壞 Flutter 本身對RenderObject
的設計,當如果 item 有使用Key
的時候,隻複用和它Key
相同的Key
Element
。但在流式場景清單資料都是不同的資料,是以在流式場景中使用了RenderObject
,也就無法進行任何的複用。如果對Key
Element
進行複用,item 元件不建議使用RenderObject
我們在對原有流式場景下Key
的分類增加一個緩存态:Element
如下圖:
-
GC 抑制
Dart 自身有 GC 的機制,類似 Java 的分代回收,可以在滑動的過程中對 GC 進行抑制,定制 GC 回收的算法。針對這項和 Google 的 Flutter 專家讨論,其實 Dart 不像 Java 會存在多線程切換進行垃圾回收的情況,單線程(主isolate)垃圾回收更快更輕量級,同時需要對 Flutter Engine 做深度的改造,考慮收益不大暫不進行。
-
異步化
Flutter Engine 限制非 Main Isolate 調用 Platform 相關 Api,将非跟 Platform Thread 互動的邏輯全部放至新的 isolate 中,頻繁
的建立和回收也會對性能有一定的影響,FlutterIsolate
每次調用會建立新的compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String debugLabel })
,執行完任務後會進行回收,實作一個類似線程池的Isolate
來進行處理非視圖任務。經過實際測試提升不明顯,不展開講述。Isolate
核心技術實作
我們可以将調用鍊路的代碼做如下分類:
所有渲染核心在繼承自
RenderObjectElement
SliverMultiBoxAdaptorElement
中,不破壞原有功能設計以及 Flutter Framework 的結構,新增了
ReuseSliverMultiBoxAdaptorElement
Element
來進行優化政策的實作,并且可以直接搭配原有
SliverList
RenderSliverList
使用或者自定義的流式元件(例如:瀑布流元件)的
RenderObject
使用。
- 調用鍊路優化
在
ReuseSliverMultiBoxAdaptorElement
update
方法做是否為局部重新整理的判斷,如果不是局部重新整理依然走
performRebuild
;如果是局部重新整理,隻建立新産生的 item。
- 核心代碼
@override
void update(covariant ReuseSliverMultiBoxAdaptorWidget newWidget) {
...
//是否進行局部重新整理
if(_isPartialRefresh(oldDelegate, newDelegate)) {
...
Widget newWidget = _buildItem(index);
...
_createChild(index, newWidget);
} else {
// need to rebuild
performRebuild();
}
}
-
- 建立
ReuseSliverMultiBoxAdaptorElement
createChild
方法讀取
_cacheElements
對應元件類型緩存的
Element
進行複用;如果沒有同類型可複用的
Element
則建立對應新的
Element
RenderObject
- 移除
ReuseSliverMultiBoxAdaptorElement
removeChild
方法将移除的
RenderObject
從雙連結清單中移除,不進行
Element
的 deactive 和
RenderObject
的 detach,并将對應的
Element
_slot
更新為null,使下次可以正常複用,然後将對應的
Element
緩存到
_cacheElements
對應元件類型的連結清單中。
注:不 deactive
Element
其實不進行調用即可實作,但不 detach RenderObject 無法直接做到,需要在 Flutter Framework 層的
object.dart
檔案中,新增一個方法
removeOnly
就是隻将
RenderObject
從雙連結清單中移除不進行 detach。
//新增的方法,createChild會調用到這個方法
_createChild(int index, Widget newWidget){
...
Type delegateChildRuntimeType = _getWidgetRuntimeType(newWidget);
child = _takeChild(delegateChildRuntimeType,index);
...
newChild = updateChild(child, newWidget, index);
...
}
@override
void removeChild(RenderBox child) {
...
removeChildRenderObject(child); // call removeOnly
...
removeElement = _childElements.remove(index);
_performCacheElement(removeElement);
}
createChild
時候判斷是否是建構 footer 來進行處理。
@override
void createChild(int index, { @required RenderBox after }) {
...
Widget newWidget;
if(_isBuildFooter(index)){ // call footerBuilder & call onLoadMore
newWidget = _buildFooter();
}else{
newWidget = _buildItem(index);
}
...
_createChild(index, newWidget);
...
}
整體結構設計
- 将核心的優化能力内聚在
層,提供底層能力;Element
- 将
做為基類預設傳回優化後的ReuseSliverMultiBoxAdaptorWidget
;Element
- 将 loadMore 和 FooterBuilder 的能力統一由繼承自
SliverChildBuilderDelegate
對上層暴露;ReuseSliverChildBuilderDelegate
- 如有自己單獨定制的流式元件
,直接把繼承關系從Widget
換為RenderObjectWidget
即可,例如自定義的單清單元件(ReuseSliverList)、瀑布流元件(ReuseWaterFall)等。ReuseSliverMultiBoxAdaptorWidget
五、優化成果
基于在之前的一系列深度優化以及切換 Flutter Engine 為UC Hummer 之上,單獨控制流式場景的優化變量,使用 PerfDog 擷取流暢度資料,進行了流暢度測試對比:
可以看到整體性能資料都有優化提升,結合替換 Engine 之前的測試資料平均來看,對幀率有 2-3 幀的提升,卡頓率下降 1.5 個百分點。
六、總結
使用方式
和原生
SliverList
的使用方式一樣,
Widget
換成對應可以進行複用的元件 (ReuseSliverList/ReuseWaterFall/ CustomSliverList),delegate 如果需要
footer
loadMore
使用
ReuseSliverChildBuilderDelegate
;如果不需要直接使用原生的
SliverChildBuilderDelegate
即可。
- 需要分頁場景
return ReuseSliverList( // ReuseWaterFall or CustomSliverList
delegate: ReuseSliverChildBuilderDelegate(
(BuildContext context, int index) {
return getItemWidget(index);
},
//建構footer
footerBuilder: (BuildContext context) {
return DetailMiniFootWidget();
},
//添加loadMore監聽
addUnderFlowListener: loadMore,
childCount: dataOfWidgetList.length
)
);
- 無需分頁場景
return ReuseSliverList( // ReuseWaterFall or CustomSliverList
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return getItemWidget(index);
},
childCount: dataOfWidgetList.length
)
);
注意點
使用的時候 item/footer 元件不要加
Key
,否則認為隻對同
Key
進行複用。因為複用了
Element
,雖然表達元件樹資料結果的
Widget
會每次進行更新,但
StatefulElement
State
是在
Element
建立的時候生成的,同時也會被複用下來,和 Flutter 本身設計保持一緻,是以需要在
didUpdateWidget(covariant T oldWidget)
State
緩存的資料重新從
Widget
擷取即可。
Reuse Element Lifecycle
将每個 item 的狀态進行回調,上層可以做邏輯處理和資源釋放等,例如之前在
didUpdateWidget(covariant T oldWidget)
State
Widget
擷取可以放置在
onDisappear
裡或者自動播放的視訊流等;
/// 複用的生命周期
mixin ReuseSliverLifeCycle{
// 前台可見的
void onAppear() {}
// 背景不可見的
void onDisappear() {}
}
七、參考資料
[[1]:Google Flutter 團隊 Xiao Yu:Flutter Performance Profiling and Theory](
https://files.flutter-io.cn/events/gdd2018/Profiling_your_Flutter_Apps.pdf)[[2]:閑魚 雲從:他把閑魚APP長清單流暢度翻了倍](
https://mp.weixin.qq.com/s/dlOQ3Hw_U3CFQM91vcTGWQ)[[3]:Google Android RecyclerView.ViewHolder:RecyclerView.Adapter#onCreateViewHolder](
https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter#onCreateViewHolder(android.view.ViewGroup,%20int))