HAP 从 38MB 压到 11MB:一次鸿蒙应用包体积瘦身的完整记录
包体积这件事,平时没人管,直到上线前运营给了一张图:应用市场里详情页到下载的转化,和安装包大小高度相关。我们的 HarmonyOS 应用当时是 38MB,对工具类 App 来说偏胖。花了两周做瘦身,最后到 11MB,而且首屏启动顺带快了 400ms。
这篇不复述「图片用 webp」这种结论,而是按实际动手的顺序走一遍:先建立一个能看懂的体积账本,再按收益从高到低砍,最后用启动耗时和功能回归确认没砍坏东西。文中的命令和配置基于 DevEco Studio + Stage 模型的工程结构,具体字段名请以你所用 SDK 版本为准。
一、先量一次:38MB 到底花在哪
不看账本就优化,等于闭眼删代码。HAP 本质上是个 zip,直接拆开看目录分布是最快的方式:
# 解包,按体积倒序列出目录 unzip -q entry-default-signed.hap -d hap_out du -sh hap_out/* du -ah hap_out | sort -rh | head -30
第一次拆开的结果很说明问题(总计 38.2MB):
| 目录/内容 | 体积 | 占比 |
|---|---|---|
| resources/base/media(图片、动效) | 16.4MB | 43% |
| libs/arm64-v8a(so) | 8.1MB | 21% |
| resources/rawfile(离线数据、字体) | 5.6MB | 15% |
| ets/modules.abc(ArkTS 字节码) | 4.2MB | 11% |
| 其余(配置、map、杂项) | 3.9MB | 10% |
结论很清楚:图片占了一半,so 拿了五分之一,而大家最爱讨论的「代码体积」只排第三。这也决定了后面的优化顺序——先动资源和 so,收益最快。
顺手做了个可持续的事:把这条 du 统计写进 CI,每次构建后输出体积报告并与上次对比。后面每一步的收益都是从这张表里读出来的,不靠感觉。
二、动作一:图片从 16.4MB 到 3.1MB
16.4MB 的图片里,真正的问题不是格式,而是尺寸和冗余。挑三个典型:
- 一张 1024×1024 的引导页插画,实际显示区域是 240dp;
- 同一个图标在 base、dark、xxhdpi 三个目录各存了一份 PNG;
- 三个 1.5MB 的 Lottie JSON,页面里其实只播其中一个。
处理顺序如下:
- 尺寸收敛:按目标设备的实际像素密度反推所需分辨率,插画从 1024 降到 480,单张 1.4MB → 210KB。这一步比换格式省得多,因为像素数直接减了四倍。
- 格式替换:静态大图统一转 WebP,质量 82。HarmonyOS 对 WebP 支持完整,一张 210KB 的 PNG 转完约 68KB。图标类改 SVG,矢量在任意密度下都清晰,还省掉了多套位图。
- 位图去重:按文件 MD5 扫一遍资源目录,找出内容完全相同的多份拷贝,合并成一个资源引用。这一步清掉了 2.7MB,纯属历史遗留。
- 动效按需:三个 Lottie JSON 里两个只在「首次使用引导」出现,移到按需下载的 rawfile 分片里,首包不再包含。
这里有个容易踩的点:别用脚本批量转格式就完事。WebP 对渐变和半透明阴影的处理在低质量下会有色带,插画类素材建议逐张看一遍,必要时单独提到 90。我们第一轮图省事全用 75,结果登录页的背景渐变出现明显条纹,返工了一轮。
三、动作二:字体子集化,砍掉 3.8MB
rawfile 里塞了一个完整的中文字体包,3.8MB。全量中文字体动辄十几 MB,直接打包是最常见也最亏的做法。两条路:
- 用系统字体:如果只是「想好看一点」,优先用系统提供的字体族,零成本。
- 子集化:确实需要品牌字体时,用 fonttools 只保留实际用到的字符集。
# 收集代码里出现的所有中文文案,导出字符集 subfont -i "常用文案字符.txt" custom.ttf -o custom-subset.woff2
我们是混合方案:正文和绝大多数 UI 用系统字体,只有品牌标题保留定制字体,并做了子集化(只含标题用到的 200 多个字),3.8MB → 96KB。
子集化的代价要提前想清楚:动态文案会缺字。用户昵称、评论内容这类运行时才出现的字符,一定会落到子集之外。所以子集字体只能用在「文案可控」的位置,凡是用户生成内容,一律走系统字体。这一条如果没想明白,上线后会出现零星的方框字,排查起来很痛苦。
四、动作三:so 只留一个 ABI,8.1MB → 3.4MB
8.1MB 的 so 里,一半是重复的架构。工程默认会把 arm64-v8a 和 armeabi-v7a 都打进去,而当前鸿蒙设备已经基本不需要 32 位:
// entry/build-profile.json5
{
"buildOption": {
"externalNativeOptions": {
"abiFilters": ["arm64-v8a"]
}
}
}
只这一行,体积少了 4.1MB。同时注意两件事:
- so 要 strip:Release 构建默认会去掉符号表,但如果你自己接了 CMake,确认
-s或strip步骤在流水线里真的执行了。我们有一个自研 so 因为构建脚本覆盖了参数,带了完整调试符号,单文件 2.3MB。 - so 也能按需:只有特定功能(比如 OCR、音视频解码)才用到的 so,可以考虑拆到 HSP 或动态加载,而不是全塞首包。
五、动作四:HSP 与 HAP 按需加载,砍掉 4.6MB 首包
Stage 模型给了两种拆分手段,很多人只知道 HSP 不知道 HAP 也能拆:
| 方式 | 适用场景 | 加载时机 |
|---|---|---|
| HSP(动态共享包) | 多个 HAP 共用的代码与资源 | 随宿主一起加载,不能独立安装 |
| HAR(静态共享包) | 编译期复用的工具库 | 打进调用方,不减少体积 |
| Feature HAP | 低频功能模块(设置、帮助中心、活动页) | 按需下载安装 |
我们把「活动页、帮助中心、扫描」三个低频模块拆成 Feature HAP,首包只留主流程。首包减少 4.6MB,代价是首次进入这三个入口时会有一小段下载等待——所以入口处必须有明确的加载态,不能白屏。
这里最容易被忽略的是HSP 的共享边界。把一段代码放进 HSP,只有被两个以上 HAP 引用时才真正省体积;如果只有一个 HAP 用,放进 HSP 反而多了一层包结构和一次加载开销。我们起初把只有主包用的网络库抽进 HSP,白折腾了一场,最后挪回去了。
六、动作五:字节码从 4.2MB 到 2.6MB
ArkTS 编译成 modules.abc,这部分靠三件事压:
- 开启混淆:在
build-profile.json5里启用 ArkGuard,移除无用代码、缩短标识符。我们的项目 4.2MB → 2.9MB。要注意配置-keep白名单,反射调用的类名、JSON 序列化的字段名被混淆后会直接崩,这类问题只在 Release 包出现,务必用 Release 包跑一遍主流程回归。 - 清掉调试残留:把
console.log和自研的 debug 工具模块在 Release 构建里条件编译掉。我们代码里散着 400 多处日志,光字符串常量就占了 300 多 KB。 - 去掉 sourcemap 与测试资源:确认 Release 产物里没有
.map,也没有 mock 数据和测试用例引用的 fixture 文件。这一项清出 1.1MB。
还有一个隐蔽的漏点:三方 SDK 的重复依赖。同一个网络库被三个 SDK 各自以不同版本引入,构建时没报错,产物体积却叠了三份。用 ohpm ls 或构建日志里的依赖树核对一遍,把版本对齐,能省下意料之外的一块。
七、动作六:别忽视「看不见」的配置
有两类配置项不占目录体积,却实实在在影响用户下载量:
- 压缩级别:HAP 打包本身是 zip,确认构建配置里没有把压缩关掉。我们迁移构建脚本时误留了一个「不压缩」的开关,产物比正常大了 6MB。
- 应用市场分包:华为应用市场支持按设备能力分发,把不同分辨率、不同 ABI 的资源分开投放,用户实际下载的会比本地构建产物更小。上线时把这一项打开,配合前面的 ABI 收敛,效果叠加。
八、效果汇总与验证
七个动作按收益排序如下:
| 动作 | 节省 | 改造成本 | 风险 |
|---|---|---|---|
| 图片尺寸收敛 + 格式替换 + 去重 | 13.3MB | 中 | 低(需逐张目检) |
| Feature HAP 拆分 | 4.6MB | 高 | 中(需处理下载态) |
| so 只留 arm64-v8a | 4.1MB | 低 | 低 |
| 字体子集化 | 3.7MB | 中 | 中(动态文案缺字) |
| ArkGuard 混淆 + 清调试残留 | 2.7MB | 中 | 中(反射/序列化) |
| 依赖去重 + 清测试资源 | 1.5MB | 低 | 低 |
| 确认压缩开启 | — | 低 | 低 |
最终 38.2MB → 11.3MB,降幅 70%。但瘦身不能只看体积,我们做了三项回归:
- 功能回归:全部用 Release 签名包跑,重点覆盖被混淆的反射路径、三个 Feature HAP 的首次下载、以及子集字体相关的标题页。
- 启动耗时:冷启动从 1.42s 降到 1.01s。主要收益来自首包 so 减少和图片解码量下降,不是代码变小——这点值得记住,体积和启动不是同一个指标。
- 低端机实测:在内存较小的设备上验证 Feature HAP 下载与加载,确认没有因为分片反而变慢。
九、哪几件事其实不值得做
踩过之后,有几条反常识的结论:
- 不要在项目初期就做极致分包。HAP 拆分带来的构建复杂度、下载态处理、跨包状态共享问题,会持续消耗日常开发效率。首包超过 20MB 再考虑拆。
- 不要为了体积牺牲可读性去手写混淆规则。ArkGuard 的默认规则已经能拿到大部分收益,过度定制规则带来的线上崩溃排查成本远高于省下的几百 KB。
- 不要把所有图片都转 WebP。小尺寸图标转 SVG 更划算,而需要透明通道又色彩简单的图形,PNG-8 有时反而更小。逐类判断比一刀切有效。
- 不要忽视「体积账本」本身。这次最有价值的产出其实不是那 27MB,而是 CI 里那张每次构建都会更新的体积报告——它让下一次的膨胀在发生时就可见,而不是等到上线前才发现。
最后回到那张转化率曲线:包体积从 38MB 降到 11MB 之后,详情页到下载的转化提升了大约 9 个百分点。这个数字比任何技术指标都更能说明,包体积是一项产品指标,不只是工程指标。
- 点赞
- 收藏
- 关注作者
评论(0)