我们的教育让我们对标准答案的依赖太深了,让我们失去了独立思考的能力。事实上,思考的过程比标准答案更重要。
前言
文章经过作者同意,转发到本博客:Flutter包体积之数据区域压缩分析与实践。
- 背景:在存量iOS应用中,接入Flutter框架,即混合开发模式,大概会给存量的iOS应用增加10M的包体积。本篇文章介绍Flutter包体积优化的一种思路。
大纲
- 开发环境:Flutter 1.9.1
- 实验工程:官方维护的Flutter插件battery_example
- 实验效果:App动态库由10.5MB减小到了7.9MB。
Flutter的包体积一直是个比较大的问题,感谢字节跳动在包体积裁剪这块的分享,让我们有了一些方向,该文章就其中一个方案数据段压缩做了详细分析和实践。battery的example工程在经过数据区域压缩后,App动态库由10.5MB减小到了7.9MB。当然所写的代码越多能减少的体积也会变多。和其它移除某些功能模块的方案相比,该方案我认为是收益最大的。
基础知识介绍
产物
当在iOS工程引入了Flutter之后,产物中将新增两个Framework,App.framework和Flutter.framework。
- 如果想要了解Flutter的生成过程,详见:Flutter build ios产物分析。
- Flutter.framework:编译过程中直接从Flutter SDK中拷贝而来。
- Flutter:Flutter引擎,Mach-O格式的动态链接库。
- App.framework: 编译工程时生成
- App:AOT Snapshot数据,由我们的Dart代码编译而成。Mach-O格式的动态链接库这两个动态链接库都会在应用启动时,因为被最外层的Runner所使用而被加载进内存,可以通过
otool -l Runner
查看Runner和它们的联系。
- App:AOT Snapshot数据,由我们的Dart代码编译而成。Mach-O格式的动态链接库这两个动态链接库都会在应用启动时,因为被最外层的Runner所使用而被加载进内存,可以通过
Dart运行方式
该章节内容来源于:Introduction to Dart VM。
Dart VM有三种运行方式
- 直接JIT运行源码或者Kernel Binary,最终产物形式为app.dill;
- 运行生成的JITSnapshot,和第一种方式相比利用快照减少了JIT预热时间运行生成的
- AOTSnapshot,直接运行编译期编译好的机器码。
iOS采用的是AOTSnapshot的方式。虽然JITSnapshot模式在运行时会逐步完成预热,当JITSnapshot达到完全预热时,性能也将达到最高。但是JITSnapshot运行模式需要在引擎中引入即时编译器,会增加引擎大小。
Dart运行AOTSnapshot
在编译期间,Dart 虚拟机将已存在内存中的isolate的堆(驻留在堆上的对象图)序列化成二进制的快照文件,当在设备上再次启动虚拟机的时候可以从快照中快速重建isolate的状态。本质上是一个序列化和一个反序列化的过程。
Dart 虚拟机的快照和其它快照有些不同,是包含机器码的,当这块机器码是不需要反序列化的,因为放在代码区,映射到内存的时候可以直接成为堆的一部分。
使用nm指令查看App符号可以看到两个架构的4个符号:
从上图可知:App.framework中只包含了4个符号。
符号 | 说明 |
---|---|
_kDartIsolateSnapshotData | Dart Isolate数据段 |
_kDartIsolateSnapshotInstructions | Dart Isolate指令段 |
_kDartVmSnapshotData | Dart虚拟机数据段 |
_kDartVmSnapshotInstructions | Dart虚拟机指令段 |
说明:
- R 表示该符号位于只读数据区
- T 表示该符号位于代码区
所以我们可以进行压缩处理的数据即kDartIsolateSnapshotData
和kDartVmSnapshotData
。在生成Snapshot的时候,在序列化后先压缩再写入,在运行时先解压再反序列化。
分析与实践
写入时压缩
Dart源码编译成App.framework的流程如下:
要实现在写入快照时针对性压缩,需要在第二步中进行处理,也就是在gen_snapshot里面处理,这里调用的指令为:
bin/cache/artifacts/engine/ios-release/gen_snapshot_armv7 \
--causal_async_stacks \
--deterministic \
--snapshot_kind=app-aot-assembly \
--assembly=build/aot/armv7/snapshot_assembly.S \
--no-sim-use-hardfp \
--no-use-integer-division build/aot/app.dill
根据dart源码gen_snapshot调用流程图如下:
真实写入到汇编的位置在image_snapshot.cc文件的AssemblyImageWriter::WriteText中,只需要针对data数据写入时做个压缩即可。flutter引擎其中包含了zlib模块,所以在这里的处理可以利用zlib完成,这样也不会增加额外的体积。为了后续解压,在压缩数据时需要记录下压缩前的大小,解压时方便分配合适内存。
读取时解压
读取的逻辑在flutter的框架中,这里调用图直接从DartVMData::Create开始:
这四个调用SearchMapping就是去获取对应App.framework/App里面的四个符号内容。往下继续跟踪SearchMapping:
SearchMapping最后也是调用dlopen与dlsym去获取符号内容,所以在实现读取解压时,只需要在ResolveVMData和ResolveIsolateData中的合适的位置做解压缩的操作,并利用解压数据替换解压前的数据即可。
总结
本文对Flutter的数据段压缩主要是针对iOS进行分析,因为Flutter带来的包体积对iOS影响更加严重,但是如果想要对Android的libapp.so进行中的data数据段压缩也是完全可以的。
更新的flutter版本还未尝试,应该变化也不大,这个方案也可以继续用。