字节跳动Flutter包体积缩减接近50%,速来围观!

2,157 阅读11分钟

前言

11 月 23 日,字节跳动技术沙龙【Flutter】技术专场 在北京后山艺术空间圆满结束。这次活动邀请到字节跳动移动平台部 Flutter 架构师袁辉辉,Google Flutter 团队工程师 Justin McCandless,字节跳动移动平台部 Flutter 资深工程师李梦云,以及阿里巴巴高级技术专家王树彬和大家进行Flutter干货分享交流。

本文先来介绍由字节跳动移动平台部 Flutter 架构师袁辉辉 带来的分享,它的主题内容是:《Flutter如何缩减接近50%的包体积》。去掉听不懂的专业术语,按逻辑顺序逐个梳理,选取最核心的部分进行解读,主要让大家体会视频里面的精华内容,学有所得。

演讲作者负责 Flutter Engine 和 Dart Runtime 这两个底层方向上的一些工作。所以这篇演讲还是很有深度的。反正主要介绍了Flutter Engine  Dart Runtime 如何删减,如何优化,最终把Flutter的包体积压缩到到尽可能的小,能有多小就有多小,PPT上面是一大堆C 或C++底层引擎有关的专业术语。可能一般的小伙伴都听得云里雾里。我只能用下面这个表情包表达他们的心情:

包体积现状

包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。这是Google 2016 年公布的研究报告。随着项目的发展,包体积会越来越大,但是随着包体积的增大,用户下载意愿就就慢慢降低,包体积越大,用户下载意愿就越低,所以包体积优化必须提上日程。

今日头条APP的现状:Android端可以动态下发,插件化框架,包体积增量约等于 0,当然其它方式也可以实现,这里就不细致探讨了。iOS端 优化前包体积是 167M,Flutter 产物占 18MB,占比超过了 10%。所以当前重点是针对iOS端的包体积优化。

引入Flutter前,使用OC写,呈现出一个线性增长关系。引入Flutter后对iOS端的包体积的增长的影响,初始增长速度极快,随着代码增多,增长速度逐渐减缓,最终趋近线性增长。

针对每一个环节具体优化细节

下面针对每一个环节优化细节做一个详细的说明。

Dart 编译产物优化

Flutter Release产物分析

为了说明包体积优化,以下是一个覆盖一些业务功能点的Demo打包后的产物分析图:

整个APP由APP Framework和 Flutter Framework两部分组成。

● APP Framework(可变化的):

● App:Dart代码AOT编译产物,它是动态链接库

● flutter_assets: Flutter 静态资源文件夹,包括图片,字体等

● Flutter Framework(固定值):

● Flutter: Flutter Engine,它是动态链接库

● icudll.dat:国际化支持相关数据文件


包体积优化方法

简单地说就是3个字:删、缩、挪。如下图所示:

※ 删:删除无用代码,无用资源。可以手动删除,可以机器删除,也可以编译时删除。Flutter 有一个Tree Shaking 机制,从 Main 方法开始,逐级引用,最终没有被引用的代码,诸如类和函数都会被裁剪掉。这个就是编译时自动删除

**※ 缩:**比如压缩图片资源。

※ 挪:从项目工程或Packages里直接挪到远端,典型是远端下发插件或者安卓里的 App Bundle,虽然“挪”对性能来说是有损的(需要动态下发),但是包体积却大大减少。


如何“挪”?

如何“挪”的步骤如下图所示:

移除非必要产物,动态下发:将 Dart 的编译产物(App)分成两部分:Part1 和 Part2,然后把 Part2 挪出去;然后把 flutter_assets 这个文件夹和icudtl.dat 挪出去。最后包体积就非常小了。


Dart源码编译流程(略)

为啥要略,因为这里涉及到汇编,直接略过。我相信一般朋友们也不容易懂,我就直接跳过了。有兴趣的朋友们可以点击左下角“阅读原文”去看完整版视频。

**【问】:**关于“挪”的那部分,说明一下:为什么Android可以全部挪动 Dart 的编译产物(App),iOS却不可以?

**【答】:**加载到内存后,指令段需要赋予可执行权限,iOS无法随意标记内存可执行,instr段必须在动态库内随包下发。如下图所示:


动态下发方案(重点学习)

挪动过程如下图所示:

Flutter 工程编译安卓的包,会编译成 4 个snapshot文件,分别是:

**※ **isolate_snapshot_instr

**※ **isolate_snapshot_data

**※ **vm_snapshot_instr

**※ **vm_snapshot_data

这里用动态下发模式的示例来举例说明,把isolate_snapshot_datavm_snapshot_data移出来,flutter_assets也移出来。挪动前:9.2M,挪动后3.8M,如果App体积庞大,那么这个收益会更明显。

**【问题1】:**关于iOS32位和64位双架构问题,不可能内置两份 Zip 包。

**解决思路:**将引擎 Zip 包置于 APP 动态库内,App Store 可以针对动态库自动实现分架构下发,如果不知道怎么做,可以参考:Dart 的Observatory Server 的 Web 静态资源,它是整个直接打到 Dart 的运行时里的,这么好的案例,当然可以参考一下。

**【问题2】:**风险应对。比如:下载失败,解压失败怎么办?

**解决思路:**转变思路,不要假设 Flutter 一定可用;提供赢钱是否Ready的Api,业务层做好上层处理。最终实际损失= Flutter 覆盖率乘 x Flutter 功能的渗透率。包体积优化之后是所有用户都收益,而损失的只是少部分用户。如果求稳,就用内置压缩,如果激进一点,就用动态下发。(今日头条团队介绍,他们的的Flutter 覆盖率目前应该可以达到三个 9,因为我们用了内部压缩方案,你也可以使用这个,但是这个动态下发确实也值得研究和尝试一下,哪怕是小范围的测试和试用也行。)

Flutter 引擎编译产物优化

接下来是Flutter.framework这一块的优化。如图8所示:

统一编译参数

在 Flutter 引擎编译时,安卓和 iOS 的编译参数不同,安卓是**-OZ**,iOS 是**-OS**,想追求极致包体积是需要用 OZ 的,不能用 OS。解决:只需要升级最新的 build-tools,改 OS 为 OZ


定制化编译

这块结合各个厂商、各个 APP 可能不一样,有几点可以借鉴的:

(1)移除 boringSSL,可用 Method Channel 调用源生网络库来替代 Dart Http 功能,这样性能绝对有提升,同时还能带来包体积的收益。

(2)定制化编译skia,skia里面的参数很多,其中有 3 个字节跳动团队已经试过了,去掉之后最终得到收益不到 200KB。官方有更高端的概念叫模块化编译,核心思想是把 Engine 拆成不同的Modular,根据自己的情况定制化编译。

机器码指令优化

最后一部分是最高端最偏向于底层的了,看到大佬这般骚操作,简直是惭凫企鹤、望洋兴叹、望尘莫及、高不可攀、难以望其项背、不可同日而语,各位对演讲嘉宾啧啧称羡,简直有一种外慕徙业的想法,但是终究是方案,没看到代码,最后只能是对屠门而大嚼

机器码指令优化如图9所示:

**实验证明:**OC 出来的机器码就比 Dart 厉害一点。比如一个简单是示例,一个函数返回自定义的 View,函数被copy次数,直接影响了包体积大小,Dart和OC编译出来的包体积和其函数被copy次数成线性关系。其中Dart 的斜率远高于 OC。

比如做过一个简单的函数测试,反编译发现,使用OC写的代码,生成了 11 条汇编指令,但是Dart生成了32条汇编指令,Dart 的汇编指令明显比OC多了很多。这也就说明了,为什么Dart和OC生成的包体积斜率不一样的原因

**【解决方案】:**所有函数前 8 个都有指令对齐头,后 6 位都有对齐指令,一头一尾可以移除。中间还有 18 条指令,然后这18条指令中,有 5 条是为了做栈溢出检查(可以考虑移除),OC 没有这个指令,还剩 13 条必要指令,基本与 OC 11 条持平,也存在优化空间。请看下图:

谷歌也在针对这个做优化,有一些已经落地,有些还在推进中。

提问环节

最后就是观众提问环节:

**Q:**谷歌的引擎是一直在迭代的,如果我从现代的版本开始修改引擎,以后谷歌的引擎更新了,我要不要马上跟进?

**A:**这个问题很多人都问过我们,在 Flutter 团队引擎不停迭代时,你的自定义引擎如何跟上节奏?这个问题是我们不需要紧跟潮流,我们挑一个稳定版本,154 做有针对性的优化,过两个月再判断一下,比如现在 191 适合不适合做适配、做迁移,如果适合,那我们就做,如果不适合,或者业务方没有紧急需求,那就不升。为什么有些团队升到 178?因为海外要支持双架构,原来的不够,只能支持单架构,那么 178 默认模式就改成 Library Mode,支持 32 位和 64 位,是为了这个事情。如果你没有这个强烈的需求,反而用自己公司内部的引擎更稳定一点。


**Q:**我是一名移动端研发。我们通过“挪”的方式使包体积变小了,但是用户在使用实际模块当中又要挪回来,我想问的是用户在使用某个模块时,把我们挪出去的这部分挪回来的时候,这个转场我们应该怎么去处理?或者有什么更好的方案让用户无感知的加载我们挪出去的这部分东西?

**A:**还是跟刚才的问题一样。这个问题对于字节跳动的 Android 研发来讲还挺司空见惯的,一个功能挪出去以后,构成插件以后,也会面临你这个相同的问题。这很简单,就判断一下插件是否存在,iOS 也要判断引擎是否存在,要么是否展示接口、是否展示功能入口。你如果一定要在启动阶段首页展示这个功能的话,那你就只能阻塞一下了。


**Q:**您提到有一个字节码优化的问题,刚才讲到 Dart 语言应该是运行在虚拟机上的,这个字节码优化是优化编译的中间语言?还是由 Dart 虚拟机最终生成?

**A:**不,它是机器,在 Release 模式下运行的是机器码。在编译器由 Dart 虚拟机生成的,但是实际运行的时候它是一个完完全全的机器码。


**Q:**关于刚才提到字节码指令的问题,Dart 针对你举的例子里,同一条 OC 是 11 条,Dart 是 32 条,这种情况对于我们来说,我们自己没办法做这个优化,但是实际过程中要不要尽量减少小函数,这样是不是也是一种方式?

**A:**实际使用中应该不会写到像我今天展示这么小的函数,应该不会直接打出一个 Int,实际使用的函数远比这个要复杂,在实际使用过程中 Dart 的代码和 OC 的区别没有今天展示的那么大,我只是为了演示冗余指令,专门挑了一个特别对 Dart 不友好的 demo,但实际上没有那么大的区别。如果已经用上动态下发模式的话,留在包里的指令段真的是非常少的一部分,我们只是追求极致把事情做到尽善尽美,但是这部分就算不优化也应该是可以接受的,我们线上已经在跑着、已经在广泛用 Flutter 了,这应该不是阻碍你发布或者采用它这个技术栈的原因。


最后给大家送一点福利,感谢大家一直以来的支持和鼓励。具体内容大家可以看看这里 ↓

在这里我分享一份私货,自己收录整理的Android学习PDF+架构视频+面试文档+源码笔记,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习

如果你有需要的话,可以点赞+评论关注我,然后点这里获取**Android学习PDF+架构视频+面试文档+源码笔记**