彦火 APP - Flutter 包體分析

拆了胡彥斌「彥火」APK:它確實是 Flutter App,為什麼體積還能這麼小?

最近胡彥斌的粉絲社群 App「彥火」挺有話題度。作為開發者,我更好奇另一件事:這個 App 到底是原生寫的,還是跨平台框架做的?

我拿到了一份 Android APK,做了一次非常輕量的靜態封包結構分析。先說結論:

從 Android APK 的檔案結構看,「彥火」Android 端可以確認是 Flutter App。

這篇文章不反編譯業務程式碼,也不分析介面和業務邏輯,只看 APK 裡的公開檔案結構,聊聊兩個問題:

  1. 怎麼判斷它是 Flutter?
  2. 為什麼 Flutter App 的體積看起來還能這麼小?

一、判斷 Flutter App,看哪些證據?

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

這裡面最關鍵的是兩個東西。

二、證據 1:libflutter.so

libflutter.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 函式庫的包。

三、證據 2:libapp.so

Flutter 在 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 身分基本就坐實了。

四、證據 3:assets/flutter_assets

Flutter 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.jsonAssetManifest.binpackages/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 都打在一起」的包。

六、先有個參照:官方最小 Flutter App 多大?

如果只是一個 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

這說明兩件事:

  1. Flutter 引擎確實會隨 App 一起打進安裝包裡。
  2. 每個 Flutter App 都是自包含的,不是依賴手機系統裡預裝一個共用 Flutter runtime。

所以討論 Flutter 包體積時,要先承認它有一個固定基礎成本。一個極簡 Flutter App,在 Android 單 ABI release 下載口徑下,也會有幾 MB 的起步體積。

但這個基礎成本不等於當前 APK 的全部體積。比如「彥火」這份 APK 裡同時帶了多套 ABI:

text 体验AI代码助手 代码解读复制代码arm64-v8a/
armeabi-v7a/
x86_64/

每套 ABI 下又各自有 libflutter.solibapp.so。所以 61MB 的 universal APK,不能直接理解成「Flutter 框架本身占了 61MB」。這裡面混合了 Flutter 引擎、Dart AOT 業務產物、外掛 native 依賴、資源檔案,以及多架構重複打包。

Flutter 官方的 App Size 文件也提醒:debug 包不代表 production 包,上架到商店的包也不一定等於使用者實際下載的包。商店可能會根據裝置 CPU 架構、螢幕密度等條件過濾 native libraries 和資源。

參考資料:

七、原因 1:應用商店可能做了架構切片

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 分發後一定會變成類似大小。

八、原因 2:資源本身並不重

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 那樣把大量圖片、音訊、影片預置進包裡。資源輕,包自然就不會特別離譜。

九、原因 3:Flutter 的固定成本不等於無限膨脹

Flutter App 會帶 engine,這是固定成本。很多人一聽 Flutter,就覺得包一定很大。

但實際要分場景:

text 体验AI代码助手 代码解读复制代码Flutter engine 固定成本
+ Dart AOT 業務程式碼
+ 圖片/字型/資源
+ 原生依賴
+ 多 ABI native 函式庫

如果業務程式碼不複雜,資源控制得好,再配合商店切片,Flutter App 的下載體積完全可以做到一個比較溫和的範圍。

「彥火」這個包就是一個例子:它確實是 Flutter,但它的資源負擔並不重。

十、從字型看資源優化:Material Icons 已經被裁剪

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 就很難判斷哪些圖示真正用到了,字型裁剪效果會變差。

十一、從 shader 看渲染資源:不是包體優化重點

這份 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 點擊水波紋/閃光效果。按鈕、InkWellListTile 這類 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 的靜態結構,可以得到幾個結論:

  1. 「彥火」Android 端可以確認是 Flutter App。
  2. 關鍵證據是 libflutter.solibapp.soassets/flutter_assets/
  3. 61MB 只對應這份 Android universal APK;iOS App Store 頁面看到的 29.2MB 是另一個平台的展示體積,不能直接和 Android APK 橫向比較。
  4. Flutter 引擎會隨 App 一起打包,官方最小 Android 單 ABI release 下載包也有幾 MB 的固定基礎成本。
  5. 這份 APK 看起來體積不算誇張,主要原因是資源不重,而且實際下發時可能不會包含所有 ABI。
  6. Material Icons 字型已經明顯做過 tree shaking,只有 132 個 codepoint。
  7. CupertinoIcons 字型相對完整,如果使用不多,可能還有優化空間。
  8. Shader 和 .9.png 都不是這份 APK 的體積大頭,優化優先級低於 ABI、native so、Flutter 大圖和字型依賴。
  9. Android APK 只能證明 Android 端技術棧;如果要確認 iOS 端,也需要分析 iOS 包或官方技術資訊。

如果用一句話總結:

「彥火」不是因為 Flutter 才一定大,也不是因為體積小就不像 Flutter。看 APK 結構,Android 端 Flutter 特徵非常明確;體積控制得住,更多是資源規模、ABI 切片和商店分發策略共同作用的結果。


原文出處:https://juejin.cn/post/7646674707382927375


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝10   💬4   ❤️1
369
🥈
alicec
📝1   ❤️2
89
#4
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登