手把手教你定位Flutter PlatformView內存泄漏

本文主要對Flutter1.12.x版本iOS端在使用PlatformView內存泄漏時發生的內存泄漏問題進行修復,並以此爲出發點從源碼解析Platform的原理,但願讀者能收穫如下內容:node

  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的數量只會遞增,不會降下來:ios

IOSurface是GL的渲染畫布,基本能夠判定這是Flutter渲染底層的泄漏,接下來開始咱們的Flutter源碼之旅。git

調試Flutter Engine

在調試源碼以前,須要編譯一個Flutter Engine(Flutter.framework)替換掉官方庫github

1、編譯Flutter Engine

咱們要站在巨人的肩膀上,充分利用現有資源,因此如何編譯再也不累贅,能夠關注下[《手把手教你編譯Flutter engine》] (juejin.cn/post/684490…)
算法

1.1 構建unopt版本的engine

爲了調試時能讓代碼聽話,按順序執行,咱們須要構建未優化版本的Engineshell

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就是咱們剛剛打包的庫了。canvas

1.3 在project中斷點

接下來咱們將以前編譯Engine時生成的products.xcodeproj拖入咱們的App工程中,並在FlutterViewController.mm 的入口處下斷點,直接跑起工程便可。 數組

PlatformView的實現原理

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

  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的視圖層級

咱們知道當沒有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的不只接入簡單,並且音視頻的渲染性能表現和原生保持一致。

做者

Levi

相關文章
相關標籤/搜索