通过上一篇对engine代码的分析,我们了解到每展示一个platform view,flutter都会同时创建一个全屏的overlay view。这个overlay view的作用,是解决platform view和flutter view遮挡的问题。

而这套实现机制,很明显会带来一些性能问题,我们用一个最简单的demo来验证一下

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation UIViewPluginController
{
UIView *_view;
}

- (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args registrar:(NSObject<FlutterPluginRegistrar> *)registrar
{
if (self = [super init]) {
_view = [UIView new];
_view.backgroundColor = [UIColor redColor];
}
return self;
}

- (nonnull UIView *)view {
return _view;
}

@end

demo主体是一个listview,然后每个list item上面我们放一个platform view,这个platform view对应的native实现就是上面的红色背景的UIView。

然后我们通过每添加一个list item去打点一下内存的方式,分析一下内存增量;同时通过flutter的performance overlay工具和instrument的time profile工具来分析一下滑动列表时的性能情况。

具体demo代码地址可以查看这里platform_view_profile

内存

首先是对内存的影响,flutter engine添加的overlay view并不是一个简单的空白UIView。其包含一套完整的OpenGL渲染环境的,是作为skia的一个全屏render target存在的。

官方文档的addPlatformView介绍有这么一段说明

Adding an additional surface doubles the amount of graphics memory directly used by Flutter for output buffers. Quartz might allocated extra buffers for compositing the Flutter surfaces and the platform view.

大体意思是,会double一份graphics memory作为输出buffer。除此之外,为了合成flutter和platform的图层,Quartz还可能创建额外buffer空间。

下面用instrument来跑一下allocation看看:

左图主体部分是展示了一个listview,每个item上都有一个Text widget 和上面说的UIView(platform view)。

这里的操作路径是每添加一个UIView就打点一次,从后面几次内存增量来看,每添加一个红色的UIView,会有45M的内存增量,具体我们再分析一下堆栈:

ps. instrument导入engine的dsym折腾了老久,得同时设置二进制文件和符号表文件才行,名称要对应上

从上图可以看到45M内存全是由FlutterPlatformViewsController::SubmitFrame创建的,在原理篇我们介绍过,这个FlutterPlatformViewsController正是处理PlatformViewLayer绘制用的。

我们再看下相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool FlutterPlatformViewsController::SubmitFrame(bool gl_rendering,
GrContext* gr_context,
std::shared_ptr<IOSGLContext> gl_context) {
bool did_submit = true;
for (size_t i = 0; i < composition_order_.size(); i++) {
int64_t view_id = composition_order_[i];

// 1. 对应上面的10.78M内存,会调用createSecondaryGPUSurface
EnsureGLOverlayInitialized(view_id, gl_context, gr_context);

// 2. 对应上面的12.98M内存
auto frame = overlays_[view_id]->surface->AcquireFrame(frame_size_);
SkCanvas* canvas = frame->SkiaCanvas();
canvas->drawPicture(picture_recorders_[view_id]->finishRecordingAsPicture());
canvas->flush();

// 3. 对应上面的21.57M内存
did_submit &= frame->Submit();
}
}

具体3块内存我们再详细分析一下:

  1. EnsureGLOverlayInitialized 10.78M

    这里就是原理篇overlay view创建的地方,具体代码我就不贴了,这里除了创建一个overlay view之外,还对应创建了一个gl 的framebuffer,buffer的size是全屏的大小,这个framebuffer是和overlay view的CALayer关联的,渲染到这个buffer的内容就能够直接展示出来。

    拿这里的测试机型iphone 8p为例,大概的内存占用为2208 x 1242 x 4bytes / 1024 / 1024 = 10.46M,与instrument跑出来的结果类似

  2. Canvas->flush 12.98M

    这一步的内存消耗主要是创建了一个新的离屏的buffer。根据flutter的注释来看,这里是考虑到离屏的bufer,在读取速度和拷贝速度上更快一些,而一些滤镜效果是必须需要先copy一下的,所以为了提升这个copy的性能,会先绘制flutter的元素到这个离屏buffer上,最后再copy到第1步的gl framebuffer。

    bool IOSSurfaceGL::UseOffscreenSurface() const {

    ​ // The onscreen surface wraps a GL renderbuffer, which is extremely slow to read.

    ​ // Certain filter effects require making a copy of the current destination, so we

    ​ // always render to an offscreen surface, which will be much quicker to read/copy.

    ​ return true;

    }

  3. SurfaceFrame::Submit 21.57M

    1
    2
    3
    4
    5
    6
    7
    8
    if (offscreen_surface_ != nullptr) {
    SkPaint paint;
    SkCanvas* onscreen_canvas = onscreen_surface_->getCanvas();
    onscreen_canvas->clear(SK_ColorTRANSPARENT);
    onscreen_canvas->drawImage(offscreen_surface_->makeImageSnapshot(), 0, 0,
    &paint);
    onscreen_surface_->getCanvas()->flush(); // 这个地方占了21M内存?? 为啥呢
    }

    这个submit主要做的操作,就是把第2步绘制到离屏buffer的内容,先通过makeImageSnapshot拷贝出来,然后再渲染到第1步创建的gl framebuffer上,整个过程消耗了21.57M的内存。

内存问题优化思路

  1. 多个platform view引入的多个overlay view,可能并没有绘制flutter的组件,这部分空白的overlay view可以考虑干掉,节约内存占用

  2. 双缓冲区渲染确实是操作系统通常会采用的渲染方案,前缓冲区用来GPU去做展示,后缓冲区用来同步写下一帧的数据,提高性能。但是放到这里我对其能带来的实际体验效果提升还是比较存疑的,而问题更大的是第3步缓冲区交换的步骤居然额外引入了21M的内存消耗,感觉是有问题的。

    操作系统的双缓存区应该是不存在2个缓存区内容拷贝的过程,而是直接切换缓存区展示,其性能才会比较高。

帧率

帧率这块,首先看看flutter提供的performance overlay工具,在添加8个UIView的情况下,性能表现:

可以看到:

  • UI线程基本每帧耗时特别少,平均为2.3ms左右。因为我们dart侧几乎没有逻辑,只有一个简单的listview,滑动时也没有做任何的操作
  • GPU线程可以看到耗时是比较高的,滑动过程中经常会有红色的矩形出现,表示当前帧耗时超过了16.6ms,发生了丢帧。

这里overlay工具不是特别直观,我在log里面打印了一下平均fps。统计方式是1s内渲染的帧数,计数是通过window的onReportTimings来计数,具体代码可以查看demo。

从输出的平均FPS数据来看,有8个platform view的情况下,有明显的掉帧,只有40-45帧左右。而去除platform view之后,则能够稳定到60fps。

具体耗时的地方,我们用instrument看看timeprofile

这里主要耗时有2个线程:

  1. 一个是iOS的主线程(从FlutterEngine代码来看,flutter的platform runner和gpu runner实际都是iOS的主线程)

    主线程最耗时的函数,正是内存部分我们分析的,绘制flutter内容到离屏buffer,然后copy到gl framebuffer的2个接口。所以这部分逻辑,不仅耗费内存巨大,对CPU的性能消耗也是比较大的。

  1. 另外一个线程应该对应的是Flutter的UI runner,从前边performance overlay工具的结论看,并不是性能的瓶颈,这里略去。