前言
文章經過作者同意,轉發到本博客: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版本還未嘗試,應該變化也不大,這個方案也可以繼續用。