上文使用篇介绍了如何使用原生的view来创建一个flutter widget。

但是,细心的同学应该能够注意到,官方文档UiKitView里面有下面这一段话,大体意思是嵌入原生的view是一个比较昂贵的操作(指性能不咋地)。

Embedding iOS views is an expensive operation and should be avoided when a Flutter equivalent is possible.

那么我们就来分析一下,flutter到底是如何去实现这个嵌套native view功能的,其性能具体又如何呢?

platform view的渲染过程

Flutter的渲染循环

首先我们来看看flutter的渲染循环具体有哪些步骤,借用一下官方的这个图:

flutter_draw
  1. 首先,每帧都是由vsync信号触发(在iOS上实际上是由CADisplayLink来触发的,具体可看VsyncWaiterIOS.mm的代码)

  2. 然后,在UI Thread由dart层处理3种树(widget、Element、RenderObject)的生成与更新

  3. 真正的渲染工作是在engine层处理的,其接受dart层传递的layerTree,然后通过GPU渲染出来。注意这里layerTree和RenderObject还有区别,并不是每一个renderObject节点都有对应的layer节点。

Engine层渲染layerTree

因为在dart侧生成layerTree的过程中,platform view widget的处理逻辑和普通的widget是一致的,最终处理都是生成一个layerTree。

所以我们这里主要关注engine层渲染layerTree的过程,platform view实际上会是layerTree上的一些叶子节点。

各种layer子类

image-20191127201050231

通过xcode的代码跳转提示,可以看到layerTree的叶子节点多种多样,那么和我们platform view相关的就是PlatformViewLayer了。当遍历到这种layer的时候,flutter就知道这里需要渲染一个native view了。

查看PlatformViewLayer相关代码,可以看到其具体逻辑都是由FlutterPlatformViewsController处理的。

FlutterPlatformViewsController的关键逻辑

onCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
NSDictionary<NSString*, id>* args = [call arguments];
long viewId = [args[@"id"] longValue];
std::string viewType([args[@"viewType"] UTF8String]);

NSObject<FlutterPlatformViewFactory>* factory = factories_[viewType].get();

NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero
viewIdentifier:viewId
arguments:params];
views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]);

FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
initWithEmbeddedView:embedded_view.view
flutterViewController:flutter_view_controller_.get()] autorelease];

touch_interceptors_[viewId] =
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]);

result(nil);
}

onCreate是由dart层触发,这块代码看过上篇使用篇的同学应该比较熟悉。关键点在于:

  1. 调用PlatformViewFactory来创建具体的native view,从使用篇mapview的demo里面,我们可以知道我们可以通过FlutterPlatformView协议的view方法拿到具体的native view实例
  2. 除了FlutterPlatformView之外,flutter还创建了一个FlutterTouchInterceptingView,实际上这个view是作为我们native view的父view存在的,作用是拦截或者传递native view的一些手势事件。

SubmitFrame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool FlutterPlatformViewsController::SubmitFrame(GrContext* gr_context,
std::shared_ptr<IOSGLContext> gl_context) {
DisposeViews();

UIView* flutter_view = flutter_view_.get();

for (size_t i = 0; i < composition_order_.size(); i++) {
int view_id = composition_order_[i];

UIView* platform_view_root = root_views_[view_id].get();
UIView* overlay = overlays_[view_id]->overlay_view;

[flutter_view addSubview:platform_view_root];
[flutter_view addSubview:overlay];

active_composition_order_.push_back(view_id);
}
composition_order_.clear();
return did_submit;
}

几个关键变量先解释一下:

  1. flutter_view 我们所有的flutter UI在iOS侧都是由FlutterViewController承载,这个flutter_view就是FlutterViewController的根view
  2. composition_order_ 这里面是根据layerTree的结构,构造的一个viewId的数组。可以理解数组里面靠前的viewID会更靠近底层一些。
  3. platform_view_root 这里可以简单理解为是factory创建的native view对象。(实际上flutter可能会在这个view上包一些ChildClippingView
  4. overlay 这个overlay是解决native view与flutter其他widget遮挡关系的关键,后面我们详细介绍。

然后有2个关键逻辑:

  1. platform_view_root是直接作为subview添加到flutter_view上面的,这说明真正的platform_view是由原生来渲染的。并没有什么先渲染到纹理,然后再转给flutter当做图片渲染的逻辑。这部分纯渲染的性能是足够可靠的。
  2. 每一个platform_view_root后面都跟了一个overlay,这个overlay才是提供给flutter侧做widget渲染的skia环境。在platform_view上层的flutter widget,可能就会被渲染到这个platform_view上面的overlay view上;而platform_view底层的flutter widge,可能会被渲染到更底层的overlay view或者flutter_view(根view)的skia环境上。

大部分性能问题,应该就出在这个overlay view上面。

flutter Demo的层级结构查看

上面代码分析了一大堆,我们直接看下xcode view的层级结构:

image-20191128150134035

这里层级结构比较简单,由于我们嵌入了一个QMapView(这个是native view的类型)。flutter自动添加了一个FlutterOverlayView,然后其他所有的flutter widget都被绘制到这个overlay view上面去了,由于直接是绘制到gl layer上的,所以xcode是看不到flutter widget的层级的。

Ps. 如果所有的view都是由flutter widget绘制的,并没有插入platform view的话,我们会发现所有的UI是直接绘制到FlutterView上的。这里就不贴图了,有兴趣的小伙伴可以自己去看一下。

多个platform view嵌套的场景

Untitled

整个widget的结构和对应的UI层级结构是这样的:

Stack

– Container(red)

– Button(定位)

– mapview0

– Container(yellow)

– mapview1

ListView

对应右边的UI层级结构图,我们可以看到Stack最底层的Red container,被绘制到FlutterView上了,其会被其他的mapview遮挡。

位于2个mapview中间的yellow container,是绘制到第mapview0的overlay上面的,这样黄色的container widget就能遮挡住mapview0,而被mapview1遮挡住。

总结

通过查看代码,我们可以了解到,Platform View是直接通过渲染一个native view的方式实现的,这部分并不是性能问题的根源。

为了解决platform view和普通flutter view的遮挡关系,每个platform view实例的引入,flutter都会对应引入一个全屏的、可以用skia渲染组件的overlay view。这个overlay view就是影响flutter app性能的关键点。

具体分析我们可以查看性能篇