本文主要對Flutter1.12.x版本iOS端在使用PlatformView記憶體洩漏時發生的記憶體洩漏問題進行修復,並以此為出發點從原始碼解析Platform的原理,希望讀者能收穫以下內容:
- 學會自助解決Flutter Engine其他問題
- 理解Flutter PlatformView的實現原理
背景
Flutter官方版本目前已經完成了1.12的大進化,該版本自1.9後解決了4,571 個報錯,合併了 1,905 份 pr,實踐中1.12在dart物件記憶體釋放上做了很大優化。通過devtool反覆進出同一頁面測試發現,1.12解決了在1.9下大量dart物件常駐現象。然而當頁面使用到 PlatformView 的場景時發現,每次進出頁面增幅高達10M。使用Instrument分析發現IOSurface的數量只會遞增,不會降下來:
IOSurface是GL的渲染畫布,基本可以斷定這是Flutter渲染底層的洩漏,接下來開始我們的Flutter原始碼之旅。
除錯Flutter Engine
在除錯原始碼之前,需要編譯一個Flutter Engine(Flutter.framework)替換掉官方庫
一、編譯Flutter Engine
我們要站在巨人的肩膀上,充分利用現有資源,所以如何編譯不再累贅,可以關注下[《手把手教你編譯Flutter engine》] (juejin.im/post/5c24ac…)
1.1 構建unopt版本的engine
為了除錯時能讓程式碼聽話,按順序執行,我們需要構建未優化版本的Engine
ninja -C out/ios_debug_unopt // debug模式下,給手機裝置用
ninja -C out/ios_debug_sim_unopt // debug模式下,給模擬器用
複製程式碼
1.2 替換官方庫
將編譯後的Flutter.framework
拷貝至你的Flutter目錄下,具體路徑為 ${你的Flutter路徑}/bin/cache/artifacts/engine/ios
,這樣當你的應用打包時,app使用的Flutter.framework
就是我們剛剛打包的庫了。
1.3 在project中斷點
接下來我們將之前編譯Engine時生成的products.xcodeproj
拖入我們的App工程中,並在FlutterViewController.mm
的入口處下斷點,直接跑起工程即可。
PlatformView的實現原理
按照官方的文件,PlatformView的使用步驟主要有兩步。
- native向Flutter註冊一個實現FlutterPlatformViewFactory協議的例項並與一個ID繫結,ViewFactory的協議方法主要用於傳入一張UIView到Flutter層;
- 二是dart層使用UiKitView時將其viewType屬性設定為native註冊的ID值。
我們知道Flutter的實現就是一張GL畫布(FlutterView),而我們傳入native的PlatformView是如何與FlutterView合作展示的?
為了幫助你順利理解整個流程,我們會從FlutterViewController開始延伸,對Flutter的幾個核心類作用進行概述。
2.1 FlutterEngine
從上面我們知道Flutter的應用入口在FlutterViewController,不過他只是UIViewController的一個封裝,其成員變數FlutterEngine才是dart執行環境的管理者。實際上,FlutterEngine不僅可以不依賴FlutterViewController進行初始化,還可以隨意切換FlutterViewController。
FlutterViewController的最大的作用在於提供了一個畫布(self.view)供FlutterEngine繪製,這也是閒魚FlutterBoost庫的原理。
/// FlutterViewController.mm
// 第一種方式 傳入 engine 初始化 FlutterViewController
- (instancetype)initWithEngine:(FlutterEngine*)engine
nibName:(nullable NSString*)nibName
bundle:(nullable NSBundle*)nibBundle {
NSAssert(engine != nil, @"Engine is required");
self = [super initWithNibName:nibName bundle:nibBundle];
if (self) {
_engine.reset([engine retain]); // 重置engine邏輯,如清理畫布
...
[engine setViewController:self]; // engine重新繫結FlutterViewController
}
return self;
}
// 第二種方式 在 FlutterViewController 初始化時同步初始化 engine
- (instancetype)initWithProject:(nullable FlutterDartProject*)project
nibName:(nullable NSString*)nibName
bundle:(nullable NSBundle*)nibBundle {
self = [super initWithNibName:nibName bundle:nibBundle];
if (self) {
// new 一個engine例項
_engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter"
project:project
allowHeadlessExecution:NO]);
// 建立engine的排程中心 shell例項
[_engine.get() createShell:nil libraryURI:nil];
...
}
return self;
}
複製程式碼
FlutterEngine有兩個核心元件,一是Shell,二是FlutterPlatformViewsController。Shell是在FlutterViewController中主動呼叫engine createShell,而FlutterPlatformViewsController則是在Engine初始化時被建立。
// FlutterEngine.mm
- (instancetype)initWithName:(NSString*)labelPrefix
project:(FlutterDartProject*)project
allowHeadlessExecution:(BOOL)allowHeadlessExecution {
...
// 建立FlutterPlatformViewsController
_platformViewsController.reset(new flutter::FlutterPlatformViewsController());
...
}
複製程式碼
2.2 Shell
Shell例項也是FlutterEngine的成員,如果說FlutterEngine是Flutter執行環境的管理者,那其成員shell則是FlutterEngine的大腦,負責協調任務排程,Flutter的四大執行緒皆由shell管理。
我們都知道Flutter內部有四條執行緒:
Platform執行緒,用於和native事件通訊,如eventchannel,messagechannel
gpu執行緒,用於在native的畫布上繪製UI元素
dart執行緒(ui執行緒),用於執行dart程式碼邏輯的執行緒
io執行緒,由於dart的執行是單執行緒的,所以需要將io這種等待耗時的操作放另外一條執行緒
/// FlutterEngine.mm
// 建立shell例項
- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI
{
...
if (flutter::IsIosEmbeddedViewsPreviewEnabled()) {
// 當Flutter使用到PlatformView時
flutter::TaskRunners task_runners(threadLabel.UTF8String, // label
fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform
fml::MessageLoop::GetCurrent().GetTaskRunner(), // gpu
_threadHost.ui_thread->GetTaskRunner(), // ui
_threadHost.io_thread->GetTaskRunner() // io
);
_shell = flutter::Shell::Create(std::move(task_runners), // task runners
std::move(settings), // settings
on_create_platform_view, // platform view creation
on_create_rasterizer // rasterzier creation
);
} else {
flutter::TaskRunners task_runners(threadLabel.UTF8String, // label
fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform
_threadHost.gpu_thread->GetTaskRunner(), // gpu
_threadHost.ui_thread->GetTaskRunner(), // ui
_threadHost.io_thread->GetTaskRunner() // io
);
_shell = flutter::Shell::Create(std::move(task_runners), // task runners
std::move(settings), // settings
on_create_platform_view, // platform view creation
on_create_rasterizer // rasterzier creation
);
}
...
}
複製程式碼
從上面程式碼我們可以知道當應用標識自己使用了PlatformView時,platform執行緒和gpu執行緒共用同個執行緒,由於FlutterViewController是在主執行緒初始化的,所以也就是共用了iOS的主執行緒。關於這點,如果你的App有用到其他渲染相關的程式碼,如直播sdk,要格外注意最好不要讓你的GL程式碼執行在主執行緒,如果是在沒辦法那呼叫前要先設定GLContext(setCurrentContext),否則會干擾到Flutter的GL狀態機,造成白屏或者甚至崩潰。
2.3 Rasterizer
rasterizer是shell的一個成員變數,每個shell僅有唯一一個rasterizer,且必須工作在GPU執行緒。當dart程式碼在dart執行緒計算生成 layer_tree 後,會回撥shell的代理方法OnAnimatorDraw()
。此時shell充當排程中心,將UI配置資訊投遞到GPU執行緒上,由rasterizer執行下一步操作。
/// shell.cc
void Shell::OnAnimatorDraw(fml::RefPtr<Pipeline<flutter::LayerTree>> pipeline) {
// shell充當排程中心,將UI配置資訊投遞到GPU執行緒上,並由rasterizer執行下一步操作
task_runners_.GetGPUTaskRunner()->PostTask(
[& waiting_for_first_frame = waiting_for_first_frame_,
&waiting_for_first_frame_condition = waiting_for_first_frame_condition_,
rasterizer = rasterizer_->GetWeakPtr(),
pipeline = std::move(pipeline)]() {
// pipeline只是一個執行緒安全容器,其內容LayerTree是dart中的widget樹經過計算後輸出的不可變UI描述物件
if (rasterizer) {
rasterizer->Draw(pipeline);
...
}
});
}
複製程式碼
rasterizer持有兩個核心元件,一是Surface,是EGALayer的封裝,作為主屏畫布;二是CompositorContext例項,他持有所有繪製相關的資訊,方便對LayerTree進行處理。
Rasterizer::DrawToSurface主要做了三件事情:
1 生成ScopedFrame聚合當前surface和gl資訊
2 呼叫ScopedFrame的Raster方法,將layer_tree進行光柵化
3 如果存在PlatformView,最後呼叫submitFrame做最終處理
/// compositor_context.cc
RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {
...
// 1 生成compositor_frame聚合當前surface和gl資訊
auto compositor_frame = compositor_context_->AcquireFrame(
surface_->GetContext(), // skia GrContext
root_surface_canvas, // root surface canvas
external_view_embedder, // external view embedder
root_surface_transformation, // root surface transformation
true, // instrumentation enabled
gpu_thread_merger_ // thread merger
);
if (compositor_frame) {
// 2 呼叫ScopedFrame::Raster方法,將layer_tree進行光柵化
RasterStatus raster_status = compositor_frame->Raster(layer_tree, false);
...
if (external_view_embedder != nullptr) {
// 3 最後submitFrame這一步基本上是為了PlatformView而才存在 詳見
external_view_embedder->SubmitFrame(surface_->GetContext());
}
...
return raster_status;
}
return RasterStatus::kFailed;
}
複製程式碼
我們都知道Flutter的繪製底層框架是SKCanva,而dart程式碼輸出的是flutter::Layer物件,所以如果想把東西畫到螢幕上,需要進行一次預處理轉換物件(preroll),再繪製圖形(paint)。如下程式碼:
layertree 是一個指向頂點的樹狀結果資料物件,其子節點為dart的widget物件對映而來。
比如dart中的Container對應flutter::ContainerLayer,而UiKitView則對應flutter::PlatformViewLayer。
layertree 會按照深度優先演算法逐級從頂點到葉子節點呼叫Preroll和Paint。
/// rasterizer.cc
RasterStatus CompositorContext::ScopedFrame::Raster(
flutter::LayerTree& layer_tree,
bool ignore_raster_cache) {
// 預處理,將dart傳過來的UI配置資訊,轉化為skia位置大小資訊
layer_tree.Preroll(*this, ignore_raster_cache);
...
// 填充圖形
layer_tree.Paint(*this, ignore_raster_cache);
return RasterStatus::kSuccess;
}
複製程式碼
/// platform_view_layer.cc
void PlatformViewLayer::Preroll(PrerollContext* context,
const SkMatrix& matrix) {
...
// 詳見2.4
context->view_embedder->PrerollCompositeEmbeddedView(view_id_,
std::move(params));
}
void PlatformViewLayer::Paint(PaintContext& context) const {
// 詳見2.4
SkCanvas* canvas = context.view_embedder->CompositeEmbeddedView(view_id_);
context.leaf_nodes_canvas = canvas;
}
複製程式碼
2.4 FlutterPlatformViewsController
FlutterPlatformViewsController 例項是 FlutterEngine 的成員,用於管理所有 PlatformView 的新增移除,位置大小,層級順序。dart層的一個 UiKitView 都會對應到 native 層的一個 PlatformView,兩者通過持有相同的 viewid 進行關聯,每次建立一個新 PlatformView 時,viewid++。
當 PlatformViewLayer.Preroll 時,會呼叫 FlutterPlatformViewsController 例項的PrerollCompositeEmbeddedView 方法,該方法新建一個 Skia 物件以view_id
為 key 儲存在picture_recorders_
字典中,同時將view_id
放入composition_order_
陣列中,該陣列用於記錄 PlatformView 的層級資訊.
/// FlutterPlatformViews.mm
void FlutterPlatformViewsController::PrerollCompositeEmbeddedView(
int view_id,
std::unique_ptr<EmbeddedViewParams> params) {
// 根據 view_id 生成一個skia物件
picture_recorders_[view_id] = std::make_unique<SkPictureRecorder>();
picture_recorders_[view_id]->beginRecording(SkRect::Make(frame_size_));
picture_recorders_[view_id]->getRecordingCanvas()->clear(SK_ColorTRANSPARENT);
// 記錄 view_id 到 composition_order_ 陣列中
composition_order_.push_back(view_id);
...
}
複製程式碼
當 PlatformViewLayer.Paint 時,會呼叫 FlutterPlatformViewsController 例項的CompositeEmbeddedView 方法,該方法根據之前 preroll 生成的skia物件,返回一個SKCanavs,並賦值給 PaintContext 的leaf_nodes_canvas
。
注意,此處更換了 PaintContext 的leaf_nodes_canvas
,而flutter::Layer們的內容就是畫在leaf_nodes_canvas
上,這意味著當呼叫完該 PlatformViewLayer 的 Paint 方法後,接下來若有其他 flutter::Layer 呼叫 Paint,其內容將繪製在與該新的 SKCanvas 上。
SkCanvas* FlutterPlatformViewsController::CompositeEmbeddedView(int view_id) {
...
return picture_recorders_[view_id]->getRecordingCanvas();
}
複製程式碼
現在我們看下當dart有兩個PlatformView存在時,iOS的檢視層級
我們知道當沒有PlatformView時,iOS的檢視中就只有一個FlutterView,而現在每多一個UiKitView時,iOS的層級上會至少都多3個View,分別為:
1 PlatformView
由 FlutterPlatformViewFactory 返回的原生 UIView
2 FlutterTouchInterceptingView
倘若 PlatformView 直接加在 FlutterView 上,按照iOS點選的響應鏈順序,手勢事件會直接落在 PlatformView 上,而Flutter的邏輯都在dart上,點選事件也不例外,所以不能讓 PlatformView 自己消化。所以這裡加多了 FlutterTouchInterceptingView,將其作為 PlatformView 的父view,再新增到 FlutterView 上,FlutterTouchInterceptingView 內部邏輯會將事件轉發到 FlutterViewController 上,確保點選手勢統一由dart處理。
3 FlutterOverlayView
作為 PlatformView 的蒙層,因為倘若在dart中有部分檢視元素需要蓋在 UiKitView 之上,那部分UI元素就需要繪製在 FlutterOverlayView 上了。這也就解釋了為什麼 PlatformViewLayer 在調了 Paint 後需要把 PaintContext 的leaf_nodes_canvas
切換到一個新的畫布上,就是為了元素層級堆疊時,能將正確的內容繪製在 FlutterOverlayView 上
At last,我們看下 Rasterizer::DrawToSurface 中最後 SubmitFrame 的邏輯,這一步主要就是對將之前preroll和paint的鋪墊進行閉環。
bool FlutterPlatformViewsController::SubmitFrame(GrContext* gr_context,
std::shared_ptr<IOSGLContext> gl_context) {
...
bool did_submit = true;
for (int64_t view_id : composition_order_) {
// 初始化FlutterOverlayView,為每個PlatformView生成一個OverlayView(EGALayer)放在overlays_字典中
EnsureOverlayInitialized(view_id, gl_context, gr_context);
auto frame = overlays_[view_id]->surface->AcquireFrame(frame_size_);
if (frame) {
// 重點!!下面程式碼可以理解為,把picture_recorders_[view_id]的畫布內容,拷貝到overlays_[view_id]上。
SkCanvas* canvas = frame->SkiaCanvas();
canvas->drawPicture(picture_recorders_[view_id]->finishRecordingAsPicture());
canvas->flush();
did_submit &= frame->Submit();
}
}
picture_recorders_.clear();
if (composition_order_ == active_composition_order_) {
// active_composition_order_是上一次的Submit後的PlatformView層級順序
// composition_order_是本次PlatformView層級順序,如果相等則表示層級順序沒變
// 那FlutterPlatformViewsController的Submit操作結束
composition_order_.clear();
return did_submit;
}
// flutter_view就是一開始我們提到的FlutterViewController的self.view
UIView* flutter_view = flutter_view_.get();
for (size_t i = 0; i < composition_order_.size(); i++) {
int view_id = composition_order_[i];
// platform_view_root就是PlatformView
UIView* platform_view_root = root_views_[view_id].get();
// overlay就是PlatformView的蒙層,每個PlatformView都有一個overlay
UIView* overlay = overlays_[view_id]->overlay_view;
// 下面是往FlutterViewController.view addSubview的邏輯
if (platform_view_root.superview == flutter_view) {
[flutter_view bringSubviewToFront:platform_view_root];
[flutter_view bringSubviewToFront:overlay];
} else {
[flutter_view addSubview:platform_view_root];
[flutter_view addSubview:overlay];
overlay.frame = flutter_view.bounds;
}
// 最後儲存下本地圖層順序,如果沒有下次submit發現層級沒變的話,上面就可以提前結束了
active_composition_order_.push_back(view_id);
}
composition_order_.clear();
return did_submit;
}
複製程式碼
Fix 記憶體洩漏
回到我們一開是討論的記憶體洩漏,從instrument上看是Surface的洩漏,到目前為止能作為surface畫布且會不斷建立的只有FlutterOverlayerView,我看下他是如何別建立的
scoped_nsobject是Flutter的模板類,在出作用域時會對內容進行[obj release];
void FlutterPlatformViewsController::EnsureOverlayInitialized(
int64_t overlay_id,
std::shared_ptr<IOSGLContext> gl_context,
GrContext* gr_context) {
...
// init+retain 引用計數+2,而scoped_nsobject只會進行一次-1操作
fml::scoped_nsobject<FlutterOverlayView> overlay_view(
[[[FlutterOverlayView alloc] initWithContentsScale:contentsScale] retain]);
std::unique_ptr<IOSSurface> ios_surface =
[overlay_view.get() createSurface:std::move(gl_context)];
std::unique_ptr<Surface> surface = ios_surface->CreateGPUSurface(gr_context);
overlays_[overlay_id] = std::make_unique<FlutterPlatformViewLayer>(
std::move(overlay_view), std::move(ios_surface), std::move(surface));
overlays_[overlay_id]->gr_context = gr_context;
}
複製程式碼
從上面程式碼我們發現程式碼在建立FlutterOverlayView時調多了一次retain,這會導致FlutterOverlayView最終引用技術為1不釋放。
這是最大一塊Memory Leak,當然還有其他會引起洩漏的程式碼,都是引用技術的錯誤,篇幅原因我就不再一一解釋了,大家直接改吧
FlutterPlatformViews.mm
FlutterPlatformViews_Internal.mm
以上修改已經給官方提了PR,大家也可以在上Github對照修改。
總結
綜上我們可以看到新建一個PlatformView的成本不小,這個成本不在PlatformView本身,而是FlutterOverlayView上,因為多了一張Surface,導致本來一次重新整理只需要繪製一次到FlutterView,現在需要多繪製一次到OverlayView上,而且可能不止一個。 但是好處也很明顯 ,很多音視訊SDK都是給出了一張UIView或者AndroidView到native,使用PlatformView的不僅接入簡單,而且音視訊的渲染效能表現和原生保持一致。