拆了胡彥斌「彥火」APK:它確實是 Flutter App,為什麼體積還能這麼小?
最近胡彥斌的粉絲社群 App「彥火」挺有話題度。作為開發者,我更好奇另一件事:這個 App 到底是原生寫的,還是跨平台框架做的?
我拿到了一份 Android APK,做了一次非常輕量的靜態封包結構分析。先說結論:
從 Android APK 的檔案結構看,「彥火」Android 端可以確認是 Flutter App。
這篇文章不反編譯業務程式碼,也不分析介面和業務邏輯,只看 APK 裡的公開檔案結構,聊聊兩個問題:
APK 本質上是一個 zip 包,所以直接列檔案就能看到很多資訊:
bash 体验AI代码助手 代码解读复制代码unzip -l yanhuo-android.apk
在「彥火」APK 裡,可以看到非常典型的 Flutter 結構:
text 体验AI代码助手 代码解读复制代码lib/arm64-v8a/libapp.so
lib/arm64-v8a/libflutter.so
lib/armeabi-v7a/libapp.so
lib/armeabi-v7a/libflutter.so
lib/x86_64/libapp.so
lib/x86_64/libflutter.so
assets/flutter_assets/AssetManifest.bin
assets/flutter_assets/FontManifest.json
assets/flutter_assets/NativeAssetsManifest.json
assets/flutter_assets/shaders/ink_sparkle.frag
assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf
這裡面最關鍵的是兩個東西。
libflutter.solibflutter.so 是 Flutter 引擎在 Android 端的原生動態函式庫。
如果一個 APK 裡出現:
text 体验AI代码助手 代码解读复制代码lib/arm64-v8a/libflutter.so
基本就已經能說明它使用了 Flutter。因為原生 Android、React Native、一般 Kotlin/Java App 都不會天然帶這個函式庫。
這份 APK 裡不只一份 libflutter.so,而是包含了多個 ABI:
text 体验AI代码助手 代码解读复制代码lib/arm64-v8a/libflutter.so
lib/armeabi-v7a/libflutter.so
lib/x86_64/libflutter.so
這說明這份 APK 是一個包含多架構 native 函式庫的包。
libapp.soFlutter 在 release 模式下,Dart 程式碼通常會 AOT 編譯成 native 產物。在 Android 端,一個典型產物就是:
text 体验AI代码助手 代码解读复制代码libapp.so
這份 APK 裡也有:
text 体验AI代码助手 代码解读复制代码lib/arm64-v8a/libapp.so
lib/armeabi-v7a/libapp.so
lib/x86_64/libapp.so
這也符合 Flutter release 包的結構。
簡單理解:
text 体验AI代码助手 代码解读复制代码libflutter.so -> Flutter engine
libapp.so -> Dart 業務程式碼 AOT 後的產物
flutter_assets -> Flutter 資源目錄
這三者一起出現,Flutter 身分基本就坐實了。
assets/flutter_assetsFlutter App 的資源會放到:
text 体验AI代码助手 代码解读复制代码assets/flutter_assets/
「彥火」APK 裡也能看到:
text 体验AI代码助手 代码解读复制代码assets/flutter_assets/AssetManifest.bin
assets/flutter_assets/FontManifest.json
assets/flutter_assets/assets/images/brand_title.png
assets/flutter_assets/assets/images/hero_huyanbin.png
assets/flutter_assets/assets/images/little_tiger.png
assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf
assets/flutter_assets/packages/flutter_map/lib/assets/flutter_map_logo.png
其中 FontManifest.json、AssetManifest.bin、packages/cupertino_icons 都是 Flutter 專案裡很常見的資源結構。
所以從 APK 結構上看,「彥火」Android 端不是「像 Flutter」,而是非常明確地帶著 Flutter 的執行環境和資源結構。
我手上的這份 Android APK 大約是 61MB。這裡需要先把口徑說清楚:這個數字只對應我手上的 Android APK。
如果你在 iPhone 12、iOS 26 的 App Store 頁面上看到「彥火」顯示 29.2MB,那是 iOS 端的 App Store 展示體積。它和這份 Android APK 屬於不同作業系統、不同平台、不同安裝包格式,不能直接橫向比較。
所以這篇文章後面討論的 61MB,只針對這份 Android APK 本身。
先看這份 APK 裡的主要檔案大小。注意,unzip -l 看到的是 APK 內條目的原始大小,不完全等於應用商店展示的壓縮下載大小:
text 体验AI代码助手 代码解读复制代码arm64-v8a/libapp.so 約 8.2 MB
arm64-v8a/libflutter.so 約 11.3 MB
armeabi-v7a/libapp.so 約 9.1 MB
armeabi-v7a/libflutter.so 約 8.3 MB
x86_64/libapp.so 約 8.5 MB
x86_64/libflutter.so 約 12.6 MB
也就是說,光 Flutter 引擎和 Dart AOT 產物,多 ABI 加起來就占了不少體積。
但使用者實際下載時,不一定總是拿到「所有 ABI 都打在一起」的包。
如果只是一個 Hello World 等級的 Flutter 頁面,Flutter 官方 FAQ 裡有一個很適合做背景的測量。
官方在 2021 年 3 月測過一個最小 Flutter App:不包含 Material Components,頁面裡只有一個 Center widget,用 flutter build apk --split-per-abi 建構 release 包。壓縮後的下載大小大約是:
text 体验AI代码助手 代码解读复制代码ARM32 約 4.3 MB
ARM64 約 4.8 MB
再拆開看,官方給出的體積構成大概是:
text 体验AI代码助手 代码解读复制代码ARM32:
core engine 約 3.4 MB
framework + app code 約 765 KB
classes.dex 約 120 KB
LICENSE 約 58 KB
ARM64:
core engine 約 4.0 MB
framework + app code 約 659 KB
classes.dex 約 120 KB
LICENSE 約 58 KB
這說明兩件事:
所以討論 Flutter 包體積時,要先承認它有一個固定基礎成本。一個極簡 Flutter App,在 Android 單 ABI release 下載口徑下,也會有幾 MB 的起步體積。
但這個基礎成本不等於當前 APK 的全部體積。比如「彥火」這份 APK 裡同時帶了多套 ABI:
text 体验AI代码助手 代码解读复制代码arm64-v8a/
armeabi-v7a/
x86_64/
每套 ABI 下又各自有 libflutter.so 和 libapp.so。所以 61MB 的 universal APK,不能直接理解成「Flutter 框架本身占了 61MB」。這裡面混合了 Flutter 引擎、Dart AOT 業務產物、外掛 native 依賴、資源檔案,以及多架構重複打包。
Flutter 官方的 App Size 文件也提醒:debug 包不代表 production 包,上架到商店的包也不一定等於使用者實際下載的包。商店可能會根據裝置 CPU 架構、螢幕密度等條件過濾 native libraries 和資源。
參考資料:
Android App 如果透過 AAB 或分包方式發布,商店可以按裝置下發對應 ABI 的包。
比如一台常見手機只需要:
text 体验AI代码助手 代码解读复制代码arm64-v8a/libapp.so
arm64-v8a/libflutter.so
它不需要同時下載:
text 体验AI代码助手 代码解读复制代码armeabi-v7a/
x86_64/
所以一個本地 Android universal APK 看起來是 61MB,但如果透過 Android App Bundle 或 ABI split 分發,真實下發到 Android 手機上的包可能會更小。
至於 iOS App Store 頁面上看到的 29.2MB,只能說明 iOS 端在 App Store 當前裝置口徑下展示的大小。它可以作為另一個平台的背景資訊,但不能拿來證明這份 Android APK 分發後一定會變成類似大小。
Flutter App 體積大,很多時候不是因為 Flutter 本身,而是因為資源。
比如:
這份 APK 裡 Flutter assets 並不算誇張,比較大的圖片主要是:
text 体验AI代码助手 代码解读复制代码hero_huyanbin.png 約 2.5 MB
little_tiger.png 約 0.66 MB
brand_title.png 約 0.4 MB
也就是說,它不像一些內容型 App 那樣把大量圖片、音訊、影片預置進包裡。資源輕,包自然就不會特別離譜。
Flutter App 會帶 engine,這是固定成本。很多人一聽 Flutter,就覺得包一定很大。
但實際要分場景:
text 体验AI代码助手 代码解读复制代码Flutter engine 固定成本
+ Dart AOT 業務程式碼
+ 圖片/字型/資源
+ 原生依賴
+ 多 ABI native 函式庫
如果業務程式碼不複雜,資源控制得好,再配合商店切片,Flutter App 的下載體積完全可以做到一個比較溫和的範圍。
「彥火」這個包就是一個例子:它確實是 Flutter,但它的資源負擔並不重。
Flutter 包裡經常能看到兩個圖示字型:
text 体验AI代码助手 代码解读复制代码assets/flutter_assets/fonts/MaterialIcons-Regular.otf
assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf
這份 APK 裡,它們的大小差異很明顯:
text 体验AI代码助手 代码解读复制代码MaterialIcons-Regular.otf 約 19KB
CupertinoIcons.ttf 約 252KB
為什麼 MaterialIcons-Regular.otf 這麼小?我解析了一下它的 cmap:
text 体验AI代码助手 代码解读复制代码glyphs: 133
codepoints: 132
這說明它不是完整 Material Icons 字型,而是 Flutter release 建構後做過 icon tree shaking,只保留了 App 實際用到的圖示。
裡面的 codepoint 也基本都在 Unicode 私用區,例如:
text 体验AI代码助手 代码解读复制代码U+E092 arrow_back_baseline
U+E098 arrow_drop_down_baseline
U+E122 calendar_today_baseline
U+E139 cancel_baseline
U+E15E chevron_left_baseline
U+E15F chevron_right_baseline
U+E16A close_baseline
U+E21A edit_baseline
U+E3DC menu_baseline
U+F17A local_fire_department_outlined
U+F737 favorite_border_rounded
U+F738 favorite_rounded
U+F7F5 home_rounded
也就是說,這不是「只保留常用漢字/英文字母」,而是只保留用到的圖示 glyph。
相比之下,CupertinoIcons.ttf 還有:
text 体验AI代码助手 代码解读复制代码glyphs: 1257
codepoints: 1280
它更像是帶了較完整的 cupertino_icons 圖示字型。這裡如果 App 實際沒怎麼用 Cupertino 圖示,就還有優化空間。
Flutter 字型優化的思路可以總結成:
text 体验AI代码助手 代码解读复制代码1. 開啟 release 建構的 icon tree shaking
2. IconData 盡量寫成 const
3. 不要動態拼 icon codepoint
4. 不用 cupertino_icons 就移除依賴
5. 自訂字型用 pyftsubset 裁剪
6. 中文字型盡量用系統字型,不要整包塞進 APK
常用建構命令是:
bash 体验AI代码助手 代码解读复制代码flutter build apk --release --tree-shake-icons
如果程式碼裡有動態 IconData,比如從伺服器下發 codepoint,再在執行時構造圖示,Flutter 就很難判斷哪些圖示真正用到了,字型裁剪效果會變差。
這份 APK 裡還有兩個 shader 檔案:
text 体验AI代码助手 代码解读复制代码assets/flutter_assets/shaders/stretch_effect.frag 約 17KB
assets/flutter_assets/shaders/ink_sparkle.frag 約 21KB
從檔案內容看,能看到:
text 体验AI代码助手 代码解读复制代码stretch_effect_fragment_main
ink_sparkle_fragment_main
GLSL.std.450
#version 300 es
ink_sparkle.frag 大概率是 Flutter Material 的 InkSparkle 點擊水波紋/閃光效果。按鈕、InkWell、ListTile 這類 Material 元件按下時,可能會用到這類效果。
stretch_effect.frag 更像是捲動越界時的 stretch overscroll 拉伸效果。
這類 shader 通常來自 Flutter framework/engine,不是業務自己手寫的。它們的體積也很小,幾十 KB 等級,不是這份 APK 的主要體積來源。
最佳實務不是「刪 shader」,而是:
text 体验AI代码助手 代码解读复制代码1. 不要手動刪除 flutter_assets/shaders
2. 如果首幀或首次點擊有卡頓,關注 shader 預熱、Impeller、SkSL warmup
3. 自訂 shader 要控制數量和複雜度
4. 包體優化優先看圖片、字型、ABI、native so
5. shader 這種幾十 KB 的檔案,通常不是優先優化目標
如果真的不想要 Material 3 的 InkSparkle 效果,可以從主題層面調整 splashFactory,但這屬於互動風格選擇,是否減少最終打包資源要以建構產物為準。
.9.png 看圖片資源:數量多,但總量很小Android res/ 目錄裡有很多 .9.png:
text 体验AI代码助手 代码解读复制代码res/qD.9.png
res/MF.9.png
res/zV.9.png
...
.9.png 是 Android Nine-Patch 圖片,常用來做可拉伸背景,比如按鈕、氣泡、輸入框、彈窗背景。它比普通 PNG 多了邊緣 1px 的拉伸和內容區域標記。
這份 APK 中 .9.png 的資料是:
text 体验AI代码助手 代码解读复制代码數量:98 個
總大小:約 47.4KB
平均:約 0.5KB
最大:約 2.8KB
所以雖然數量看起來很多,但總量只有幾十 KB。對這份 APK 來說,.9.png 不是包體大頭。
圖片資源優化可以這樣做:
text 体验AI代码助手 代码解读复制代码1. 純色、圓角、描邊背景優先用 shape.xml
2. 簡單圖示優先用 VectorDrawable
3. 大圖優先壓縮成 WebP/AVIF
4. 刪除不用的資源,開啟 resource shrink
5. 控制多 dpi 資源,不要重複塞多套相近圖片
6. PNG 可用 pngquant、zopflipng 做無損/有損壓縮
7. .9.png 不要盲目轉 WebP,避免丟失 nine-patch 拉伸資訊
這份 APK 的圖片優化重點其實不在 .9.png,而在 Flutter assets 裡的大圖,例如:
text 体验金代码助手 代码解读复制代码hero_huyanbin.png 約 2.5MB
little_tiger.png 約 0.66MB
brand_title.png 約 0.4MB
如果繼續壓縮包體,優先看這些 Flutter 業務圖片,而不是 Android res 裡的 nine-patch。
APK 裡還能看到一些 Android 端依賴痕跡,比如:
text 体验AI代码助手 代码解读复制代码androidx.*
kotlinx_coroutines_android
play-services-location
okhttp3
這些說明它並不是「純 Dart 世界」,而是和 Android 原生生態也有整合。Flutter App 很常見:UI 和大部分業務用 Flutter,部分能力透過外掛或原生依賴接入。
這也能解釋為什麼 APK 裡既有:
text 体验AI代码助手 代码解读复制代码assets/flutter_assets/
libflutter.so
libapp.so
也有:
text 体验AI代码助手 代码解读复制代码classes.dex
AndroidManifest.xml
res/
androidx/kotlin/google play services 相關依賴
Flutter App 仍然是一個 Android App,只是 UI 渲染和 Dart 業務執行在 Flutter 體系裡。
基於這份 APK 的靜態結構,可以得到幾個結論:
libflutter.so、libapp.so 和 assets/flutter_assets/。.9.png 都不是這份 APK 的體積大頭,優化優先級低於 ABI、native so、Flutter 大圖和字型依賴。如果用一句話總結:
「彥火」不是因為 Flutter 才一定大,也不是因為體積小就不像 Flutter。看 APK 結構,Android 端 Flutter 特徵非常明確;體積控制得住,更多是資源規模、ABI 切片和商店分發策略共同作用的結果。