技術乾貨 | Flutter在線編程實踐總結
本文主要記錄瞭如何一步步學習瞭解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內部,這就使得它具有了很好的跨端一致性。
![](http://oscimg.oschina.net/oscnet/b2f96196-a9dd-4a45-8175-ffb70ece463b.png)
圖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實例
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
final PlatformDispatcher platformDispatcher;
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 隊列。
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()方法源碼:
void 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。
// WidgetsBinding
void 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視圖繪製的時序圖,如下
![](http://oscimg.oschina.net/oscnet/98cb9a79-1b1c-4eb5-9bd7-f090ca0c50bb.png)
圖 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=/
![](http://oscimg.oschina.net/oscnet/f2998afc-bd6f-4f5a-ac35-95514c29fd6a.png)
圖 3
比方説選擇了timeline後,可以進行性能分析,如圖
![](http://oscimg.oschina.net/oscnet/8b854792-1f8f-4f16-b0a6-9cd87f0ab9b7.png)
圖 4
2.devTools
Observatory
提供的完善. 可視性比較強。
![](http://oscimg.oschina.net/oscnet/a048d36a-74b4-402e-a8c4-93aefcaed185.png)
圖 5
打開後的頁面
![](http://oscimg.oschina.net/oscnet/30703f46-5970-4ee9-b9e0-9f2a37ad6b53.png)
圖 6
devtools中的timeline就是performance,我們選擇之後頁面如下,操作體驗上好了很多
![](http://oscimg.oschina.net/oscnet/18ba2bc7-da9f-46e7-8537-1ce4fbffde7f.png)
圖 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);
}
![](http://oscimg.oschina.net/oscnet/32ed696f-275c-4f30-90a1-756fda036ca0.png)
圖 8
獲取內存信息,調用一個VmService實例的getMemoryUsage,就能拿到當前的內存信息
Future<MemoryUsage> getMemoryUsage(String isolateId) =>
_call('getMemoryUsage', {'isolateId': isolateId});
![](http://oscimg.oschina.net/oscnet/49a97650-2827-4f4c-b7fc-3cba6c551907.png)
圖 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 可以重新定義自己的print
、timers
、microtasks
還有最關鍵的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 • http://github.com/flutter/flutter.git
Framework • revision f4abaa0735 (4 months ago) • 2021-07-01 12:46:11 -0700
Engine • revision 241c87ad80
Tools • 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來檢測工程性能,這樣有助於我們實現健壯性更強的應用,在排查過程中,我發現視頻詳情頁存在渲染耗時的問題,如圖
![](http://oscimg.oschina.net/oscnet/5e272896-ad4e-4a5d-a0f4-1711fef10b1c.png)
圖 10
4.1
build耗時優化
VideoControls控件的build耗時是28.6ms,如圖
![](http://oscimg.oschina.net/oscnet/d97c68a5-6993-4e2d-ac39-4c02c58cb767.png)
圖 11
所以這裏我們的優化方案是提高build效率,降低Widget tree遍歷的出發點,將setState刷新數據儘量下發到底層節點,所以將VideoControl內觸發刷新的子組件抽取成獨立的Widget,setState下發到抽取出的Widget內部
優化後為11.0ms,整體的平均幀率也達到了了60fps,如圖
![](http://oscimg.oschina.net/oscnet/eadfe565-ec60-4254-a7b0-f8d254414235.png)
圖 12
4.2
paint耗時優化
接下來分析下paint過程有沒有可以優化的部分,我們打開debugProfilePaintsEnabled變量分析可以看到Timeline顯示的paint層級,如圖
![](http://oscimg.oschina.net/oscnet/4bfefcb3-b73e-4b56-9b59-541430ef4427.png)
圖 13
我們發現頻繁更新的_buildPositionTitle和其他Widget在同一個layer中,這裏我們想到的優化點是利用RepaintBoundary提高paint效率,它為經常發生顯示變化的內容提供一個新的隔離layer,新的layer paint不會影響到其他layer
看下優化後的效果,如圖
![](http://oscimg.oschina.net/oscnet/075f11cf-10b2-4712-8889-19369a1c3278.png)
圖 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源創計劃”,歡迎正在閲讀的你也加入,一起分享。
- 有道詞典Android客户端包體積優化之路
- 有道詞典Android客户端包體積優化之路
- 從 Redux 源碼談談函數式編程
- 從 Redux 源碼談談函數式編程
- 測試在項目流程中的那些事兒
- 程序設計優化之管道數據流
- js幾種網絡請求方式梳理——擺脱回調地獄
- 前端技術分享:頁面性能優化問題覆盤
- 前端技術分享:頁面性能優化問題覆盤
- 有道圍棋 AI:智能匹配兒童棋力的良師益友
- 網易有道 REDIS 雲原生實戰
- 網易有道 REDIS 雲原生實戰
- 語音合成(TTS)技術在有道詞典筆中的應用實踐
- 語音合成(TTS)技術在有道詞典筆中的應用實踐
- 在有道 | 同宇:一個正在老去的程序員
- 上班沒找到車位,硬核程序員做了套“園區車位實時推薦系統”,還獲了獎
- 技術乾貨 | Flutter在線編程實踐總結
- NEJ Build太慢怎麼辦?試試MOOC NEJ吧,只需兩步,提升70%構建性能!
- NEJ Build太慢怎麼辦?試試MOOC NEJ吧,只需兩步,提升70%構建性能!
- 遞推算法與遞推套路(手撕算法篇)