Flutter 包体积之数据区域压缩分析与实践

我们的教育让我们对标准答案的依赖太深了,让我们失去了独立思考的能力。事实上,思考的过程比标准答案更重要。

前言

文章经过作者同意,转发到本博客:Flutter包体积之数据区域压缩分析与实践。

  • 背景:在存量iOS应用中,接入Flutter框架,即混合开发模式,大概会给存量的iOS应用增加10M的包体积。本篇文章介绍Flutter包体积优化的一种思路。

文章目录

  • 前言
  • 大纲
  • 基础知识介绍
    • 产物
    • Dart运行方式
      • Dart VM有三种运行方式
      • Dart运行AOTSnapshot
  • 分析与实践
    • 写入时压缩
    • 读取时解压
  • 总结
  • 附录

大纲

  • 开发环境: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 包体积之数据区域压缩分析与实践_第1张图片

  • Flutter.framework:编译过程中直接从Flutter SDK中拷贝而来。
    • Flutter:Flutter引擎,Mach-O格式的动态链接库。
  • App.framework: 编译工程时生成
    • App:AOT Snapshot数据,由我们的Dart代码编译而成。Mach-O格式的动态链接库这两个动态链接库都会在应用启动时,因为被最外层的Runner所使用而被加载进内存,可以通过otool -l Runner查看Runner和它们的联系。

Flutter 包体积之数据区域压缩分析与实践_第2张图片

Dart运行方式

该章节内容来源于:Introduction to Dart VM。

Dart VM有三种运行方式

  1. 直接JIT运行源码或者Kernel Binary,最终产物形式为app.dill;
  2. 运行生成的JITSnapshot,和第一种方式相比利用快照减少了JIT预热时间运行生成的
  3. AOTSnapshot,直接运行编译期编译好的机器码。

iOS采用的是AOTSnapshot的方式。虽然JITSnapshot模式在运行时会逐步完成预热,当JITSnapshot达到完全预热时,性能也将达到最高。但是JITSnapshot运行模式需要在引擎中引入即时编译器,会增加引擎大小。

Dart运行AOTSnapshot

Flutter 包体积之数据区域压缩分析与实践_第3张图片在编译期间,Dart 虚拟机将已存在内存中的isolate的堆(驻留在堆上的对象图)序列化成二进制的快照文件,当在设备上再次启动虚拟机的时候可以从快照中快速重建isolate的状态。本质上是一个序列化和一个反序列化的过程。

Dart 虚拟机的快照和其它快照有些不同,是包含机器码的,当这块机器码是不需要反序列化的,因为放在代码区,映射到内存的时候可以直接成为堆的一部分。

使用nm指令查看App符号可以看到两个架构的4个符号:

Flutter 包体积之数据区域压缩分析与实践_第4张图片
从上图可知:App.framework中只包含了4个符号。

符号 说明
_kDartIsolateSnapshotData Dart Isolate数据段
_kDartIsolateSnapshotInstructions Dart Isolate指令段
_kDartVmSnapshotData Dart虚拟机数据段
_kDartVmSnapshotInstructions Dart虚拟机指令段

说明:

  • R 表示该符号位于只读数据区
  • T 表示该符号位于代码区

所以我们可以进行压缩处理的数据即kDartIsolateSnapshotDatakDartVmSnapshotData。在生成Snapshot的时候,在序列化后先压缩再写入,在运行时先解压再反序列化。

分析与实践

写入时压缩

Dart源码编译成App.framework的流程如下:
Flutter 包体积之数据区域压缩分析与实践_第5张图片
要实现在写入快照时针对性压缩,需要在第二步中进行处理,也就是在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调用流程图如下:

Flutter 包体积之数据区域压缩分析与实践_第6张图片
真实写入到汇编的位置在image_snapshot.cc文件的AssemblyImageWriter::WriteText中,只需要针对data数据写入时做个压缩即可。flutter引擎其中包含了zlib模块,所以在这里的处理可以利用zlib完成,这样也不会增加额外的体积。为了后续解压,在压缩数据时需要记录下压缩前的大小,解压时方便分配合适内存。

读取时解压

读取的逻辑在flutter的框架中,这里调用图直接从DartVMData::Create开始:

Flutter 包体积之数据区域压缩分析与实践_第7张图片
这四个调用SearchMapping就是去获取对应App.framework/App里面的四个符号内容。往下继续跟踪SearchMapping:

Flutter 包体积之数据区域压缩分析与实践_第8张图片
SearchMapping最后也是调用dlopen与dlsym去获取符号内容,所以在实现读取解压时,只需要在ResolveVMData和ResolveIsolateData中的合适的位置做解压缩的操作,并利用解压数据替换解压前的数据即可。

总结

本文对Flutter的数据段压缩主要是针对iOS进行分析,因为Flutter带来的包体积对iOS影响更加严重,但是如果想要对Android的libapp.so进行中的data数据段压缩也是完全可以的。

更新的flutter版本还未尝试,应该变化也不大,这个方案也可以继续用。

附录

  • 官方对于Dart VM的介绍:Introduction to Dart VM
  • 如何缩减接近 50% 的 Flutter 包体积 | Flutter 沙龙回顾
  • Flutter机器码生成gen_snapshot

你可能感兴趣的:(Flutter探索之路)