1.Flutter架構
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內部,這就使得它具有了很好的跨端一致性。
2.Flutter檢視繪製
對於開發者來說,使用最多的還是framework,我就從Flutter的入口函式開始一步步往下走,分析一下Flutter檢視繪製的原理。
在Flutter應用中,main()函式最簡單的實現如下
// 引數app是一個widget,是Flutter應用啟動後要展示的第一個Widget。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
1.WidgetsFlutterBinding
WidgetsFlutterBinding繼承自BindingBase 並混入了很多Binding,檢視這些 Binding的原始碼可以發現這些Binding中基本都是監聽並處理Window物件(包含了當前裝置和系統的一些資訊以及Flutter Engine的一些回撥)的一些事件,然後將這些事件按照Framework的模型包裝、抽象然後分發。
WidgetsFlutterBinding正是粘連Flutter engine與上層Framework的“膠水”。
- GestureBinding:提供了window.onPointerDataPacket 回撥,繫結Framework手勢子系統,是Framework事件模型與底層事件的繫結入口。
- ServicesBinding:提供了window.onPlatformMessage 回撥, 用於繫結平臺訊息通道(message channel),主要處理原生和Flutter通訊。
- SchedulerBinding:提供了window.onBeginFrame和window.onDrawFrame回撥,監聽重新整理事件,繫結Framework繪製排程子系統。
- PaintingBinding:繫結繪製庫,主要用於處理圖片快取。
- SemanticsBinding:語義化層與Flutter engine的橋樑,主要是輔助功能的底層支援。
- RendererBinding: 提供了window.onMetricsChanged 、window.onTextScaleFactorChanged 等回撥。它是渲染樹與Flutter engine的橋樑。
- 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;
}
}
看到這個WidgetsFlutterBinding混入(with)很多的Binding,下面先看父類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.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;
}
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()方法原始碼:
@override
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();
...
}
4.小結
Flutter從啟動到顯示影像在螢幕主要經過:首先監聽處理window物件的事件,將這些事件處理包裝為Framework模型進行分發,通過widget建立element樹,接著通過scheduleWarmUpFrame進行渲染,接著通過Rendererbinding進行佈局,繪製,最後通過呼叫ui.window.render(scene)Scene資訊發給Flutter engine,Flutter engine最後呼叫渲染API把影像畫在螢幕上。
我大致整理了一下Flutter檢視繪製的時序圖,如下
3.Flutter效能監控
在對檢視繪製有一定的瞭解後後,思考一個問題,怎麼在檢視繪製的過程中去把控效能,優化效能,我們先來看一下Flutter官方提供給我們的兩個效能監控工具
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=/
比方說選擇了timeline後,可以進行效能分析,如圖
2.devTools
devTools也提供了一些基本的檢測,具體的細節沒有Observatory提供的完善. 可視性比較強
可以通過下面命令安裝
flutter pub global activate devtools
安裝完成後通過devtools命令開啟,輸入DartVM地址
開啟後的頁面
devtools中的timeline就是performance,我們選擇之後頁面如下,操作體驗上好了很多
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 之後我們就可以使用 vm_service 的服務了,官方有一個幫我們寫好的 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);
}
獲取記憶體資訊,呼叫一個VmService例項的getMemoryUsage,就能拿到當前的記憶體資訊
Future<MemoryUsage> getMemoryUsage(String isolateId) =>
_call('getMemoryUsage', {'isolateId': isolateId});
獲取 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
}
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 外拋的異常。
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details.exception, details.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.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
4.Flutter效能優化
在業務開發中我們要學會用devtools來檢測工程效能,這樣有助於我們實現健壯性更強的應用,在排查過程中,我發現視訊詳情頁存在渲染耗時的問題,如圖
1.build耗時優化
VideoControls控制元件的build耗時是28.6ms,如圖
所以這裡我們的優化方案是提高build效率,降低Widget tree遍歷的出發點,將setState重新整理資料儘量下發到底層節點,所以將VideoControl內觸發重新整理的子元件抽取成獨立的Widget,setState下發到抽取出的Widget內部
優化後為11.0ms,整體的平均幀率也達到了了60fps,如圖
2.paint耗時優化
接下來分析下paint過程有沒有可以優化的部分,我們開啟debugProfilePaintsEnabled變數分析可以看到Timeline顯示的paint層級,如圖
我們發現頻繁更新的_buildPositionTitle和其他Widget在同一個layer中,這裡我們想到的優化點是利用RepaintBoundary提高paint效率,它為經常發生顯示變化的內容提供一個新的隔離layer,新的layer paint不會影響到其他layer
看下優化後的效果,如圖
3.小結
在Flutter開發過程中,我們用devtools工具排查定位頁面渲染問題時,主要有兩點:
1.提高build效率,setState重新整理資料儘量下發到底層節點。
2.提高paint效率,RepaintBoundry建立單獨layer減少重繪區域。
當然 Flutter 中效能調優遠不止這一種情況,build / layout / paint 每一個過程其實都有很多能夠優化的細節。
5.總結
1.回顧
這篇文章主要從三個維度來介紹Flutter這門技術,分別為繪製原理講解,我們review了一下原始碼,發現整個渲染過程就是一個閉環,Framework,Engine,Embedder各司其職,簡單來說就是Embedder不斷拿回Vsync訊號,Framework將dart程式碼交給Engine翻譯成跨平臺程式碼,再通過Embedder回撥宿主平臺;效能監控就是不斷得在這個迴圈中去插入我們的哨兵,觀察整個生態,獲取異常資料上報;效能優化通過一次專案實踐,學習怎麼用工具提升我們定位問題的效率。
2.優缺點
優點:
我們可以看到Flutter在檢視繪製過程中形成了閉環,雙端基本保持了一致性,所以我們的開發效率得到了極大的提升,效能監控和效能優化也比較方便。
缺點:
1)宣告式開發 動態操作檢視節點不是很友好,不能像原生那樣指令式程式設計,或者像前端獲取dom節點那般容易
2)實現動態化機制,目前沒有比較好的開源技術可以去借鑑