乐于分享
好东西不私藏

Flutter 物联网控制 App 优化篇:APK 体积从 187MB 砍到 21MB

Flutter 物联网控制 App 优化篇:APK 体积从 187MB 砍到 21MB
01:TLS 与 mTLS 技术详解:从原理到 MQTT 实战
02:ESP32 物联网 MQTTS 实战(上):自建 EMQX 服务端
03:ESP32 物联网 MQTTS 实战(中):ESP32-C5 固件落地
04:ESP32 物联网 MQTTS 实战(下):mTLS 双向认证落地
05:Flutter 物联网控制 App 实战:从 Demo 到量产级

读前摘要:你 flutter build apk 出来的包快 200MB,微信发同事都嫌大、应用商店还有体积红线——可那玩意用户永远下载不到。05 那篇我们把一套开源控制面板改到了量产级,同一套代码实测:debug 187MB → release 通用包 52.6MB → 按架构分包后 arm64 单包 21MB,差出将近 9 倍。本文接着 05 的脉络,把 187MB 到底是谁、每一步砍掉什么、以及"开混淆就能瘦身"这个误区一次讲清;下面这套拆法和命令对任意 Flutter 工程都成立。iOS 侧因无 Mac 未实测,不在本文展开。源码已开源,地址在文末


先接住 05:那篇我们把开源控制面板从 Demo 改造成了量产级——连接状态机、退避重连、离线优先、证书安全存储,代码层面已经能扛量了。

但代码量产级了,分发这一关还没过。你本地 flutter build apk 出的包 187MB,微信发同事都嫌大,应用商店对安装包体积也有红线——这玩意能直接发吗?

答案是不能,但也不用你动一行业务代码。那 187MB 里绝大多数是调试产物,用户根本碰不到;真正发出去的 release 包,同一个工程实测只有 21MB。下面我把这个包拆开,告诉你哪几块在虚胖、怎么一刀刀切掉,以及哪一刀其实不用你砍。

第一刀:看清 187MB 是谁(别急着优化)

拆开 debug 包看,大头根本不是你的代码:

组成部分
大小
占比
是什么 / 量产包里有没有
assets
77.8 MB
37%
debug 版 Dart 内核 kernel_blob.bin,未优化未 tree-shake → release 直接消失
lib/arm64-v8a
50.5 MB
24%
libflutter.so 35.8 + Vulkan 校验层 14.5 + dartjni → 校验层 release 剥离、lib 剥符号
lib/x86_64
37.1 MB
18%
模拟器专用
,真机从不装 → 不该进 release
lib/armeabi-v7a
30.5 MB
15%
32 位老设备
classes.dex
12.8 MB
6%
未混淆字节码 → release 混淆后更小
res / 其他
~0.6 MB
0.3%

其实大头就这三块:77.8MB 的 assets 是 debug 内核、14.5MB 的 Vulkan 校验层是调试工具、37MB 的 x86_64 是模拟器——这三块在 release 构建里一个都不留。所以第一步不是删你的代码,而是别拿 debug 包当正式包

第二刀:一行命令切 release,体积先砍到 52.6MB

debug 和 release 的核心区别在编译方式:debug 是解释执行的 Dart 内核(巨大),release 是 AOT 编译成机器码,同时剥离调试符号、去掉 Vulkan 校验层。光这一刀:

flutter build apk --release        # 通用包:包含全部 ABI(实测 52.6MB)flutter build appbundle --release  # 上架 Google Play 用 AAB

⚠️ 真坑预警:release 必须先配签名 本项目 android/app/build.gradle.kts 里 release 的 signingConfig 指向 key.properties,没有它直接报 SigningConfig "release" is missing required property "storeFile"。你得先 keytool 生成密钥库(放 android/app/upload-keystore.jks),再写 android/key.propertiesstoreFile/storePassword/keyAlias/keyPassword)。这两个文件已在 .gitignore 忽略,别提交进仓库。

构建完对比一下,体积从 187MB 砍到 52.6MB。关键变化就两块:

  • assets 77.8MB → 0.4MB:Dart 内核被 AOT 编译进 libapp.so,不再以巨大内核文件存在;
  • libflutter.so 35.8MB → 11MB:剥掉调试符号、丢掉 14.5MB 的 Vulkan 校验层。

这一步没开混淆(gradle 里 isMinifyEnabled=false),所以还有收尾空间——但别指望它太大,见第四刀。

第三刀:split-per-abi,按架构分包到 21MB

通用 release 包把 arm64 / armeabi-v7a / x86_64 三种架构全塞进一个包,用户手机只用其中一种,却要为另外两种买单。按架构拆分:

flutter build apk --release --split-per-abi

产出三个独立包(实测):

  • app-arm64-v8a-release.apk21.0 MB ← 现在 99% 的真机都是它
  • app-armeabi-v7a-release.apk:18.5 MB ← 老 32 位设备
  • app-x86_64-release.apk:22.4 MB ← 模拟器,分发时直接不传

只发 arm64 单包,体积就到 21MB 这个量级。国内渠道(应用宝、官网直下)通常传 arm64 + armeabi-v7a 两个包即可。

拆开 arm64 单包看内部(21MB = 解压 28MB):lib 占 16.3MB(其中 libflutter.so 11MB、libapp.so 5.1MB),dex 10.6MB,assets 只剩 0.4MB。记住这个结构——它决定了第四刀能走多远。

第四刀:收尾两动作,但先戳破一个误区

两个收尾动作:

  1. x86_64 别进生产:模拟器调试用 debug 包就够了,分发集里直接不传 x86_64 那个包;要更彻底,可在 android/app/build.gradle.kts 的 ndk.abiFilters 里只留 arm64-v8a 和 armeabi-v7a
  2. 关于 --shrink(R8 混淆 + 资源压缩):搏哥实测开了之后,arm64 单包还是 21.0MB,几乎没变

为什么?看第三刀的拆解就明白:Flutter App 的体积大头是原生引擎 libflutter.so(11MB)+ 你的编译产物 libapp.so(5MB),这两项 R8 压不动——混淆主要针对 Java/Dex 和 res,对 .so 无能为力;而本 App 的 dex(10.6MB)大半是 Flutter 引擎自身的类,不是你的业务代码,可压缩空间极小。所以Flutter 的体积优化主战场是 release 构建 + split-per-abi + 精简 assets,别指望混淆兜底。你的业务代码真要瘦身,靠的是减少依赖、删无用资源、按需引入,而不是开个开关。

第五刀:AAB 还是 APK?上架姿势决定体积

  • Google Play:必须传 AAB.aab)。Play 按用户设备架构动态下发,用户实际下载量比单 APK 还小,且支持 Play Feature Delivery;
  • 国内渠道 / 官网直下:没有动态下发,就传 split-per-abi 出的几个 APK,让用户下对应架构的包;
  • 千万别:把 187MB 的 debug 包或通用 fat APK 当正式包发出去。

关于 iOS:本篇只覆盖 Android。iOS 包需在 macOS + Xcode 下 flutter build ipa 构建,搏哥手头没有 Mac,未做实测,体积数据待补。


把五刀串起来,同一个 App 的体积轨迹是:debug 187MB → release 通用 52.6MB → arm64 单包 21MB,差出将近 9 倍。真正的大头(debug 内核、Vulkan 校验层、模拟器架构)根本不用你"优化",切对构建方式就自动消失;而最后那点空间,靠混淆是挤不出来的——得从依赖和 assets 下手。

源码与完整构建脚本在 Gitee:https://gitee.com/jameschenbo/iot_control_app_flutter

我是搏哥,在嵌入式和物联网这一行干了挺多年,公众号里写的都是自己真趟过的坑。05 把代码量产了,06 把体积量产了——上面这套拆法和命令,对你自己的 Flutter 工程一样成立,把仓库克隆下来照着跑一遍,21MB 这个数你也能复现。如果这类实战对你有用,点个关注,搏哥后面接着唠物联网产品落地那些事。