作者:易旭昕 本文由作者授权发布。
这篇文章主要是对在原生长列表中嵌入多个 Flutter 卡片,每个卡片都对应一个独立的 FlutterView/Engine 这种使用场景进行调研,分析该场景下的性能和内存使用等指标。通过调研,我们希望了解这种使用场景下 Flutter 的性能表现如何,在实际的业务中是否可行。
主要调研的指标包括三方面:
心急的同学可以直接跳到最后结论的部分。
为了进行调研,我们编写了一个 Android Demo,Demo 在 Android Native 端使用了 androidx 提供的 RecyclerView 实现长列表。RecyclerView 会自动创建多个卡片并循环使用,在 Demo 中,每个卡片都是一个 FlutterCard 对象,其中包含一个独立 FlutterView 和 FlutterEngine,卡片的内容由 Flutter 呈现。
测试手机使用了 Google Pixel,在现在来说算是性能比较差了,可以更好地反映实际的状况。
FlutterCard
可能是因为压缩的原因,视频显示不如实际表现流畅
除了初始滚动时,可能因为集中创建和初始化 FlutterEngine 导致主线略微阻塞,会有轻微掉帧的现象外,整个滚动过程都非常流畅。在惯性滚动中,卡片会不断地被回收和重用,所以 Surface 的 Destroy 和 Create 会频繁地被触发,在应用主线程,也就是 Flutter.platform 线程触发 Surface Destroy 和 Create,主线程需要阻塞等待 Flutter 完成清理或者初始化的操作,如果它造成明显阻塞就很容易导致掉帧。
在 Android 平台上,PlatformViewAndroid::NotifyDestroyed 主要工作:
PlatformViewAndroid::NotifyCreated 主要工作:
通过分析发现,在对比开启和关闭我们的引擎优化的情况下:
可以看到,在开启引擎优化后,Surface Destroy 和 Create 的耗时都很少,绝大部分情况下都不会导致掉帧。
在 Demo 的场景中,RecyclerView 在惯性滚动时,将新的卡片从不可见区域移进可见区域,触发了 TextureView 的绘制,而 TextureView 的 Surface Available(Create)是在它第一次被绘制的时候触发。
RecyclerView 会提前一些将卡片加入 View 树参与布局
按照原生的逻辑,Flutter 需要在 Surface Create 时才触发 ScheduleFrame。如果当前帧是第 N 帧,在第 N 帧的 Draw 的过程中触发了 TextureView 的 Surface Available(Create),同时触发了 Flutter 的 ScheduleFrame,Flutter 要等到 N + 1 帧的 VSync 回调时才触发 BeginFrame 开始绘制,如果 Flutter 首帧的布局 + 光栅化耗时少于一个 VSync 周期,那 Flutter 的首帧可以在 Native UI 第 N + 2 帧输出。
也就是说即使卡片的 Widget 树很简单,或者设备的性能非常高,Flutter 卡片最少也有两帧的空白时间,实际空白持续的帧数跟设备的性能,Widget 树的复杂程度都有关系。从 Demo 在 Pixel 上运行的情况来看,因为卡片比较简单,大部分情况下都是两帧空白。
如果仅仅只是两帧的空白,考虑到卡片本身只是一部分可见,设置卡片的 Flutter Widget 背景色跟原生 View 保持一致,或者干脆 Flutter Widget 不绘制背景,完全透明(需要使用 TextureView),这样一般情况下也不太容易察觉。
另外,因为 Flutter 的图片是异步加载和解码,所以图片如果太大,图片的绘制相比其它 Widget 可能会有更明显的延迟。
相关的 Android 渲染流水线帧调度的分析,可以参考我的文章TextureView 的血与泪
为了排除图片解码缓存内存管理的干扰,我们专门测试了无图和有图两种情况,并且增加了开启引擎优化和关闭引擎优化的对比。我们加入了只有一个 FlutterView/Engine 的无图简单 Demo 作为对比参考(使用 SurfaceView,大小只有窗口的一半),另外也加入了一个纯原生无图的长列表 Demo 作为对比参考(卡片内容不完全一致,仅供参考)。
内存占用通过 meminfo 查看,主要看 PSS,PSS 虽然不能完全代表真实的物理内存占用,不过用于对比增量还是有一定参考价值的。实际操作中会滚动到底部之后再滚动回头部,长列表设置显示 200 张卡片,在这个过程中 RecyclerView 一共创建了 9 个 FlutterCard 对象,也就是 9 对 FlutterView/Engine 循环使用。
我们首先对比单引擎的简单 Demo 和完全原生的应用,主要增加的部分在:
所以一个单引擎全屏简单的 Flutter App 对比纯原生也会带来 40 ~ 50m 左右的额外开销。
再对比多引擎同时运行多个 Flutter App 的情况:
从上面的对比,如果在可见的 FlutterView 面积一样的情况下,并且开启引擎优化,9 个引擎运行 9 个比较简单的 Flutter App 对比只有一个引擎运行一个 Flutter App 大约增加了 40 ~ 50m 左右的额外开销。如果没有开启引擎优化,我们会看到大量额外的线程和 GL 上下文会导致 Native Heap 和 GL mtrack 大幅增加,总共增加了 68m。
开启有图之后,我们可以看到 Gfx Dev 大幅增加 348m,主要来自于图片解码后上传的纹理。Unknown 部分也有一定幅度增加,猜测主要来自于图片原始数据的内存缓存。这里面最主要的问题是 Engine 在循环使用的过程中,会一直累积图片纹理缓存不会主动释放,并且每个 Engine 独立管理纹理缓存,缺少全局管控。