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

4,485 阅读10分钟

本文主要对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的数量只会递增,不会降下来:

IOSurface是GL的渲染画布,基本可以断定这是Flutter渲染底层的泄漏,接下来开始我们的Flutter源码之旅。

调试Flutter Engine

在调试源码之前,需要编译一个Flutter Engine(Flutter.framework)替换掉官方库

一、编译Flutter Engine

我们要站在巨人的肩膀上,充分利用现有资源,所以如何编译不再累赘,可以关注下[《手把手教你编译Flutter engine》] (juejin.cn/post/684490…)

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的使用步骤主要有两步。

  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