本文主要對Flutter1.12.x版本iOS端在使用PlatformView內存泄漏時發生的內存泄漏問題進行修復,並以此爲出發點從源碼解析Platform的原理,但願讀者能收穫如下內容:node
- 學會自助解決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的數量只會遞增,不會降下來:ios
IOSurface是GL的渲染畫布,基本能夠判定這是Flutter渲染底層的泄漏,接下來開始咱們的Flutter源碼之旅。git
在調試源碼以前,須要編譯一個Flutter Engine(Flutter.framework)替換掉官方庫github
咱們要站在巨人的肩膀上,充分利用現有資源,因此如何編譯再也不累贅,能夠關注下[《手把手教你編譯Flutter engine》] (juejin.cn/post/684490…)
算法
爲了調試時能讓代碼聽話,按順序執行,咱們須要構建未優化版本的Engineshell
ninja -C out/ios_debug_unopt // debug模式下,給手機設備用
ninja -C out/ios_debug_sim_unopt // debug模式下,給模擬器用
複製代碼
將編譯後的Flutter.framework
拷貝至你的Flutter目錄下,具體路徑爲 ${你的Flutter路徑}/bin/cache/artifacts/engine/ios
,這樣當你的應用打包時,app使用的Flutter.framework
就是咱們剛剛打包的庫了。canvas
接下來咱們將以前編譯Engine時生成的products.xcodeproj
拖入咱們的App工程中,並在FlutterViewController.mm
的入口處下斷點,直接跑起工程便可。 數組
按照官方的文檔,PlatformView的使用步驟主要有兩步。
xcode
- native向Flutter註冊一個實現FlutterPlatformViewFactory協議的實例並與一個ID綁定,ViewFactory的協議方法主要用於傳入一張UIView到Flutter層;
- 二是dart層使用UiKitView時將其viewType屬性設置爲native註冊的ID值。
咱們知道Flutter的實現就是一張GL畫布(FlutterView),而咱們傳入native的PlatformView是如何與FlutterView合做展現的?
爲了幫助你順利理解整個流程,咱們會從FlutterViewController開始延伸,對Flutter的幾個核心類做用進行概述。安全
從上面咱們知道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());
...
}
複製代碼
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狀態機,形成白屏或者甚至崩潰。
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;
}
複製代碼
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; } 複製代碼
回到咱們一開是討論的內存泄漏,從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的不只接入簡單,並且音視頻的渲染性能表現和原生保持一致。