Flutter APK 体积144MB到23MB:瘦身实战
嗯,用户让我总结这篇文章的内容,控制在100字以内,而且不需要用“文章内容总结”之类的开头。我得先仔细读一下这篇文章。 文章讲的是一个Flutter项目优化APK体积的过程,从144MB降到23MB。用了几个方法,比如单ABI构建、恢复代码混淆、GBK映射表去重、启用useLegacyPackaging,还有按需导入highlight包。这些优化措施涵盖了AGP压缩策略、ABI分包、代码混淆等多个方面。 用户可能是一个Flutter开发者,正在优化自己的应用体积。他们需要简洁明了的总结,可能是在准备报告或者分享经验。所以,我需要把重点放在优化策略和结果上。 控制在100字以内的话,得把每个关键点都涵盖进去,但不能太详细。比如提到从144MB降到23MB,使用了单ABI构建、代码混淆、映射表去重、useLegacyPackaging和按需导入这些方法。 最后总结的时候,要突出优化效果和关键策略。这样用户能快速了解文章的核心内容。 </think> 本文记录了一个 Flutter 项目通过单 ABI 构建、恢复代码混淆、GBK 映射表去重、启用 useLegacyPackaging 和按需导入等方式将 APK 体积从 144MB 优化至 23MB 的过程。 2026-4-13 12:8:1 Author: www.uedbox.com(查看原文) 阅读量:22 收藏

本文记录了一个真实 Flutter 项目(含 MediaKit 视频播放器、QuickJS 引擎、InAppWebView 等重量级插件)从 144MB 优化到 23MB 的全过程,涵盖 AGP 压缩策略、ABI 分包、代码混淆、Dart AOT 分析等多个维度。

背景

项目是一个多功能影视聚合应用,技术栈:

  • Flutter 3.35.0 / Dart 3.9.0
  • MediaKit(libmpv 视频播放)、QuickJS(JS 引擎)、InAppWebView
  • AGP 8.6.1 / Kotlin 2.1.0 / Gradle 8.11.1
  • 目标:通过 OTA 和仓库分发 APK

某次升级 AGP 和 Kotlin 版本后,APK 从 ~25MB 暴涨到 ~47MB——几乎翻倍。排查发现问题并非代码膨胀,而是 一个被忽视的 native library 压缩策略变更

TL;DR 优化效果

阶段APK 大小节省
初始(3 ABI 全包)144.3 MB
单 ABI 构建46.9 MB-97.4 MB
恢复代码混淆44 MB-2.9 MB
GBK 映射表去重42.9 MB-1.1 MB
启用 useLegacyPackaging23.5 MB-19.4 MB
highlight 按需导入23.1 MB-0.4 MB

第一刀:单 ABI 构建(-97MB)

Flutter 默认构建包含多个 CPU 架构的 native 库。对于 APK 直接分发场景,没必要把三种架构塞进同一个包。

问题

<em># 默认构建会包含 armeabi-v7a + arm64-v8a + x86_64</em>

flutter build apk --release

<em># 输出:144.3 MB</em>

方案:通过  --target-platform  指定单 ABI

<em># 32位 ARM(覆盖绝大多数设备)</em>

flutter build apk --release --target-platform android-arm

<em># 64位 ARM(新设备,性能更优)</em>

flutter build apk --release --target-platform android-arm64

但  --target-platform  只控制 Flutter 引擎和 Dart AOT 产物的架构,不影响第三方插件 AAR 中打包的 native 库(如 libmpv.so)。需要在  build.gradle  中配合 ABI filter 和 packaging excludes:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

android {

    defaultConfig {

        ndk {

            <em>// 通过构建参数动态控制,默认 v7a</em>

            if (project.hasProperty('targetAbi')) {

                abiFilters project.property('targetAbi')

            } else {

                abiFilters 'armeabi-v7a'

            }

        }

    }

    <em>// 关键:排除插件 AAR 中非目标架构的 .so</em>

    packaging {

        jniLibs {

            def targetAbi = project.hasProperty('targetAbi') ?

                project.property('targetAbi') : 'armeabi-v7a'

            def allAbis = ['armeabi-v7a', 'arm64-v8a', 'x86_64', 'x86']

            allAbis.findAll { it != targetAbi }.each { abi ->

                excludes += ["lib/${abi}/**"]

            }

        }

    }

}

为什么需要  packaging.jniLibs.excludes  ndk.abiFilters  只控制 CMake/ndk-build 编译的产物。像 media_kit 这类通过 JAR 分发预编译 .so 的插件,其多架构库会通过 Gradle 依赖解析进入 APK,不受  abiFilters  约束。

第二刀:恢复代码混淆(-2.9MB)

问题

项目原先通过  gradle.properties  配置混淆:

extra-gen-snapshot-options=--obfuscate

升级 AGP 后此配置被注释掉,改为依赖构建命令行参数。但常常忘记加  --obfuscate

验证:Flutter Gradle 插件确实读取此属性

查看 Flutter SDK 源码  FlutterPlugin.kt

val extraGenSnapshotOptionsValue: String? =

    project.findProperty("extra-gen-snapshot-options")?.toString()

<em>// ...</em>

extraGenSnapshotOptions = extraGenSnapshotOptionsValue

方案:使用标准 Gradle 属性

比起旧的  extra-gen-snapshot-options ,Flutter 的 Gradle 插件还支持更语义化的属性:

<em># gradle.properties</em>

dart-obfuscation=true

split-debug-info=build/debug-info

这样无论构建命令是否带  --obfuscate ,混淆都会自动生效。 split-debug-info  配合使用可保留符号映射用于崩溃日志还原。

第三刀:GBK 映射表去重(-1.1MB)

问题

项目中 三个位置 各自嵌入了一份 23,943 条的 GBK-UTF16 映射表:

  • lib/utils/decode_body.dart (24,047 行)
  • lib/utilsv2/decode_body.dart (24,051 行)
  • package:fast_gbk (依赖库自带)

三份映射表在 AOT 编译后占用  libapp.so  约 6MB

方案

统一使用  fast_gbk  包,删除两处嵌入的映射表:

<em>// lib/utilsv2/decode_body.dart — 从 24,051 行精简到 92 行</em>

import 'package:fast_gbk/fast_gbk.dart';

class DecodeBody {

  String decode(Uint8List bodyBytes, String? contentType) {

    if (_isGBK(contentType)) {

      return gbk.decode(bodyBytes, allowMalformed: true);

    }

    return utf8.decode(bodyBytes, allowMalformed: true);

  }

}

<em>// lib/utils/decode_body.dart — 从 24,047 行精简到 3 行</em>

export '../utilsv2/decode_body.dart';

第四刀:关键转折—— useLegacyPackaging (-19.4MB)

这是本文最核心的优化,也是最容易被忽视的。

问题定位

通过  python3 + zipfile  分析 APK 内部压缩情况时,发现所有  .so  文件的压缩率竟然是 100%(即完全未压缩):

libapp.so:    18.33MB raw -> 18.33MB compressed (100%)

libmpv.so:    10.76MB raw -> 10.76MB compressed (100%)

libflutter.so: 7.56MB raw ->  7.56MB compressed (100%)

37.8MB 的 native 库占了 APK 的 86%,却一字节都没有压缩。

根因

AGP 8.x + minSdkVersion ≥ 23 时,默认行为变为  extractNativeLibs=false

  • .so 文件以**未压缩、页面对齐(16KB aligned)**的方式存入 APK
  • Android 6.0+ 系统可直接从 APK mmap 加载 .so,无需解压
  • 优势:安装后磁盘占用小(不需要 APK + 解压两份),安装速度快
  • 劣势:APK 下载体积显著增大

旧版 AGP 8.3.2 +  minSdkVersion 21  时默认压缩 .so,升级 AGP 8.6.1 +  minSdkVersion  改为  flutter.minSdkVersion (值为 24 ≥ 23)后,默认行为静默改变。

方案

在  build.gradle  中显式启用传统打包:

android {

    packaging {

        jniLibs {

            <em>// 压缩 .so 文件,减少 APK 下载体积</em>

            useLegacyPackaging = true

        }

    }

}

在  AndroidManifest.xml  中同步声明:

<application

    android:extractNativeLibs="true"

    ...>

效果

libapp.so:    18.33MB -> 6.25MB  (34%, 节省 12.1MB)

libmpv.so:    10.76MB -> 4.94MB  (46%, 节省 5.8MB)

libflutter.so: 7.56MB -> 4.24MB  (56%, 节省 3.3MB)

libqjs.so:     0.66MB -> 0.37MB  (57%, 节省 0.3MB)

──────────────────────────────────────────────────

Total .so:    37.8MB  -> 16.0MB  (节省 21.8MB)

兼容性评估

Android 版本API Level行为兼容性
5.0-5.121-22系统总是解压 .so,忽略  extractNativeLibs
6.0-9.023-28 extractNativeLibs=true  是原生默认行为
10.0+29+两种模式都支持,true 走传统解压路径
  • useLegacyPackaging = true 是 Android 从第一个版本就支持的传统打包方式
  • media_kit 官方在 v1.0.2 CHANGELOG 中明确标注 perf: enable extractNativeLibs
  • 唯一代价:安装后磁盘占用增大(需同时存储 APK 和解压的 .so),对现代设备的 128GB+ 存储不构成问题

选择策略建议

分发方式推荐配置原因
APK 直接分发 / OTA useLegacyPackaging = true 下载体积优先
Google Play AAB可用默认(false)Play Store 有自己的增量分发和压缩机制
内部测试 useLegacyPackaging = true 传输效率优先

第五刀:highlight 按需导入(-0.4MB)

问题

package:highlight  的默认导入方式  import 'package:highlight/highlight.dart'  会注册全部 190 种语言定义,在 AOT 编译中占用 1.3MB。项目只用了 JavaScript 高亮。

方案

<em>// Before: 导入全部 190 种语言</em>

import 'package:highlight/highlight.dart';

<em>// highlight.parse(code, language: 'javascript')</em>

<em>// After: 只导入需要的语言</em>

import 'package:highlight/src/highlight.dart';

import 'package:highlight/src/node.dart';

import 'package:highlight/languages/javascript.dart';

import 'package:highlight/languages/json.dart';

final _highlight = Highlight()

  ..registerLanguage('javascript', javascript)

  ..registerLanguage('json', json);

<em>// _highlight.parse(code, language: 'javascript')</em>

附:如何分析你的 Flutter APK 体积

方法一:Flutter 官方  --analyze-size

flutter build apk --release \

  --target-platform android-arm \

  --analyze-size

注意: --analyze-size  不能与  --obfuscate  /  --split-debug-info  同时使用。

会输出一份 JSON 报告,可用 Dart DevTools 可视化:

dart devtools --appSizeBase=~/.flutter-devtools/apk-code-size-analysis_01.json

方法二:Python 脚本分析 APK 压缩状况

上文的  useLegacyPackaging  问题就是用此方法发现的——官方工具只看到 raw size,看不出压缩率:

import zipfile

with zipfile.ZipFile('app-release.apk') as z:

    for info in z.infolist():

        if info.filename.endswith('.so'):

            ratio = info.compress_size / info.file_size * 100

            method = 'DEFLATED' if info.compress_type == 8 else 'STORED'

            print(f'{info.filename}: '

                  f'{info.file_size/1024/1024:.1f}MB -> '

                  f'{info.compress_size/1024/1024:.1f}MB '

                  f'({ratio:.0f}%) [{method}]')

方法三:解析  --analyze-size  JSON 各包体积

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import json

with open('apk-code-size-analysis.json') as f:

    data = json.load(f)

def find_node(node, name):

    if node.get('n') == name:

        return node

    for child in node.get('children', []):

        r = find_node(child, name)

        if r: return r

def calc_size(node):

    total = node.get('value', 0)

    for child in node.get('children', []):

        total += calc_size(child)

    return total

dart_aot = find_node(data, 'libapp.so (Dart AOT)')

for pkg in sorted(dart_aot['children'],

                   key=lambda x: calc_size(x), reverse=True)[:20]:

    size = calc_size(pkg) / 1024

    print(f"  {pkg['n']}: {size:.0f} KB")

最终 APK 组成

APK Total: 23.1 MB

  Native Libraries (.so): 16.00 MB (73.1%)

    libapp.so (Dart AOT):       6.25 MB   你的 Dart 代码

    libmpv.so (MediaKit):       4.94 MB   视频播放器(mpv)

    libflutter.so (Engine):     4.24 MB   Flutter 引擎

    libqjs.so (QuickJS):        0.37 MB   JS 引擎

    其他 media_kit .so:          0.20 MB

  Assets:                    2.18 MB (10.0%)

  Java/Kotlin (.dex):        1.79 MB  (8.2%)

  Resources (res/):          1.41 MB  (6.4%)

  Resources (arsc):          0.37 MB  (1.7%)

  Other:                     0.15 MB  (0.7%)

libapp.so 内部各包体积 Top 15

通过  --analyze-size  报告(未混淆版本)递归计算:

包名大小占比说明
package:foxwlr4,554 KB20.2%项目自身代码
package:flutter4,467 KB19.8%Flutter Framework
package:fast_gbk2,259 KB10.0%GBK 编解码映射表
@unknown1,565 KB6.9%生成代码/内部
package:highlight1,329 KB5.9%代码高亮(可优化)
@shared703 KB3.1%共享 stubs
dart:core478 KB2.1%Dart 核心库
package:pointycastle400 KB1.8%加密库(传递依赖)
dart:ui308 KB1.4%UI 引擎绑定
package:flutter_inappwebview295 KB1.3%WebView
package:flutter_localizations287 KB1.3%国际化
package:html266 KB1.2%HTML 解析
dart:io214 KB0.9%IO 库
package:image204 KB0.9%图像处理
package:flutter_html186 KB0.8%HTML 渲染

总结:优化检查清单

  1. 单 ABI 构建--target-platform android-arm + packaging.jniLibs.excludes
  2. 启用 native 库压缩useLegacyPackaging = true + extractNativeLibs="true"
  3. 代码混淆dart-obfuscation=true 写入 gradle.properties
  4. 去除重复数据 — 排查项目中嵌入的大数据表是否与依赖库重复
  5. 按需导入 — 检查 highlight、intl 等包是否加载了不需要的资源
  6. 使用分析工具--analyze-size + Python zipfile 脚本双管齐下
  7. 注意 AGP 升级的副作用 — 版本升级可能静默改变 native 库打包策略

最容易忽视且收益最大的是第 2 点。当你发现 APK 里的  .so  文件没有被压缩时,一行配置就能砍掉近一半体积。


本文基于 Flutter 3.35.0 / AGP 8.6.1 / Gradle 8.11.1 环境编写,数据来自实际项目构建。


文章来源: https://www.uedbox.com/post/119797/
如有侵权请联系:admin#unsafe.sh