招贤纳士
我们急切需要浏览器渲染引擎/Flutter 渲染引擎的人才,欢迎大牛们加入我们。
前言
1 月 16 日,UC 技术委员会联合掘金、谷歌开发者社区举办了 2021 年首届 Flutter 引擎为主题的技术沙龙活动。活动吸引了 150 多名同学报名,由于疫情控制现场人数的影响,最终我们只能安排 50 名同学来到现场。另外,有 2000 余名同学围观了现场直播。活动中,来自阿里巴巴集团内的五位技术专家与大家分享了基于 Flutter 构建的研发体系,开发与优化经验,动态化方案,以及 UC 定制的 Flutter 增强引擎 Hummer 的优势与新特性。
第一场分享是由 UC/夸克客户端负责人、UC 移动技术中台负责人辉鸿带来的《打造基于 Flutter 的 UC 移动技术中台》。
第二场分享是由 UC Flutter Hummer 引擎技术负责人佬龙带来的《Hummer (Flutter 定制引擎)优化及体系化建设探索》。本文约 8400 字,38 张图片,4 个视频,整体阅读时间约为 45 分钟。
分享内容
大家下午好,我是 UC 内核团队的佬龙。刚才大辉也说到了,其实 Flutter 并没有那么完美,它有很多的问题,一开始可能都是业务在摸爬滚打,去尝试解决这些问题,但有些问题在业务层面是很难解决的,所以就需要一个引擎团队来去支撑这个事情。我们 UC 内核团队在去年的时候开始参与到整个引擎的优化里面,也有一些初步的成果,所以今天我主要是在这里给大家分享一下我们的一些进展,还有一些成果,也希望给大家提供一些思路做参考。
今天主要会分为 4 个方面去做介绍:第一部分我们来简单地回顾一下整个 Flutter 的技术特点以及它的现状;第二部分我们来整体地看一下 Hummer 的技术架构;第三部分是今天的主要内容,会对我们的一些核心优化点做一个展开的介绍;第四部分会做一个总结,以及同步一下我们未来的一些规划。
首先是第一部分的内容,这个是官网的一个介绍,简单来说 Flutter 就是一个 UI Toolkit,它最大的特点就是跨平台。
它有几个核心的理念,第一个就是美观,这里体现在两个方面,一个是说它整个 Widget 的设计是非常漂亮的,然后跨平台一致性非常好,这主要也是得益于它自绘渲染的这种实现方式,可以做到像素级的控制;
第二个特点就是流畅,这里主要是得益于两个方面,第一个方面是因为业务是用 Dart 代码去写的,它可以做 AOT 编译,所以整个运行性能会比 js 要高;第二个方面渲染管线比较简短高效,这主要是相对于 React Native 这样的一些技术,因为它的渲染管线可能会更加地长一些;
第三点就是高效,这主要体现在开发上面,刚才大辉也说到,Flutter 它支持这种 HotReload 的机制,它的开发非常高效;
最后一点我觉得也是非常重要的一点,就是 Flutter 它是一个非常开放的项目,它不仅开源,而且它的整个开源社区的运作也是非常地好,社区的响应非常地快,你去提什么问题,官方工作人员都会很快回复你,所以我觉得这一点也是做得非常地好。
这里并没有提到跨平台,因为跨平台就是 Flutter 最基础的一个属性,整个 Flutter 引擎的演进思路也是围绕着这几个点去做的。
接下来我们来看一下时间版本线,我们从这个版本线主要看到三个点:第一个就是 Flutter 它是一个非常年轻的引擎,它的发展历史是比较短的,它在 18 年底才发布第一个正式版本;第二个点就是它的 add-to-app 的能力,也就是混合开发能力是在 19 年底才加入的,我们在后面的使用过程中也发现,add-to-app 的使用体验其实是比较差的;第三个点就是整个 Flutter 的版本迭代速度是非常快的,这幅图没有画出所有的版本,但是我们可以看到大概 2~3 个月,它就会更新迭代一个新的版本,这样也给我们引擎升级带来一些挑战,我们需要能够保持这种快速迭代的能力。
接下来我们看一下整个社区的发展,虽然说 Flutter 的历史比较短,但它的社区发展是非常快的。从去年 10 月份的官方统计数据来看,Google Play 上的应用数以及开发者数量都已经达到一个非常庞大的水平。
国内的很多厂商也在或多或少地使用 Flutter 技术,包括我们内部其实也有像 UC、夸克、还有咸鱼都是在非常高强度地去使用 Flutter 技术。
随着这些业务使用的场景越来越深入,越来越复杂之后,我们就会发现 Flutter 其实它并没有那么完美,它存在的问题还是比较多的,包括像内存问题、性能问题,以及像包 Size 过大、没有动态化能力,这样的一些问题阻碍了业务的发展,也阻碍了整个业务对 Flutter 的深入使用。
总结下来现在 Flutter 最大的痛点,从我们这边看到主要是这 6 个点。
第一个就是它缺乏动态化的能力;
第二个点就是它的性能在一些场景下,或者在一些复杂场景其实是不够的;
另外它整个内存消耗偏大,特别是在图片比较多的场景;
第四个点就是包 Size,你用 Flutter 去开发,相对于用 native 去开发,size 会更大一点,这个可能对一些 APP 来说也是一个痛点,因为现在很多 APP 也在主打下沉市场;
还有另外两个点,比如刚才提到混合栈的开发能力比较弱,然后由于它整个历史发展比较短,整个基建其实也是不够完善的。
为了解决这些问题,业务上可能有三种选择,第一种就是从业务去优化,这可能是初始阶段,也是比较痛苦的;第二个选择就是等待官方更新,但这个其实是跟不上业务节奏的,因为官方没办法那么快速地去支持业务;所以我们选择了第三条路,去做引擎的定制和优化,我们的引擎就叫做 Hummer。
首先我们来看一下 Hummer 的整体架构图,Hummer 引擎就是左下角这一部分,它主要包括 Framework 和 Engine,还有像第三方库这样的一些代码。在 Hummer 的右边是我们配套的工具平台,这里主要包括三块:线上监控平台、构建平台以及开发过程中的一些支撑的工具平台。在研发支撑平台这一块,我们目前主要在做的是优化海鸥实验室以及 DevTools。
在 Hummer 之上主要就是它的插件层,除了支持官方通用插件之外,我们也会去定制一些插件,然后与 Hummer 去做融合,比如说我们的 Aion,支持动态化能力的一个插件。
在最上面就是我们的接入业务,目前我们除了服务 UC 内部以外,也服务了集团的一些客户端,比如说像淘花这样的一些 APP。
我们再回过头来看一下左下角这部分,蓝色方块部分都是我们相对官方引擎的一些优化点,这里还是比较多的,总结下来主要有三个核心点:第一个点就是我们做了内存的优化,以及整个工具平台的建设;第二个点就是我们做了性能的全方位梳理以及优化;第三个点就是我们做了引擎能力的增强,使得 Flutter 可以提供更多的业务能力支撑。接下来我主要会围绕这三个核心点去做一个展开。
首先我们来看内存这一块的一些优化。先看看我们在内存这一块遇到一些挑战,这里主要分为4个层次:
- 从业务这边来看,在多图片的场景下,内存的压力会变得非常的大,因为 Flutter 图片的释放,它需要依赖于 Dart 的 GC 机制,它可能会因为业务写的不好,出现泄露,或者是说 GC 不及时,导致很大的一个内存峰值;
- 从整个 Flutter 的内存管理来看,它缺乏一个中央的内存管理机制,这种缺乏中央管理机制的设计,在多 Engine 的场景下内存问题就会变得非常突出,比如说你使用多个 FlutterView,它有多个 FlutterEngine,这时候它的图片缓存也是各自控制,没办法统一管理,会容易出现内存峰值;
- 从架构层面看,Flutter 的整个进程模型还是比较简单的,它采用的是单进程架构,在 32 位的机器上容易出现虚拟内存不足,然后导致崩溃问题;
- 工具层面目前是比较缺乏的,如果出现问题要去分析,也是比较困难的。
我们内存优化主要是从这 5 个方面入手去做一些工作,这里做了一个全链路的优化,包括从引擎到业务到整个工具平台。引擎这边我们做了挺多优化点,总结下来有两个方面:第一是我们对图片内存管控做了比较多的优化工作;第二块针对多引擎的场景我们也做了一些优化。
业务这边主要是做一些核心对象的泄露监控,以及解决内存泄露问题。
中间这两部分的平台主要是面向于本地开发过程中的工具平台,对于这两个平台我们主要的核心思路有两点:第一点是我们希望通过这些工具平台,提供给业务快速发现内存问题的手段;第二我们也希望通过这个平台让业务分析这些问题更加地简单高效。
最后一块是我们的 iTrace,也就是线上监控平台,这里我们主要做的事情是完善崩溃日志,补充一些内存分组信息,帮助我们去更高效地分析线上的 OOM 问题。
我们先看一下内存优化的效果,这是单引擎场景下,通过我们跟业务一起优化,整个崩溃率下降了 67% 左右。然后单引擎场景的内存占用相对原生引擎也可以节省 10~20%,但是节省内存并不是我们的主要目的,因为内存跟性能是一对矛盾体,我们更希望可以去控制内存的峰值,让它在尽量使用多内存的情况下,又不出现 OOM 崩溃问题。
我们对内存峰值控制做了挺多的优化工作,可以看到它的一个效果。右边的这两幅图,上面是原生内存曲线,它整个内存峰值在这种多图的场景很容易飙到很高,达到了差不多 1G 这样的一个内存占用,优化后的版本可以比较好地稳定控制在我们想要的一个内存水位下。
我们再来看一下多引擎的优化效果,这里我们主要做了三个优化点:第一个点是图片的中央内存控制,
第二是共享 GLContext 和 GrContext,第三个点是共享 GPU 和 IO 线程。这些点我们做的是比较早的,近期官方也推出了这一块的优化计划,发现官方想要做的事情跟我们做的这些事情思路非常吻合,他们也会去做共享 GLContext、共享 GrContext、共享线程这些事情。做了这些优化之后,在这种多图文场景下,整个效果是非常明显的,就像右边这个视频,它的每个 Item 都是 1 个 FlutterView,这里有 9 个 FlutterView 循环使用。我们可以看到在这种复杂的图文场景下,多 Engine 的情况,我们的内存占用比原生减少 40%~70%。
接下来就针对几个优化点做一个展开的介绍。首先是我们的中央图片纹理缓存,我们看一下这个简单示意图,Flutter 的图片解码,是发生在图片加载之后的,图片加载完就会去做解码,然后它的 Build 和 Layout 的流程是需要去等待图片完全解码之后才能进行的。这样就会有两个问题,第一个是 Layout 可能会受到阻塞,会变慢;第二个问题是它的图片纹理缓存管理是在 Dart 那边的,这样的话他需要依赖 Dart 的 GC 机制,然后多 isolate 之间也没办法共享这些缓存,没办法去做统一的管理。
我们做了图片中央纹理缓存优化之后,图片解码是放到光栅化的步骤去做的,在图片加载完成之后,我们只需要去解它的图片头,得到一个宽高信息,然后就可以用于排版,这样的话就可以加速它整个排版流程,另外的话在光栅化的阶段去做解码,就可以做到图片解码数据管理跟 Widget 是解耦的,可以在 C++ 去做一个统一的内存管控。
这样的话在多 isolate 的场景下,我们也可以去统一的管理,使得它的内存峰值可以得到一个比较好的控制。
我们再来看一下我们在 DevTools 上做的优化,这里我们做了一个叫 DMA 的面板,这个面板可以配合 Framework 层提供的一些接口去使用,Framework 提供了一个 LeakAdd 和 LeakCheck 的接口,让业务可以通过这些接口去监控一些对象,然后当它们发生泄漏的时候,就可以把对象的信息上报到 Devtools 上面。可以看到这边有一个对象名,然后这边是它的一个引用路径,我们针对引用路径还做了一个智能化的分析,会去算出它的最短引用路径,这样的话就可以更好地帮助业务去快速地去分析这些内存泄露问题。
我们的本地自动化测试平台——海鸥实验室,这里我们做了内存的自动化测试,包括了可以去自动测试整个内存的变化,然后把这些内存去分模块地展现出来。在测试过程中,会跟我们的内存异常检查功能去结合,当测试过程中发现了一些内存异常,它可以在测试报告里面体现出来,然后点击报告这个地方,就可以看到整个异常的详情,去分析内存异常。所以我们海鸥实验室做到了自动测试,以及辅助分析内存异常问题。
线上这一块主要是补充了一些日志,包括了一些内存分组信息。我们这里把整个 Flutter 内存分了很多模块,非常地详细,这样的话如果出现一些 OOM 崩溃问题,就可以更好地定位大概是哪个模块引起的,从而可以去更有效地去分析这些 OOM 问题。
说完内存相关的一些优化,我们再来看一下我们在性能上所做的一些事情,主要包括 5 个方面:
第一个方面就是我们对惯性滚动流畅度做了一个全方位的梳理和优化,这一块后面我会重点展开介绍一下;
第二点就是我们对首帧和启动也做了一些优化,现在的效果非常好,可以减少 60% 的首帧时间消耗;
第三点我们也利用了 LLVM 后端去优化 Dart 的代码生成性能,通过我们引入 LLVM 编译器后端,整个 DartVM 代码的性能可以再提升 30% 以上;
最后两点是两个特殊场景的优化,一个是动图,一个是混合渲染场景。
我们看一下动图和混合渲染这两个场景的效果,左边是动图,优化后的帧率有明显的提升;右边是混合渲染的优化效果,像这块三角形区域以及这些彩色球的区域,优化后效果也是更加流畅。
接下来会介绍我们在惯性滚动流畅度上所做的一些优化。首先我们先来大概了解一下整个 Flutter 的惯性滚动帧渲染流程,这里简单来说分为三个步骤,第一个就是用户事件的响应,UI 线程收到用户事件之后,它就会去计算滚动速度和滚动距离,然后它检测到需要做滚动的时候,就会去注册一个 Vsync 的 Callback,然后等待系统的 Vsync 信号过来,Vsync 过来之后,它就会通过 Callback 回调到 UI 线程这边,然后去做帧生产的动作。帧生产这里主要包括了 4 个流程,第一个是Animate,还有Build、Layout、Paint。 Animate 主要是计算动画数据,然后得到偏移,Build 就是创建这些节点,然后做 Layout、Paint。Paint 这里会生成一些绘制指令,它们会保存到 Scene 的对象里面,发到 raster 线程这边,然后再去做一个渲染上屏的动作,这就是 Draw 的动作。这里的帧生产和 Draw 的流程,它是同步的,这两步加起来要在16毫秒以内,这样才能避免掉帧。
我们从帧渲染的流程里面,可以看到三个问题点,第一个是 Vsync 的信号注册是比较绕的,而且它需要到 Platform 线程那边去注册一个 callback 回调,这就可能会因为 platform 线程比较繁忙,而导致整个 Vsync 的调度不及时;
第二个问题就是 Draw 的操作,它需要等待前面帧生产的流程,它是同步的。假设说前面的 Build 或者 Layout 的过程比较复杂,比较耗时,就非常容易出现掉帧的问题;
第三个点是 Draw 的操作,包括了 CPU 和 GPU 的操作,这里全部的操作都放到一条线程去做,它的并发度不够好,性能可能不够高。
看完了它的原理分析,我们再看一下实际业务的 trace。这个是我们在幽默业务上抓的一个 trace,我们发现 Flutter 的惯性滚动有一个特点:帧率其实并不低,但是在滑动过程中容易出现某一帧卡得比较厉害的情况。这个主要跟它 ListView 的实现是有很大关系的,ListView 在滚动过程中,会去检测它的缓冲区域有没有节点,没有的话它就会去做一个节点插入的动作,插入程中它就会触发节点的 Build 和 Layout,如果这个节点比较复杂,就容易说出现这种掉帧的情况。
所以说 Flutter 的惯性滚动流畅度,主要的挑战还是在于如何去减少这种严重的卡帧情况。
这里我们做了一些优化的探索,目前主要包括这 6 个方面。第一就是我们做了一个独立的 GPU线程,然后我们针对字体排版也做一些优化,以及像 Sliver 分帧渲染。
接下来主要会针对这三个点去展开做一个介绍。
首先是 GPU 的独立进程,这个原理说起来还是比较简单,原来 Draw 全部在 raster 线程去做,但现在我们把 Draw 的操作里面 GPU 的部分放到了另外一条独立的 GPU 线程去做,这样的话就可以去减少 raster 线程的压力,可以做到并发,去提升它的性能。
整个方案的改动还是非常地大,也遇到了一些不少问题,这里不再展开。
第二点就是我们做了字体的 Layout 并行优化,我们发现 Flutter 字体 Layout 耗时是比较长的,我们看这个 trace,这里一个只有几百个中文字体的 TextView,Layout 耗时就要达到了 100 毫秒以上(profile 模式)。这里我们做了一个优化,我们发现字体它在 Layout 过程中,会去计算两部分的信息,一部分是需要用在 Layout 过程中的,比如说它整个文本段的一个宽高信息,还有一些单个字体字型信息,其实并不是在 Layout 的过程去使用,而是在 Paint 阶段才去使用,我们就把这部分 Paint 才需要的信息放到了另外一条线程去并行计算,这样的话就可以加速它 Layout 的流程,使得它的时间可以缩短 30%~40% 以上。
我们再来看 Sliver 的分帧渲染。Flutter 滚动一般都是用 ListView 或者 GridView 这样的方式去实现,这里有几个概念,一个是 Viewport,表示用户可以看到的这部分区域,还有两个 Cache Extent,上下各有一个缓冲区,它的节点是承载在 SliverList 上面的。
当我们去滚动的时候,SliverList 有部分节点滚出 Cache 区域,当它发现下面的这部分缓冲区没有内容了,这时候就会马上补充节点上去,比如这里缺两个,他就补两个进去。如果这两个节点比较复杂的话,就很容易导致这一帧的卡顿,而且会出现一个非常大的卡顿。我们的分帧渲染主要思路是希望能把一帧的大卡顿,可以平摊到多帧里面去,使得它不容易出现这种很大的卡顿,尽量让帧率更加平滑。
比如这个场景第一帧滚出去之后,这时候会补充一个节点,当我们发现这个节点插入已经非常耗时,就会停止插入另外一个节点,下一个节点会等到下一帧才会去做插入,这样就可以避免出现一帧非常卡的情况。
我们首屏和启动相关一些优化,这里主要有 6 个点。这一块在我以前的一些分享里面有比较详细的介绍,今天就不展开了。整个的启动的耗时和首帧耗时优化之后也可以提升多倍,这一块我们很多优化点也已经提交回社区,大家如果去使用最新的官方引擎,也有部分优化是可以体验到的。
第三点再介绍一下我们基于 LLVM 的 Dart 编译器优化,这里我们先简单了解一下 Dart 代码的编译流程,它的编译流水线简单来说分为这几个步骤:Dart 源码先经过一个通用的前端编译器,生成 Kernel 二进制文件,然后它会经过类型流分析,优化掉一些没用的方法,生成一些叫 IL 的中间语言,然后 IL 经过汇编器的汇编,再链接生成最终的目标代码。
这个优化我们主要是切断了后面的流程,然后加入一个 LLVM 编译器后端,将 IL 指令转换成 LLVM 编译后端的通用输入 IR 指令,然后输入之后利用 LLVM 强大的代码优化能力,使得整个代码产物的性能可以进一步地提高。Dart 代码的性能其实已经非常高了,经过我们的优化之后,它可以再提升 30% 以上,可以基本达到像 C++ 一样的性能。
最后一块再说一下我们在引擎能力上做的一些增强。这里主要也有几块,比如说我们做一个外接网络库,支持 Android 厂商第三方定制字体,然后也做了一个自适应 DarkMode 的能力,可以让业务开发者通过一个 Widget,一键切换日间和夜间模式;还有我们也支持了像 hevc/heif 这样的图片格式;单引擎复用官方的支持是比较差的,这里我们也做了一些支持。接下来会介绍我们前面两个点。
第一个是我们的外接网络库, Flutter 网络库主要的能力是通过插件去提供的,业务方一般都使用像 DIO 或者 HTTP 这样的一些网络库插件,去使用网络的能力。这些网络库插件它是通过 HttpClient,然后一直对接到下面的实现层,最终调到网络库这边。我们做了一个新的插件叫 uNet,uNet 主要就是去实现 HttpClient 的相关接口,然后做一个桥接到我们外接网络库上面去,这样的话业务就可以在不需要更改代码情况下,通过 DIO 或者 HTTP 这样的插件使用我们的网络库,从而享受到我们外接网络部的一些能力。
外接网络库它主要有几个好处,第一个是它的网络协议支持会更加好,比如说它对 h2、h3 的支持会更好,然后性能也会更高,我们的线上数据,性能可以提升 30% 以上,然后错误率也会大幅降低。
最后一个功能点是我们的第三方字体支持,大家使用 Android 手机可能都知道,国内很多厂商它都会提供让用户去下载字体,然后替换系统字体的能力。
但这种第三方字体 Flutter 内部它是不支持的,切换系统自定义字体后,Flutter 整个界面的字体还是保持着原来的系统字体,界面一致性看起来比较差。这里我们也去做了这方面的支持,使得 Flutter 内部可以去使用上这种第三方字体,它的整个界面一致性可以更加地好。
说了这么多的点,可能大家会有点乱,这里再来做一个总结。
今天主要讲三方面的内容,第一个是介绍了整个 Flutter 的核心理念,它主要有几个特点:美观、流畅、高效以及开放,跨平台是它一个基础属性,整个官方引擎主要是围绕这几个点去演进的。
Flutter 虽然是一个非常年轻的引擎,但是它整个发展势头非常迅猛,虽然只有两年多的时间,但是开发者的数量已经非常庞大,国内也有很多大厂在使用。
但是随着这些使用场景越来越多,我们发现了很多问题,比如说它的内存和性能以及 size 上都有一些问题,所以我们做了一个定制引擎,叫 Hummer。Hummer 目前主要围绕三个点去做优化:
第一是对内存做优化,以及它相关的一些配套工具的建设;
第二点是我们对整个性能也做了一个全方位的优化;
第三点是增强了引擎的一些能力,使得它可以更好地去支撑业务的一些场景。
最后再来讲一下后面的一些规划,刚才也提到了痛点,包括像动态化能力、包 size、混合栈开发这些痛点,接下来我们会继续围绕这些痛点去展开优化。
比如惯性滚动流畅度优化,虽然刚才说到一些点,但距离我们想要去对标 native 这样的一个目标,其实还有一定的距离,我们接下来也会继续去探索这一块,然后去做更多的一些优化点。
第二块是动态化能力,现在我们跟客户端团队在共建这个 Aion 方案,最后我们有一个同事也会详细地去介绍 Aion 方案,大家等一下可以了解一下。
包 Size 我们之前的投入并不够,随着现在很多 APP 在主打下沉市场,它们对包 Size 的要求又提高了,这一块后续会投入更多人力去做优化。
像刚才说到的混合开发的能力,虽然说现在国内也有不少插件会去支持这块的支持,比如说像 FlutterBoost、还有像 hello 单车的 Thrio 之类的这些插件,它可以去支持这种混合开发,但是目前还存在一些问题,比如说 FlutterBoost 2.0,因为拷贝一些代码,它使得整个升级成本会比较高,然后它的接口使用上面以及内存占用,还有一些设计都不是特别好,会有比较多的 Bug。后面我们也会跟闲鱼团队共建 FlutterBoost 3.0 的方案,这个方案也是会继续采用开源的方式去运作,大概会在今年的第一季度去推出。
最后一块是内存诊断,刚才也说到做了一些优化,但是在整个工具和平台建设方面还不够,现在还没办法做到非常好地发现问题以及快速诊断,这一块我们也会持续地去探索,然后去做完善,希望可以提高效率。
今天的分享就到这里,谢谢大家。
关注公众号请搜索 U4内核技术,即时获取最新的技术动态