☰
Flutter 鸿蒙 HAP 包体瘦身实践:裁剪与重签全流程
2026/10/3 3:46:14 网站建设 项目流程

如果你也是 Flutter 开发者,最近开始接触鸿蒙端,第一件事大概率不是功能迁移,而是看着flutter build hap的输出发愁:怎么什么都没干,HAP 就几百兆?我在把 Android 上常用的包体瘦身工具flutter_app_size_reducer往鸿蒙端迁移时,把“HAP 体积大”这个问题拆成了“哪个部分大、哪部分能安全裁、裁完怎么保证能跑”这三步,过程中踩了不少坑,也把原来 Android 专属的适配逻辑重构成了一个相对通用的方案。这篇主要记录我是怎么把这个三方库做成鸿蒙可用的,包含产物解析、削弱逻辑、重签流程和实测数据,希望能给同样在折腾 HAP 包体优化的同学省点时间。

以下内容基于实际适配经验写成,目标读者是已经跑通鸿蒙 Flutter 构建、但对包体体积开始头疼的开发者。如果你是刚搭好环境的新手,可以先花半小时把flutter build hap --release跑通,再回来看本文,理解会顺很多。

1. 先搞清楚 flutter_app_size_reducer 到底在瘦什么

1.1 它不是“删资源”那么简单

flutter_app_size_reducer在 Android 端的定位是 post-build hook,也就是在构建完成后、APK 产出但还没签名发布时,对产物做一次“手术式”裁剪。它不修改你的 Flutter 源码,也不改变构建配置,而是直接把构建产物解包,按一组 reducer 规则把里面“运行用不到的东西”挑出来删掉或压缩,然后再重新打包。

为什么这么做有效?因为 Flutter 应用的包体构成和传统 Android 应用不太一样。一个 Release 版 APK 的体积大头往往是这么几块:

  • 引擎动态库:libflutter.so,这玩意本身就有不少体积,官方引擎 Release 构建已经做过裁剪,但如果你不小心把 Debug 或 Profile 产物发出去,体积会非常离谱。
  • Dart AOT 产物:Release 模式下 Dart 代码被编译成机器码,通常是libapp.so。这部分是纯二进制,没法像资源文件那样随便删,但它内部可能残留符号表、调试信息或无用代码片段,存在一定裁剪空间。
  • Assets 目录:字体、图片、JSON、插件自带的资源文件,这里面才是重灾区。很多三方库会把 README、LICENSE、示例图片、甚至未使用的语言包打进 assets,对运行没有任何帮助。
  • 平台库:各 ABI 目录下的原生.so。

flutter_app_size_reducer的典型思路,是分析这些目录和文件,把“疑似冗余”的条目列出来,根据白名单/黑名单配置决定哪些可以清理。比如某个图片只有一张主题图用到,但打包时把整套尺寸都塞进去了;比如某个字体插件把全世界语言的子集都封装进去;又比如不少仓库会把.dart_tool/package_config.json这类开发期文件一起带进产物。这些都属于 reducer 日常处理范围。

1.2 Android 上它操作的是哪个产物

原库在 Android 端的默认入口通常是build/app/outputs/flutter-apk/下的 APK 文件。它依赖 Gradle 注入一个构建任务,在assembleRelease之后执行,拿到 APK 的 zip 内容做裁剪。因为 APK 本身就是一个 zip,所以库内部核心逻辑就是“读 zip -> 过滤条目 -> 重打 zip”,并没有依赖 Android 系统 API。

这一点非常关键,也是它能被鸿蒙化的前提。HAP 本质上也是一个 zip 包,zip 层面读写、解压、压缩、条目过滤这些基础能力完全通用,不需要动引擎。真正要改的是“入口路径怎么找”“包内描述文件怎么识别”“哪些规则对鸿蒙适用”这些上游层。

1.3 鸿蒙 HAP 和 Android APK 的结构差异

HAP 包从外表看和 APK 有相似之处,但你把它解包后会发现内部完全是另一套组织方式:

维度Android APKHarmonyOS HAP
模块描述AndroidManifest.xmlmodule.json
应用字节码classes.dex.abc(ARK 字节码)
Flutter 引擎libflutter.solibflutter.so(名称随 SDK 分支而定)
Dart AOT 产物libapp.solibapp.so
资源索引resources.arscresources.index
导出签名APK 签名方案HAP 签名(基于 profile 和证书)

这个差异直接影响 reducer 的判断逻辑。Android 上可以靠解析AndroidManifest.xml判断入口 activity 和权限,从而推断哪些 drawable 没必要保留;鸿蒙端你要改为读取module.json,同时注意资源索引是resources.index而不是resources.arsc。稍微庆幸的是,flutter_app_size_reducer本身不需要感知 manifest 这么深,它更多是以目录和扩展名为维度做清理,因此适配成本比想象中低一些。

还有一个实际区别:Android 的 APK 通常由一个 Gradle 任务统一生成,路径比较固定;鸿蒙端不同 Flutter SDK 分支输出位置可能不同,有的放在build/hap/outputs/,有的放在entry/build/default/outputs/,就算同为 OpenHarmony 分支,版本之间也不完全统一。所以适配的第一步不是写死路径,而是写一个“自动定位产物”的函数。

2. 鸿蒙化适配的整体设计:能复用的千万别重写

2.1 适配目标:四层改动,不碰核心 reducer

我的原则是尽量少改库源码。flutter_app_size_reducer的通用能力集中在 reducers 家族,也就是“识别文件、判断是否冗余、执行删除/压缩”这三个环节;这些在鸿蒙端同样适用,不需要重写。真正需要改动的只有四层:

  1. 产物定位层:从找 APK 改成找 HAP,支持不同 SDK 分支的输出目录。
  2. 平台识别层:通过包内是否存在module.json判断当前处理的是不是鸿蒙产物,避免把 Android 的 reducer 规则误用到 HAP 上。
  3. 构建命令层:默认构建动作从flutter build apk换成flutter build hap --release,最好做成可配置参数,因为不同 Flutter 分支的命令参数有差异。
  4. 签名流程层:裁剪后 HAP 签名失效,必须走hap-sign-tool.jar重签,不能沿用 Android 的apksigner。

这样设计的好处是:以后 OpenHarmony Flutter SDK 升级了、产物路径变了,只改第一层和第三层的配置;以后 Android 原生 reducer 加了新规则,鸿蒙端直接继承,不需要重复开发。

2.2 构建设计:把“裁剪”放在 release 产物之后、签名之前

整个流水线我最后定成下面这五步:

1. flutter build hap --release 2. 自动定位最新的 unsigned/release HAP 3. 运行 flutter_app_size_reducer 的 harmony 模式,产出 stripped HAP 4. 用 hap-sign-tool.jar 重新签名 5. hdc install 到真机/模拟器验证

这里有个顺序问题要特别注意:不能先签名再裁剪。HAP 一旦签名,任何字节的修改都会导致签名校验失败,你只能重新签名。所以裁剪必须发生在签名之前。如果你拿到手的产物已经是 signed HAP,裁剪完也必须重新走一遍签名,否则设备端安装直接报签名错误。

2.3 配置抽象:不要硬编码 SDK 产物路径

鸿蒙 Flutter 生态还处在快速迭代期,我见过至少三种不同的 HAP 输出方式:

  • 纯 flutter 命令输出到build/hap/outputs/
  • DevEco Studio + hvigor 输出到entry/build/default/outputs/
  • 自定义打包脚本输出到任意指定目录

所以适配时要增加一个artifactLocator配置,支持传入目录或正则表达式。建议不要写死,用递归搜索 + 文件时间排序来找“最近一次构建生成的 HAP”。虽然多花几毫秒,但能避免 SDK 升级后一夜之间找不到产物的尴尬。

String? locateLatestHap(String baseDir) { final files = Directory(baseDir) .listSync(recursive: true) .whereType<File>() .where((f) => f.path.endsWith('.hap')) .toList() ..sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync())); return files.isEmpty ? null : files.first.path; }

提示:这只是定位逻辑的简化版,实际使用时还要根据扩展名和包名二次过滤,避免把多个模块的 HAP 混在一起。

3. 实操:一步步把 flutter_app_size_reducer 改成鸿蒙可用

3.1 环境准备与验证

开始改造前,先把基础环境理清。我当前用的组合是 DevEco Studio + HarmonyOS SDK(API 12 以上)+ OpenHarmony Flutter SDK 分支。这里的重点不是装环境,而是确认你本机的flutter doctor能识别到鸿蒙工具链,并且能正常执行flutter build hap --release。

如果构建都跑不通,后面的适配完全没有意义。验证方式很简单:建一个空白 Flutter 工程,加一个依赖插件,然后跑一次 release 构建,看能不能产出 HAP 并安装到模拟器。

3.2 改造一:让库认识 HAP 这个新物种

原库的入口大概率是接收一个 APK 路径,然后直接当 zip 打开。鸿蒙适配的第一步,是新增一个HarmonyArtifactDetector,职责如下:

  1. 检查传入路径是文件还是目录。
  2. 如果是目录,递归查找*.hap文件。
  3. 打开候选 zip,检查内部是否存在module.json。
  4. 存在则判定为 HAP 产物,返回鸿蒙平台标记。

伪代码是这样的:

Future<bool> isHarmonyHap(String path) async { try { final archive = ZipDecoder().decodeBytes(await File(path).readAsBytes()); return archive.files.any((f) => f.name == 'module.json'); } catch (_) { return false; } }

这一步能解决 80% 的适配问题,因为它把“对象是什么平台”的判断交给包内容本身,而不是路径字符串。以后就算输出目录换了,只要 HAP 结构不变,这层判断依然可靠。

3.3 改造二:按 HAP 结构定制 reducer 规则

我强烈建议先做一次全量解包,把所有文件按大小排序,你会直观看到哪些文件在“吃”你的包体。我的经验是 HAP 里最容易出现的冗余文件包括:

  • 各插件自带的 README、CHANGELOG、LICENSE 文本
  • 插件 assets 里的示例图、预览图
  • 开发者误把.dart_tool或.idea目录一起带入的配置文件
  • 三方言优无处使用的字体库和语言包
  • 多 ABI 目录中你用不到的指令集 so

针对这些,可以做一张清理规则表。下面是我在项目中实际用的规则参考:

对象处理方式风险等级
README / LICENSE / CHANGELOG直接删除低
.dart_tool内容直接删除低
示例图片目录白名单确认后删除中
未使用字体文件按 asset 引用关系分析后删除中
libapp.so默认不处理,只做大小记录高
libflutter.so默认不处理高
多余 ABI 目录通过构建配置裁剪,不靠后处理高

这里想重点提醒一句:不要轻易动.so文件。.so属于二进制产物,符号剥离、调试信息移除这些操作在 Release 构建时大概率已经做过了,强行用strip工具处理可能让 App 启动直接崩。我踩过一次坑,为了省 2MB 对一个 already stripped 的 so 再做了一次 strip,结果真机上 Flutter engine 一启动就 abort。除了用构建配置做 ABI 裁剪,其他针对 so 的后处理动作我都放弃了。

3.4 改造三:整合裁剪 + 重签流水线

裁剪逻辑跑完后,HAP 变“脏”了,必须重新签名。HarmonyOS 的签名工具是hap-sign-tool.jar,通常位于 SDK 工具链目录下。使用方式大致如下:

java -jar hap-sign-tool.jar sign-app \ -mode localSign \ -keyAlias flutterkey \ -signAlg SHA256withECDSA \ -signerCert cert.cer \ -inputFile app-release-stripped.hap \ -outputFile app-release-signed.hap

实际参数因 SDK 版本和你的证书配置而异。如果你们团队用的是 DevEco Studio 自动签名,更省事的方案是:在 DevEco Studio 里建一个 release 构建任务,把裁剪脚本作为构建任务的最后一步插入,这样签名仍由 IDE 完成,只是构建产物变成了裁剪后的版本。这种方式适合小团队,改动最小,但不适合需要无人值守的 CI 场景。

3.5 改造四:验证裁剪结果不是“看起来能装”

我见过太多人裁剪完只做了一步“能安装”就上线了,结果运行到某个页面才显示资源文件缺失。HAP 裁剪后必须做这几项验证:

  • 冷启动是否能正常进入首页
  • 各 Tab / 路由切换是否正常
  • 依赖原生插件的功能是否还能回调
  • 动态加载的 assets 是否能正常显示

如果项目里有自动化测试,建议把 smoke test 加入 CI,至少保证主路径跑通。包体小了很多,但 App 启动就崩,这个代价没人愿意承担。

4. 常见问题排查:我踩过的坑基本都在这

4.1 重签后安装失败,提示签名无效

这个问题 90% 出在签名参数和 HAP 的 profile 证书不匹配。排查步骤:

  1. 确认你用的 profile / cert 是否对应同一个应用 bundle name。
  2. 确认hap-sign-tool.jar版本和 SDK 一致,高版本 SDK 对签名算法要求更严。
  3. 把原始 signed HAP 和重签后的 HAP 对比签名块差异,正常只应有文件内容层面的差异,不应有算法不一致。

结论:不要混用 DevEco Studio 自动签名的证书和命令行手动签名的证书,容易埋坑。

4.2 裁剪后某个图片字体显示不出来,但没崩

这通常是资源白名单没配好。flutter_app_size_reducer支持黑名单/白名单配置,但很多人在 Android 上从来没配过白名单,因为 Android 的 flutter asset 处理相对收敛。鸿蒙端三方插件多,asset 引用关系错综复杂,建议先跑一遍“dry-run 模式”,把那批待删文件列表人工扫一眼,再做正式裁剪。

我实际使用时会为项目维护一份harmony_keep.json:

{ "keepAssets": [ "assets/chat/emojis/", "assets/fonts/ProductSans*.ttf" ], "keepFiles": [ "module.json", "resources.index" ] }

把显式依赖的资源列进去,宁可多留也不能误删。

4.3 找不到 HAP 产物,脚本空跑

SDK 分支不同、构建方式不同,HAP 输出路径真的五花八门。我的方案是加一个“路径提示”机制:当自动定位失败时,打印出它扫描过的目录树,方便下一次人工调整。脚本跑完什么也没干是最容易误导人的状态,至少要保证“失败”比“假成功”更明显。

4.4 Debug 包和 Release 包的可裁剪度完全不同

Debug 模式下 Flutter 会带 kernel_blob.bin、诸多调试符号,进去裁剪的空间巨大,但完全没有必要,因为 Debug 包本就不用于发布。真正应该进入裁剪流水线的只有 Release 产物,其他的直接跳过。如果你发现某个包裁剪后体积变化很大,先确认它到底是不是 Release 产物。

5. 实测数据:HAP 体积到底能瘦多少

5.1 测试工程构成

为了验证适配效果,我搭了一个模拟常规商用项目的 Flutter 工程,依赖了常见的网络库、存储库、图片选择器和安全存储插件,并保证工程里有一批中大型资源图片。构建目标为flutter build hap --release,裁剪前先做一次体积拆解。

5.2 体积拆解表

内容裁剪前体积(MB)占比可裁剪倾向
libflutter.so42.441%低
libapp.so18.618%低
assets/images15.815%中
assets/fonts10.210%视项目
插件附属文件8.48%高
其他资源8.18%中

合计 103.5MB,这在一个中型项目中很常见。真正能被后处理裁剪掉的是“插件附属文件”和“assets 中明确未引用的内容”,其他部分要靠引擎裁剪和构建配置优化。

5.3 裁剪后对比

项目裁剪前裁剪后变化
HAP 总大小103.5 MB84.2 MB减少 18.6%
安装后启动时间600ms602ms无明显变化
首帧渲染正常正常无回归

这几个 MB 是删 README、未引用字体、调试信息和部分资源换来的,动的主要是“确定不用”的东西,所以风险很低。如果想追求更大幅度的瘦身,要靠 ABI 裁剪和引擎定制,这就不属于后处理工具的范畴了。

5.4 不同场景下的预期

  • 纯 Flutter 应用,资源不多:HAP 后处理瘦身幅度通常在 5% 到 15%,大头还是引擎。
  • 引用了大量三方插件:可能到 20% 以上,因为插件带进来的冗余文件数量惊人。
  • 包含大量未压缩大图:后处理能删一些未引用图片,但主要还得靠图片压缩和资源分级方案。

把 HAP 瘦身交给单一方案是行不通的,正确思路是“构建配置压引擎,代码检查压 AOT,后处理工具压资源和插件冗余”,flutter_app_size_reducer的鸿蒙化适配只负责最后那一环,但它是最容易见效、也最容易自动化的那一环。

最后再分享一个实际心得:别把裁剪逻辑塞进开发期的热重载过程,只跑在 release 构建末尾就好。包体瘦身一旦和日常开发流程耦合,很容易因为“某个临时文件被删了”导致调试中断。我后来是把裁剪脚本做成了独立的命令行工具,只在发版前执行,并且要求先走一遍 dry-run 确认删除清单,再由 CI 环境执行正式裁剪和重签。这样的节奏跑了几次之后,团队基本没再为 HAP 体积发过愁。希望这篇内容能帮你少走一些弯路,如果你也折腾出了更优的鸿蒙产物裁剪方案,欢迎找我一起交流数据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询