技術乾貨 | Flutter在線編程實踐總結

語言: CN / TW / HK

本文主要記錄瞭如何一步步學習瞭解Flutter視圖繪製原理,然後應用到性能監控和性能優化的實踐

作者 / 郭凱旋

編輯 / 李軼楠(實習)

youdao

1.Flutter架構

ydtech

Flutter的架構主要分成三層:Framework,Engine,Embedder。

1.Framework使用dart實現,包括Material Design風格的Widget,Cupertino(針對iOS)風格的Widgets,文本/圖片/按鈕等基礎Widgets,渲染,動畫,手勢等。此部分的核心代碼是:flutter倉庫下的flutter package,以及sky_engine倉庫下的io,async,ui(dart:ui庫提供了Flutter框架和引擎之間的接口)等package。

2.Engine使用C++實現,主要包括:Skia,Dart和Text。Skia是開源的二維圖形庫,提供了適用於多種軟硬件平台的通用API。

3.Embedder是一個嵌入層,即把Flutter嵌入到各個平台上去,這裏做的主要工作包括渲染Surface設置,線程設置,以及插件等。從這裏可以看出,Flutter的平台相關層很低,平台(如iOS)只是提供一個畫布,剩餘的所有渲染相關的邏輯都在Flutter內部,這就使得它具有了很好的跨端一致性。

圖1

youdao

2.Flutter視圖繪製

ydtech

對於開發者來説,使用最多的還是framework,我就從Flutter的入口函數開始一步步往下走,分析一下Flutter視圖繪製的原理。

在Flutter應用中,main()函數最簡單的實現如下:

void main() {  runApp(MyApp()); }

runApp方法調用了WidgetsFlutterBinding類ensureInitialized、attachRootWidget(app)、scheduleWarmUpFrame()三個方法,代碼如下

// 參數app是一個widget,是Flutter應用啟動後要展示的第一個Widget。void runApp(Widget app) {    WidgetsFlutterBinding.ensureInitialized()                 ..scheduleAttachRootWidget(app)      ..scheduleWarmUpFrame(); }


2.1

WidgetsFlutterBinding

WidgetsFlutterBinding繼承自BindingBase 並混入了很多Binding,查看這些 Binding的源碼可以發現這些Binding中基本都是監聽並處理Window對象(包含了當前設備和系統的一些信息以及Flutter Engine的一些回調)的一些事件,然後將這些事件按照Framework的模型包裝、抽象然後分發。

WidgetsFlutterBinding正是粘連Flutter engine與上層Framework的“膠水”。

1.GestureBinding:

提供了window.onPointerDataPacket 回調,綁定Framework手勢子系統,是Framework事件模型與底層事件的綁定入口。

2.ServicesBinding:

提供了window.onPlatformMessage 回調, 用於綁定平台消息通道(message channel),主要處理原生和Flutter通信。

3.SchedulerBinding:

提供了window.onBeginFrame和window.onDrawFrame回調,監聽刷新事件,綁定Framework繪製調度子系統。

4. PaintingBinding:

綁定繪製庫,主要用於處理圖片緩存。

5. SemanticsBinding:

語義化層與Flutter engine的橋樑,主要是輔助功能的底層支持。

6.RendererBinding: 

提供了window.onMetricsChanged 、window.onTextScaleFactorChanged 等回調。它是渲染樹與Flutter engine的橋樑。

7.WidgetsBinding:

提供了window.onLocaleChanged、onBuildScheduled 等回調。它是Flutter widget層與engine的橋樑。

WidgetsFlutterBinding.ensureInitialized()負責初始化一個WidgetsBinding的全局單例,代碼如下:

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {  static WidgetsBinding ensureInitialized() {    if (WidgetsBinding.instance == null)      WidgetsFlutterBinding();    return WidgetsBinding.instance;  }}


看到這個混入(with)很多的,下面先看父類:BindingBase

abstract class BindingBase {   ...  ui.SingletonFlutterWindow get window => ui.window;//獲取window實例  @protected  @mustCallSuper  void initInstances() {    assert(!_debugInitialized);    assert(() {      _debugInitialized = true;      return true;    }());  }}


看到有句代碼Window get window => ui.window鏈接宿主操作系統的接口,也就是Flutter framework 鏈接宿主操作系統的接口。系統中有一個Window實例,可以從window屬性來獲取,看看源碼:

// window的類型是一個FlutterView,FlutterView裏面有一個PlatformDispatcher屬性ui.SingletonFlutterWindow get window => ui.window;// 初始化時把PlatformDispatcher.instance傳入,完成初始化ui.window = SingletonFlutterWindow._(0, PlatformDispatcher.instance);// SingletonFlutterWindow的類結構class SingletonFlutterWindow extends FlutterWindow {  ...  // 實際上是給platformDispatcher.onBeginFrame賦值  FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;  set onBeginFrame(FrameCallback? callback) {    platformDispatcher.onBeginFrame = callback;  }    VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;  set onDrawFrame(VoidCallback? callback) {    platformDispatcher.onDrawFrame = callback;  }    // window.scheduleFrame實際上是調用platformDispatcher.scheduleFrame()  void scheduleFrame() => platformDispatcher.scheduleFrame();  ...}class FlutterWindow extends FlutterView {  FlutterWindow._(this._windowId, this.platformDispatcher);  final Object _windowId;  // PD  @override  final PlatformDispatcher platformDispatcher;  @override  ViewConfiguration get viewConfiguration {    return platformDispatcher._viewConfigurations[_windowId]!;  }}


2.2

scheduleAttachRootWidget

scheduleAttachRootWidget緊接着會調用WidgetsBinding的attachRootWidget方法,該方法負責將根Widget添加到RenderView上,代碼如下:

void attachRootWidget(Widget rootWidget) {    final bool isBootstrapFrame = renderViewElement == null;    _readyToProduceFrames = true;    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(      container: renderView,      debugShortDescription: '[root]',      child: rootWidget,    ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);    if (isBootstrapFrame) {      SchedulerBinding.instance!.ensureVisualUpdate();    }  }


renderView變量是一個RenderObject,它是渲染樹的根。renderViewElement變量是renderView對應的Element對象。可見該方法主要完成了根widget到根 RenderObject再到根Element的整個關聯過程。


RenderView get renderView => _pipelineOwner.rootNode! as RenderView;

renderView是RendererBinding中拿到PipelineOwner.rootNode,PipelineOwner在 Rendering Pipeline 中起到重要作用:

隨着 UI 的變化而不斷收集『 Dirty Render Objects 』隨之驅動 Rendering Pipeline 刷新 UI。

簡單講,PipelineOwner是『RenderObject Tree』與『RendererBinding』間的橋樑。

最終調用attachRootWidget,執行會調用RenderObjectToWidgetAdapter的attachToRenderTree方法,該方法負責創建根element,即RenderObjectToWidgetElement,並且將element與widget 進行關聯,即創建出 widget樹對應的element樹。如果element 已經創建過了,則將根element 中關聯的widget 設為新的,由此可以看出element 只會創建一次,後面會進行復用。BuildOwner是widget framework的管理類,它跟蹤哪些widget需要重新構建。代碼如下:

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {  if (element == null) {    owner.lockState(() {      element = createElement();      assert(element != null);      element.assignOwner(owner);    });    owner.buildScope(element, () {      element.mount(null, null);    });  } else {    element._newWidget = this;    element.markNeedsBuild();  }  return element;}


2.3

scheduleWarmUpFrame

runApp的實現中,當調用完attachRootWidget後,最後一行會調用 WidgetsFlutterBinding 實例的 scheduleWarmUpFrame() 方法,該方法的實現在SchedulerBinding 中,它被調用後會立即進行一次繪製(而不是等待"vsync" 信號),在此次繪製結束前,該方法會鎖定事件分發,也就是説在本次繪製結束完成之前Flutter將不會響應各種事件,這可以保證在繪製過程中不會再觸發新的重繪。

下面是scheduleWarmUpFrame() 方法的部分實現(省略了無關代碼):

void scheduleWarmUpFrame() {  ...  Timer.run(() {    handleBeginFrame(null);   });  Timer.run(() {    handleDrawFrame();      resetEpoch();  });  // 鎖定事件  lockEvents(() async {    await endOfFrame;    Timeline.finishSync();  }); ...}


該方法中主要調用了handleBeginFrame() 和 handleDrawFrame() 兩個方法。

查看handleBeginFrame() 和 handleDrawFrame() 兩個方法的源碼,可以發現前者主要是執行了transientCallbacks隊列,而後者執行了 persistentCallbacks 和 postFrameCallbacks 隊列。

1. transientCallbacks:用於存放一些臨時回調,一般存放動畫回調。可以通過SchedulerBinding.instance.scheduleFrameCallback 添加回調。

2. persistentCallbacks:用於存放一些持久的回調,不能在此類回調中再請求新的繪製幀,持久回調一經註冊則不能移除。    SchedulerBinding.instance.addPersitentFrameCallback(),這個回調中處理了佈局與繪製工作。

3. postFrameCallbacks:在Frame結束時只會被調用一次,調用後會被系統移除,可由 SchedulerBinding.instance.addPostFrameCallback() 註冊。

注意,不要在此類回調中再觸發新的Frame,這可以會導致循環。

真正的渲染和繪製邏輯在RendererBinding中實現,查看其源碼,發現在其initInstances()方法中有如下代碼:


void initInstances() {  ... // 省略無關代碼  addPersistentFrameCallback(_handlePersistentFrameCallback);}void _handlePersistentFrameCallback(Duration timeStamp) {  drawFrame();}void drawFrame() {  assert(renderView != null);  pipelineOwner.flushLayout(); // 佈局  pipelineOwner.flushCompositingBits(); //重繪之前的預處理操作,檢查RenderObject是否需要重繪  pipelineOwner.flushPaint(); // 重繪  renderView.compositeFrame(); // 將需要繪製的比特數據發給GPU  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.}

需要注意的是:由於RendererBinding只是一個mixin,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重寫該方法,查看WidgetsBinding的drawFrame()方法源碼:

@overridevoid drawFrame() { ...//省略無關代碼  try {    if (renderViewElement != null)      buildOwner.buildScope(renderViewElement);     super.drawFrame(); //調用RendererBinding的drawFrame()方法    buildOwner.finalizeTree();  } }

在調用RendererBinding.drawFrame()方法前會調用 buildOwner.buildScope() (非首次繪製),該方法會將被標記為“dirty” 的 element 進行 rebuild()


我們再來看WidgetsBinding,在initInstances()方法中創建BuildOwner對象,然後執行buildOwner!.onBuildScheduled = _handleBuildScheduled;,這裏將_handleBuildScheduled賦值給了buildOwnder的onBuildScheduled屬性。

BuildOwner對象,它負責跟蹤哪些widgets需要重新構建,並處理應用於widgets樹的其他任務,其內部維護了一個_dirtyElements列表,用以保存被標“髒”的elements。

每一個element被新建時,其BuildOwner就被確定了。一個頁面只有一個buildOwner對象,負責管理該頁面所有的element。

// WidgetsBindingvoid initInstances() {  ...  buildOwner!.onBuildScheduled = _handleBuildScheduled;  ...  }());}


當調用buildOwner.onBuildScheduled()時,便會走下面的流程。

// WidgetsBinding類void _handleBuildScheduled() {  ensureVisualUpdate();}// SchedulerBinding類void ensureVisualUpdate() {    switch (schedulerPhase) {      case SchedulerPhase.idle:      case SchedulerPhase.postFrameCallbacks:        scheduleFrame();        return;      case SchedulerPhase.transientCallbacks:      case SchedulerPhase.midFrameMicrotasks:      case SchedulerPhase.persistentCallbacks:        return;    }  }


當schedulerPhase處於idle狀態,會調用scheduleFrame,然後經過window.scheduleFrame()中的performDispatcher.scheduleFrame()去註冊一個VSync監聽。

 void scheduleFrame() {    ...
window.scheduleFrame(); ... }


2.4

小結

Flutter從啟動到顯示圖像在屏幕主要經過:首先監聽處理window對象的事件,將這些事件處理包裝為Framework模型進行分發,通過widget創建element樹,接着通過scheduleWarmUpFrame進行渲染,接着通過Rendererbinding進行佈局,繪製,最後通過調用ui.window.render(scene)Scene信息發給Flutter engine,Flutter engine最後調用渲染API把圖像畫在屏幕上。

我大致整理了一下Flutter視圖繪製的時序圖,如下

圖 2

youdao

3.Flutter性能監控

ydtech

在對視圖繪製有一定的瞭解後後,思考一個問題,怎麼在視圖繪製的過程中去把控性能,優化性能,我們先來看一下Flutter官方提供給我們的兩個性能監控工具。

3.1

Dart VM Service

1.observatory

observatory: 在engine/shell/testings/observatory可以找到它的具體實現,它開啟了一個ServiceClient,用於獲取dartvm運行狀態.flutter app啟動的時候會生成一個當前的observatory服務器的地址

flutter: socket connected in service Dart VM Service Protocol v3.44 listening on http://127.0.0.1:59378/8x9XRQIBhkU=/


圖 3

比方説選擇了timeline後,可以進行性能分析,如圖

圖 4

2.devTools

devTools也提供了一些基本的檢測,具體的細節沒有 Observatory 提供的完善. 可視性比較強。
可以通過下面命令安裝:
f lutter pub global activate devtools
安裝完成後通過devtools命令打開,輸入DartVM地址

圖 5

打開後的頁面

圖 6

devtools中的timeline就是performance,我們選擇之後頁面如下,操作體驗上好了很多

圖 7

observatory與devtools都是通過vm_service實現的,網上使用指南比較多,這邊就不多贅述了,我這邊主要介紹一下Dart VM Service (後面 簡稱 )vm_service,是 Dart 虛擬機內部提供的一套 Web 服務,數據傳輸協議是 JSON-RPC 2.0。

不過我們並不需要要自己去實現數據請求解析,官方已經寫好了一個可用的 Dart SDK 給我們用:vm_service。vm_service 在啟動的時候會在本地開啟一個 WebSocket 服務,服務 URI 可以在對應的平台中獲得:

 1)Android 在 FlutterJNI.getObservatoryUri() 中;

 2)iOS 在 FlutterEngine.observatoryUrl 中。


有了 URI 之後我們就可以使用  的服務了,官方有一個幫我們寫好的SDK: vm_service

  Future<void> connect() async {    ServiceProtocolInfo info = await Service.getInfo();    if (info.serverUri == null) {      print("service  protocol url is null,start vm service fail");      return;    }    service = await getService(info);    print('socket connected in service $info');    vm = await service?.getVM();    List<IsolateRef>? isolates = vm?.isolates;    main = isolates?.firstWhere((ref) => ref.name?.contains('main') == true);    main ??= isolates?.first;    connected = true;  }
Future<VmService> getService(info) async { Uri uri = convertToWebSocketUrl(serviceProtocolUrl: info.serverUri); return await vmServiceConnectUri(uri.toString(), log: StdoutLog()); }


獲取frameworkVersion,調用一個VmService實例的callExtensionService,傳入'flutterVersion',就能拿到當前的flutter framework和engine信息

  Future<Response?> callExtensionService(String method) async {    if (_extensionService == null && service != null && main != null) {      _extensionService = ExtensionService(service!, main!);      await _extensionService?.loadExtensionService();    }    return _extensionService!.callMethod(method);  }


圖 8

獲取內存信息,調用一個VmService實例的getMemoryUsage,就能拿到當前的內存信息

  Future<MemoryUsage> getMemoryUsage(String isolateId) =>      _call('getMemoryUsage', {'isolateId': isolateId});


圖 9

獲取 Flutter APP 的 FPS,官方提供了好幾個辦法來讓我們在開發 Flutter app 的過程中可以使用查看 fps等性能數據,如devtools,具體見文檔 Debugging Flutter apps 、Flutter performance profiling 等。

// 需監聽fps時註冊void start() {  SchedulerBinding.instance.addTimingsCallback(_onReportTimings);}// 不需監聽時移除void stop() {  SchedulerBinding.instance.removeTimingsCallback(_onReportTimings);}void _onReportTimings(List<FrameTiming> timings) {  // TODO}


3.2

崩潰日誌捕獲上報

flutter 的崩潰日誌收集主要有兩個方面:


1)flutter dart 代碼的異常(包含app和framework代碼兩種情況,一般不會引起閃退,你猜為什麼)

2)flutter engine 的崩潰日誌(一般會閃退)

Dart 有一個 Zone 的概念,有點類似sandbox的意思。不同的 Zone 代碼上下文是不同的互不影響,Zone 還可以創建新的子Zone。Zone 可以重新定義自己的printtimersmicrotasks還有最關鍵的how uncaught errors are handled 未捕獲異常的處理

runZoned(() {    Future.error("asynchronous error");}, onError: (dynamic e, StackTrace stack) {    reportError(e, stack);});


1.Flutter framework 異常捕獲

註冊 FlutterError.onError 回調,用於收集 Flutter framework 外拋的異常。

runZoned(() {    Future.error("asynchronous error");}, onError: (dynamic e, StackTrace stack) {    reportError(e, stack);});


2.Flutter engine 異常捕獲

flutter engine 部分的異常,以Android 為例,主要為 libfutter.so發生的錯誤。

這部份可以直接交給native崩潰收集sdk來處理,比如 firebase crashlytics、 bugly、xCrash 等等 

我們需要將 dart 異常及堆棧通過 MethodChannel傳遞給 bugly sdk 即可。 

收集到異常之後,需要查符號表(symbols)還原堆棧。

首先需要確認該 flutter engine 所屬版本號,在命令行執行:

flutter --version

輸出如下:

Flutter 2.2.3 • channel stable • https://github.com/flutter/flutter.gitFramework • revision f4abaa0735 (4 months ago) • 2021-07-01 12:46:11 -0700Engine • revision 241c87ad80Tools • Dart 2.13.4


可以看到 Engine 的 revision 為 241c87ad80。

其次,在 flutter infra 上找到對應cpu abi 的 symbols.zip 並下載,解壓後,可以得到帶有符號信息的 debug so 文件—— libflutter.so,然後按照平台文檔上傳進行堆棧還原就可以了,如bugly平台就提供了上傳工具

java -jar buglySymbolAndroid.jar -i  xxx


youdao

4.Flutter性能優化

ydtech

在業務開發中我們要學會用devtools來檢測工程性能,這樣有助於我們實現健壯性更強的應用,在排查過程中,我發現視頻詳情頁存在渲染耗時的問題,如圖

圖 10

4.1

build耗時優化

VideoControls控件的build耗時是28.6ms,如圖

圖 11

所以這裏我們的優化方案是提高build效率,降低Widget tree遍歷的出發點,將setState刷新數據儘量下發到底層節點,所以將VideoControl內觸發刷新的子組件抽取成獨立的Widget,setState下發到抽取出的Widget內部


優化後為11.0ms,整體的平均幀率也達到了了60fps,如圖

圖 12

4.2

paint耗時優化

接下來分析下paint過程有沒有可以優化的部分,我們打開debugProfilePaintsEnabled變量分析可以看到Timeline顯示的paint層級,如圖

圖 13

我們發現頻繁更新的_buildPositionTitle和其他Widget在同一個layer中,這裏我們想到的優化點是利用RepaintBoundary提高paint效率,它為經常發生顯示變化的內容提供一個新的隔離layer,新的layer paint不會影響到其他layer

看下優化後的效果,如圖

圖 14

4.3

小結

在Flutter開發過程中,我們用devtools工具排查定位頁面渲染問題時,主要有兩點:

1.提高build效率,setState刷新數據儘量下發到底層節點。

2.提高paint效率,RepaintBoundry創建單獨layer減少重繪區域。

當然 Flutter 中性能調優遠不止這一種情況,build / layout / paint 每一個過程其實都有很多能夠優化的細節。

youdao

5.回顧

ydtech

5.1

回顧

這篇文章主要從三個維度來介紹Flutter這門技術,分別為:

1.繪製原理講解,我們review了一下源碼,發現整個渲染過程就是一個閉環,Framework,Engine,Embedder各司其職,簡單來説就是Embedder不斷拿回Vsync信號,Framework將dart代碼交給Engine翻譯成跨平台代碼,再通過Embedder回調宿主平台;

2.性能監控就是不斷得在這個循環中去插入我們的哨兵,觀察整個生態,獲取異常數據上報;

3.性能優化通過一次項目實踐,學習怎麼用工具提升我們定位問題的效率。

5.2

優缺點

優點:

我們可以看到Flutter在視圖繪製過程中形成了閉環,雙端基本保持了一致性,所以我們的開發效率得到了極大的提升,性能監控和性能優化也比較方便。

缺點: 

1)聲明式開發 動態操作視圖節點不是很友好,不能像原生那樣命令式編程,或者像前端獲取dom節點那般容易;

2)實現動態化機制,目前沒有比較好的開源技術可以去借鑑。

- END -


本文分享自微信公眾號 - 有道技術團隊(youdaotech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。