手把手教你定位Flutter PlatformView記憶體洩漏

YYDev發表於2020-03-13

本文主要對Flutter1.12.x版本iOS端在使用PlatformView記憶體洩漏時發生的記憶體洩漏問題進行修復,並以此為出發點從原始碼解析Platform的原理,希望讀者能收穫以下內容:

  1. 學會自助解決Flutter Engine其他問題
  2. 理解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的數量只會遞增,不會降下來:

手把手教你定位Flutter PlatformView記憶體洩漏

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 的入口處下斷點,直接跑起工程即可。

手把手教你定位Flutter PlatformView記憶體洩漏

PlatformView的實現原理

按照官方的文件,PlatformView的使用步驟主要有兩步。

  1. native向Flutter註冊一個實現FlutterPlatformViewFactory協議的例項並與一個ID繫結,ViewFactory的協議方法主要用於傳入一張UIView到Flutter層;
  2. 二是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的檢視層級

手把手教你定位Flutter PlatformView記憶體洩漏

我們知道當沒有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

手把手教你定位Flutter PlatformView記憶體洩漏

FlutterPlatformViews_Internal.mm

手把手教你定位Flutter PlatformView記憶體洩漏

以上修改已經給官方提了PR,大家也可以在上Github對照修改。

總結

綜上我們可以看到新建一個PlatformView的成本不小,這個成本不在PlatformView本身,而是FlutterOverlayView上,因為多了一張Surface,導致本來一次重新整理只需要繪製一次到FlutterView,現在需要多繪製一次到OverlayView上,而且可能不止一個。 但是好處也很明顯 ,很多音視訊SDK都是給出了一張UIView或者AndroidView到native,使用PlatformView的不僅接入簡單,而且音視訊的渲染效能表現和原生保持一致。

作者

Levi

相關文章