本文主要对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的不只接入简单,并且音视频的渲染性能表现和原生保持一致。