HarmonyOS应用《民族图鉴》开发第67篇:包体积优化——应用瘦身的艺术与科学

📖 引言
在应用商店里,两个功能类似的App,一个10MB,一个100MB,你会下载哪个?大多数用户会选择更小的那个。包体积不仅仅是数字,它直接影响着下载转化率、安装时间、存储空间,甚至用户的卸载意愿。
对于「民族图鉴」这样的工具类应用来说,包体积尤为重要。用户下载它是为了快速查询民族信息,而不是为了等待一个几百MB的安装包下载完成。如果包体积太大,用户可能在下载页面就放弃了;即使下载了,也可能因为占用空间太多而卸载。
你可能会问:我的代码没写多少,为什么安装包这么大?图片资源到底占了多少空间?混淆和压缩真的有用吗?WebP格式能省多少?什么是Tree Shaking?资源分包又是怎么回事?
这些问题都是包体积优化的核心。很多开发者对包体积的认知停留在"压缩图片"这个层面,但实际上,包体积优化是一个系统工程——从代码到资源,从构建配置到分发策略,每个环节都有优化空间。
本文将从鸿蒙安装包的构成讲起,全面介绍包体积优化的各种手段:代码优化、资源优化、图片优化、字体优化、按需加载、资源分包。结合「民族图鉴」项目的实际情况,带你完成一次完整的应用瘦身之旅。
🎯 学习目标
完成本文后,你将能够:
- ✅ 理解为什么要优化包体积以及它对业务的影响
- ✅ 掌握鸿蒙安装包(HAP/APP)的构成与体积分布
- ✅ 学会代码优化手段:Tree Shaking、混淆、压缩、按需引入
- ✅ 掌握资源优化方法:图片压缩、WebP格式、资源删除、资源混淆
- ✅ 学会图片优化三部曲:尺寸、格式、质量
- ✅ 了解字体优化:子集化、按需加载
- ✅ 理解按需加载与动态特性的使用场景
- ✅ 掌握资源分包与按需下载策略
- ✅ 能够在「民族图鉴」项目中实践完整的包体积优化
- ✅ 能够解决优化过程中的常见问题
💡 需求分析
为什么要优化包体积?
1. 提升下载转化率
应用商店的数据显示:包体积每增加1MB,下载转化率就会下降一定比例。对于新用户来说,下载是第一道门槛。
影响因素:
- 网络环境:移动网络下,用户更不愿意下载大包
- 等待时间:下载越久,用户放弃的概率越高
- 心理预期:用户对不同类型的App有不同的体积预期
对于「民族图鉴」这类工具型应用,用户的心理预期可能是"几十MB以内"。如果超过这个预期,转化率就会明显下降。
2. 减少安装时间
包体积越大,安装时间越长。用户点击"安装"后等待越久,体验越差。尤其是在低端设备上,安装速度本身就慢,大包体会让这个问题雪上加霜。
3. 节省存储空间
手机存储空间是有限的。很多用户会定期清理不常用的App来释放空间。如果你的App体积大、使用频率又不高,很容易成为被清理的对象。
「民族图鉴」虽然不是高频应用,但如果体积足够小,用户更愿意保留它。
4. 降低分发成本
虽然这一点对开发者来说可能不那么直观,但包体积小确实能降低CDN带宽成本。对于用户量大的应用,这是一笔不小的开支。
鸿蒙安装包的构成
要优化包体积,首先要知道安装包里有什么。
鸿蒙应用的安装包主要有两种形式:
- HAP(HarmonyOS Ability Package):模块级安装包
- APP(Application Package):应用级安装包,包含多个HAP
一个典型的HAP包结构:
HAP包
├── ets/ # 代码文件(编译后)
├── resources/ # 资源文件
│ ├── base/ # 默认资源
│ │ ├── element/ # 元素资源(字符串、颜色等)
│ │ ├── media/ # 媒体资源(图片等)
│ │ └── profile/ # 配置文件
│ ├── rawfile/ # 原始文件(保持原样)
│ ├── zh_CN/ # 中文资源
│ ├── en/ # 英文资源
│ └── dark/ # 深色模式资源
├── libs/ # 原生库(.so文件)
├── module.json5 # 模块配置
└── pack.info # 打包信息
体积占比通常是:
- 资源文件:50%~80%(图片占大头)
- 代码文件:10%~30%
- 原生库:0%~20%(如果有C++库的话)
- 其他:5%左右
对于「民族图鉴」这样的应用,图片资源(尤其是56个民族的封面图)很可能是体积最大的部分。
「民族图鉴」包体积现状分析
让我们看看「民族图鉴」的资源情况:
-
图片资源:
- 56个民族封面图(rawfile/coverImage/,每张约100-300KB)
- 启动图标、Logo等(resources/base/media/)
- 背景图、前景图等
-
代码文件:
- 页面组件(15个页面)
- 服务层(8个服务)
- 模型层、工具类等
-
资源文件:
- 多语言资源(中文、英文)
- 深色模式资源
- 字符串、颜色、尺寸等
粗略估算,如果56张封面图每张平均200KB,光是图片就有56 × 200KB ≈ 11MB。再加上代码和其他资源,整个App可能在15-20MB左右。对于工具类应用来说,这个体积不算小,但也不算大。不过,我们仍然有很大的优化空间。
🔧 核心实现
一、代码优化
代码优化是包体积优化的基础。虽然代码通常不是体积最大的部分,但优化代码不仅能减小包体积,还能提升运行时性能。
1. Tree Shaking(摇树优化)
Tree Shaking是一种通过静态分析移除未使用代码的技术。就像摇树一样,把枯叶(未使用的代码)摇掉,只保留有用的部分。
原理:
基于ES Module的静态结构(import/export),在编译时分析哪些代码被使用了,哪些没有,然后将未使用的代码删除。
ArkUI中的Tree Shaking:
ArkUI的构建工具默认开启了Tree Shaking,但我们需要遵循一些最佳实践才能让它发挥最大效果。
最佳实践:
// ✅ 好:按需导入,只导入需要的部分
import { EthnicGroup } from '../models/EthnicModels';
// ❌ 不好:导入整个模块
import * as EthnicModels from '../models/EthnicModels';
// ✅ 好:使用具名导出,便于Tree Shaking分析
export function formatPopulation(pop: string): string {
// ...
}
export function getRegionList(): string[] {
// ...
}
// ❌ 不好:导出整个对象,无法单独移除
export default {
formatPopulation,
getRegionList
}
注意事项:
- 尽量使用ES Module的import/export语法
- 避免副作用代码(模块顶层的函数调用、全局变量修改等)
- 第三方库也要选择支持Tree Shaking的
2. 代码混淆
代码混淆是将代码中的类名、方法名、变量名等替换成短而无意义的名字,从而减小代码体积。
在「民族图鉴」项目中,已经有混淆配置文件(obfuscation-rules.txt),但在build-profile.json5中默认是关闭的:
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false, // 默认关闭
"files": [
"./obfuscation-rules.txt"
]
}
}
}
}
开启混淆:
将enable改为true即可开启混淆。但在此之前,我们需要配置好混淆规则,避免把不能混淆的代码也混淆了。
混淆规则配置(obfuscation-rules.txt):
# 开启属性混淆
-enable-property-obfuscation
# 开启顶层混淆
-enable-toplevel-obfuscation
# 开启文件名混淆
-enable-filename-obfuscation
# 开启导出混淆
-enable-export-obfuscation
# 移除console.log等调试代码
-remove-log
# 代码压缩(移除空格、换行等)
-compact
# 保留入口类名(根据实际情况配置)
-keep-global-name EntryAbility
-keep-global-name EntryBackupAbility
混淆的效果:
- 代码体积通常能减少20%~50%
- 同时也有一定的代码保护作用(反编译更难)
注意事项:
- 反射调用的类名/方法名不能混淆
- 序列化/反序列化的对象属性不能混淆
- 与原生交互的接口不能混淆
- 开启混淆后一定要充分测试
3. 代码压缩
代码压缩是指移除代码中的空白字符、注释、换行等,让代码变得更紧凑。
// 压缩前
function calculateTotal(items: Item[]): number {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price;
}
return total;
}
// 压缩后
function calculateTotal(a){let b=0;for(let c=0;c<a.length;c++)b+=a[c].price;return b}
在ArkUI中,release构建默认会进行代码压缩。我们也可以在混淆配置中通过-compact选项来启用。
4. 按需引入
按需引入是指只引入需要的模块,而不是整个库。
示例:工具函数按需引入
// ❌ 不好:引入整个lodash(假设存在)
import _ from 'lodash';
// ✅ 好:只引入需要的函数
import { debounce, throttle } from 'lodash';
在「民族图鉴」项目中,大部分代码都是自己写的,所以主要注意内部模块的按需引入即可。
「民族图鉴」实战:代码优化
让我们看看「民族图鉴」中可以优化的地方:
1. 开启混淆
修改build-profile.json5,在release构建中开启混淆:
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
}
2. 更新混淆规则
在obfuscation-rules.txt中添加必要的保留规则:
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
-remove-log
-compact
# 保留数据模型的属性名(避免序列化问题)
-keep-property-name EthnicGroup
-keep-property-name AIMessage
-keep-property-name MusicItem
-keep-property-name QuizQuestion
-keep-property-name UserProfile
# 保留枚举值
-keep-global-name AppLanguage
-keep-global-name TTSState
3. 检查import语句
确保所有import都是按需导入的,而不是导入整个模块。
二、资源优化
资源通常是包体积的"大头",尤其是图片资源。优化资源往往能带来最显著的体积 reduction。
1. 图片压缩
图片压缩是最直接、效果最明显的优化手段。
图片压缩的两个维度:
- 尺寸压缩:减小图片的像素尺寸(宽×高)
- 质量压缩:在保持尺寸不变的前提下,降低图片质量
压缩工具推荐:
- 在线工具:TinyPNG、Squoosh
- 本地工具:ImageOptim、XnConvert
- 命令行:sharp、imagemin
压缩原则:
- 在可接受的视觉质量下,尽可能压缩
- 不同类型的图片采用不同的压缩策略
- 不要过度压缩,影响用户体验
2. 选择合适的图片格式
不同的图片格式有不同的特点,选择合适的格式能在相同质量下得到更小的体积。
常见格式对比:
| 格式 | 透明度 | 动画 | 压缩率 | 适用场景 |
|---|---|---|---|---|
| PNG | ✅ | ❌ | 中 | 图标、透明背景图 |
| JPEG | ❌ | ❌ | 高 | 照片、复杂图片 |
| WebP | ✅ | ✅ | 很高 | 大多数场景 |
| SVG | ✅ | ✅ | - | 矢量图标 |
WebP格式:
WebP是Google推出的图片格式,支持有损和无损压缩,支持透明度和动画。在相同视觉质量下,WebP比JPEG小25%35%,比PNG小26%34%。
鸿蒙系统支持WebP格式,所以在「民族图鉴」中,我们可以考虑将民族封面图转换为WebP格式。
3. 图片优化三部曲
让我们总结一下图片优化的标准流程:
第一步:压缩尺寸
先问自己:这张图真的需要这么大吗?
- 图标:按照设计规范,通常是24x24、32x32、48x48等
- 列表缩略图:根据列表项大小决定,通常不超过400x400
- 详情页大图:根据屏幕尺寸决定,通常不超过1080p
对于「民族图鉴」的56张民族封面图,我们需要确认:
- 这些图片在哪些地方使用?
- 最大显示尺寸是多少?
- 当前图片尺寸是多少?有没有超大?
如果封面图原本是2000x2000的,但实际最大显示尺寸是400x400,那我们就可以把图片尺寸缩小到800x800(考虑2x屏),体积会大幅减小。
第二步:选择格式
根据图片内容选择最合适的格式:
- 照片类(民族封面图):JPEG或WebP(有损)
- 图标类(小图标、Logo):PNG或WebP(无损)
- 矢量图:SVG(如果支持的话)
第三步:平衡质量与体积
找到"质量可接受、体积最小"的那个平衡点。
怎么找?可以做对比测试:
- 用不同的质量参数导出图片
- 对比视觉效果和文件大小
- 选择质量下降不明显但体积下降明显的那个点
对于JPEG,通常质量参数在70-85之间是性价比最高的。
对于WebP,通常质量参数在60-80之间效果不错。
4. 删除未使用的资源
项目中经常会有一些不再使用的资源文件,比如旧版本的图片、测试用的资源等。这些"僵尸资源"白白占用着包体积。
怎么找未使用的资源?
- 手动检查:搜索资源引用
- 工具检查:使用构建工具的分析功能
- 代码审查:定期清理
对于「民族图鉴」,我们可以检查:
- resources/media/ 目录下的图片是否都被引用了
- rawfile/ 目录下的文件是否都在使用
- 不同语言/分辨率的资源是否有冗余
5. 资源混淆
资源混淆是将资源文件的路径和名字替换成短名字,从而减小资源索引表的体积。
比如:
resources/base/media/ic_search.png→r/a/b.png
资源混淆对资源本身的体积没有影响,但能减小资源索引表和代码中资源引用的体积。对于资源很多的应用,效果还是比较明显的。
在ArkUI中,可以通过构建配置来开启资源混淆。
「民族图鉴」实战:图片优化
「民族图鉴」的图片资源主要是56个民族的封面图,位于entry/src/main/resources/rawfile/coverImage/目录下。
优化步骤:
1. 统一图片尺寸
先确认封面图的使用场景:
- 详情页头部:宽度约为屏幕宽度,高度约200vp
- 列表项:不使用封面图(用首字图标代替)
所以封面图的最大显示宽度大约是屏幕宽度(比如1080px)。我们可以将所有封面图的宽度统一调整为1080px(高度按比例缩放)。
2. 转换为WebP格式
将所有JPG封面图转换为WebP格式,质量设置为75左右。
预期效果:
- JPG → WebP,体积减少约30%~50%
- 56张图,每张平均从200KB降到100KB,总共减少约5.6MB
3. 重命名文件(可选)
当前文件名是01_han.jpg、02_zhuang.jpg这样的格式。如果数量很多,可以考虑更短的命名,比如01.webp、02.webp。不过对于56个文件来说,这个优化收益很小,可以不做。
三、字体优化
字体文件通常比较大,中文字体尤其如此。如果应用中使用了自定义字体,字体优化就很重要。
「民族图鉴」目前使用的是系统字体,所以这部分可能不是重点。但如果未来要添加自定义字体,就需要考虑优化。
1. 字体子集化
中文字体动辄几MB甚至几十MB,但我们实际用到的字可能只有几千个。字体子集化就是从完整字体中提取出我们需要的字符,生成一个更小的字体文件。
子集化工具:
- fontmin
- 百度字体编辑器
- 在线字体子集化工具
子集化的两种策略:
- 静态子集化:提前确定需要的字符,生成固定的子集字体
- 动态子集化:根据用户输入动态加载需要的字体片段
对于「民族图鉴」,如果要使用自定义字体,可以提取所有民族名称、常用UI文字等字符,生成一个子集字体。
2. 按需加载字体
如果有多种字体(比如正文用一种,标题用一种),可以只加载当前页面需要的字体,而不是全部加载。
不过在鸿蒙应用中,字体通常是打包在安装包里的,所以按需加载更多是指"不把不用的字体打进包里"。
四、按需加载与动态特性
1. 什么是按需加载?
按需加载,顾名思义就是"需要的时候才加载"。传统方式是把所有代码和资源都打进安装包,用户安装后就能用所有功能。按需加载则是把非核心功能拆分出去,用户用到的时候才下载。
好处:
- 减小安装包体积
- 用户按需下载,节省流量
- 核心功能秒开,体验更好
2. 鸿蒙的动态特性
鸿蒙系统支持动态特性(Dynamic Feature),可以将某些功能模块做成动态HAP,用户需要时才下载安装。
适用场景:
- 使用频率低的功能(比如「民族图鉴」的AI聊天)
- 体积大的功能(比如视频播放、AR功能)
- 可选的高级功能(比如Pro版功能)
对于「民族图鉴」来说,可以考虑做成动态特性的功能:
- AI聊天功能(依赖大模型,可能体积较大)
- 音乐播放功能(如果有很多音乐资源的话)
- 知识问答功能(如果数据量很大)
不过,动态特性也有缺点:
- 用户使用需要等待下载
- 实现复杂度更高
- 需要服务器支持分发
所以是否使用动态特性,需要根据业务情况权衡。
3. 资源分包
资源分包是将资源按功能模块拆分,只下载当前需要的资源。
比如「民族图鉴」的56个民族封面图,如果用户只查看几个民族,理论上只需要下载这几个民族的图片就够了。但实际上,因为图片太多了,逐个下载反而可能影响体验。所以是否做资源分包,要根据具体情况判断。
资源分包的两种方式:
- 按模块分包:每个功能模块的资源单独打包
- 按质量分包:高清/标清资源分开,根据设备情况下载
五、包体积分析工具的使用
在优化之前,我们需要知道包体积到底花在哪里了。工欲善其事,必先利其器。
1. 构建产物分析
最直接的方法就是看构建产物。
查看HAP包内容:
HAP包本质上是一个ZIP文件,可以直接解压查看:
# 1. 找到构建产物
# entry/build/default/outputs/default/entry-default-signed.hap
# 2. 复制一份,改后缀为.zip
cp entry-default-signed.hap entry-default.zip
# 3. 解压
unzip entry-default.zip -d entry-content/
# 4. 查看各目录大小
du -sh entry-content/*
这样就能看到每个目录占用了多少空间,找到体积最大的部分。
2. DevEco Studio 构建分析
DevEco Studio 内置了构建分析功能。
使用方法:
- 执行构建(Build → Build HAP(s)/APP(s))
- 打开构建产物目录
- 找到构建报告(build-report.html)
- 用浏览器打开,查看详细的体积分析
构建报告包含:
- 总体积统计
- 各模块体积占比
- 资源文件列表及大小
- 代码体积分析
- 体积变化趋势(和上一个版本对比)
3. 第三方工具
除了官方工具,还有一些第三方工具可以用来分析包体积:
| 工具 | 用途 | 特点 |
|---|---|---|
| Android Studio APK Analyzer | APK分析 | 功能强大,可视化好 |
| 7-Zip / WinRAR | 压缩包查看 | 简单直接,看文件列表 |
| du / TreeSize | 目录大小统计 | 快速看哪些文件大 |
| 在线图片压缩工具 | 图片优化 | TinyPNG、Squoosh等 |
4. 体积分析的步骤
标准分析流程:
1. 构建Release包
↓
2. 解压,看总体积分布
↓
3. 找到最大的目录/文件类型
↓
4. 深入分析该类文件
↓
5. 制定优化计划
重点关注:
- 哪些资源文件最大?
- 有没有重复的资源?
- 图片格式是否最优?
- 代码有没有冗余?
- 有没有未使用的资源?
「民族图鉴」体积分析示例
假设我们对「民族图鉴」做一次体积分析,结果可能是这样的:
总大小:25MB
├── resources/rawfile/ 12MB (48%) —— 56张封面图
├── resources/base/ 3MB (12%) —— 图标、背景图等
├── ets/ 5MB (20%) —— 代码
├── libs/ 3MB (12%) —— 原生库
└── 其他 2MB (8%) —— 配置文件等
结论很明显:图片是最大的优化点,占了将近一半的体积。优化图片,ROI最高。
💡 分析工具的价值:
不要凭感觉猜哪里体积大,用工具测一下。
有时候你以为代码是大头,结果发现图片占了70%。
找到最大的那块,优化它,收益最大。
六、实战:「民族图鉴」56张封面图WebP转换全记录
理论说了这么多,让我们来一次实战:把「民族图鉴」的56张民族封面图从JPG转换成WebP,看看效果到底如何。
第一步:准备工作
原始文件信息:
- 位置:
entry/src/main/resources/rawfile/coverImage/ - 数量:56张
- 格式:JPG
- 命名:01_han.jpg, 02_zhuang.jpg … 56_hasake.jpg
- 预估单张大小:150-300KB
- 预估总体积:约12MB
工具选择:
- 在线工具:Squoosh(https://squoosh.app/)- 适合少量图片
- 命令行:cwebp / sharp - 适合批量处理
- 图形工具:XnConvert、ImageOptim - 适合可视化操作
这里我们用命令行方式,因为有56张图,批量处理效率高。
第二步:批量转换脚本
使用 Node.js + sharp 库做批量转换:
// 安装sharp
// npm install sharp --save-dev
// convert-to-webp.ts
import * as fs from 'fs';
import * as path from 'path';
import sharp from 'sharp';
const inputDir = './rawfile/coverImage/';
const outputDir = './rawfile/coverImage-webp/';
const quality = 75; // WebP质量参数
// 确保输出目录存在
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 获取所有jpg文件
const files = fs.readdirSync(inputDir).filter(f =>
f.endsWith('.jpg') || f.endsWith('.jpeg') || f.endsWith('.png')
);
console.log(`找到 ${files.length} 张图片,开始转换...`);
let totalOriginalSize = 0;
let totalWebpSize = 0;
// 批量转换
async function convertAll() {
for (const file of files) {
const inputPath = path.join(inputDir, file);
const outputPath = path.join(outputDir,
file.replace(/\.(jpg|jpeg|png)$/i, '.webp')
);
// 原始文件大小
const originalSize = fs.statSync(inputPath).size;
totalOriginalSize += originalSize;
// 转换
await sharp(inputPath)
.webp({ quality: quality })
.toFile(outputPath);
// 转换后文件大小
const webpSize = fs.statSync(outputPath).size;
totalWebpSize += webpSize;
const ratio = ((1 - webpSize / originalSize) * 100).toFixed(1);
console.log(`${file}: ${(originalSize/1024).toFixed(1)}KB → ${(webpSize/1024).toFixed(1)}KB (减少${ratio}%)`);
}
console.log('\n=== 转换完成 ===');
console.log(`原始总大小: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`);
console.log(`WebP总大小: ${(totalWebpSize / 1024 / 1024).toFixed(2)}MB`);
console.log(`减少了: ${((1 - totalWebpSize / totalOriginalSize) * 100).toFixed(1)}%`);
}
convertAll();
运行脚本:
npx ts-node convert-to-webp.ts
第三步:查看转换结果
假设运行后的输出是这样的:
找到 56 张图片,开始转换...
01_han.jpg: 220.5KB → 105.2KB (减少52.3%)
02_zhuang.jpg: 185.3KB → 88.7KB (减少52.1%)
03_hui.jpg: 210.1KB → 98.3KB (减少53.2%)
...
56_hasake.jpg: 195.6KB → 92.1KB (减少52.9%)
=== 转换完成 ===
原始总大小: 11.25MB
WebP总大小: 5.38MB
减少了: 52.2%
效果分析:
- 56张图从11.25MB减少到5.38MB
- 减少了5.87MB,约52.2%
- 平均每张图从200KB降到96KB
- 整体包体积从25MB降到约19MB,减少了24%
这个效果非常显著!而且是用很小的开发成本换来的。
第四步:质量检查
体积减下来了,但质量怎么样?会不会变得很模糊?
质量检查方法:
- 肉眼对比:在手机上实际查看,看能不能看出区别
- 放大对比:把图片放大,看细节损失多少
- 盲测:找几个人来猜哪张是原图哪张是WebP,看能不能猜出来
建议的质量参数:
- 质量参数 75-80:大多数场景肉眼看不出区别
- 质量参数 65-70:体积更小,细看有轻微差异
- 质量参数 60以下:差异明显,不建议使用
对于「民族图鉴」的封面图,75的质量参数应该是比较合适的——体积减少一半,质量损失不明显。
第五步:代码适配
图片格式换了,代码里引用的地方也要改:
// 原来
const imagePath = `coverImage/${id}_${pinyin}.jpg`;
// 改成WebP
const imagePath = `coverImage/${id}_${pinyin}.webp`;
如果担心兼容性问题(比如要支持很老的系统版本),可以做降级处理:
// 先尝试加载WebP,失败则加载JPG
Image(this.webpSupported ? webpPath : jpgPath)
.onError(() => {
// WebP加载失败,降级到JPG
this.currentImagePath = jpgPath;
})
不过鸿蒙系统对WebP的支持很好,一般不需要降级。
第六步:验证效果
转换完成后,做一轮完整测试:
- 列表页图片加载是否正常
- 详情页大图显示是否正常
- 图片清晰度是否可接受
- 包体积是否确实减少了
- 有没有其他功能受影响
确认一切正常,优化就完成了!
💡 WebP转换的经验总结:
- 先转几张试试看效果,再批量转
- 质量参数不是越低越好,找到平衡点
- 转换后一定要在真机上看效果
- 记得更新代码里的文件后缀
- 做好版本管理,万一要回滚也方便
七、实战:「民族图鉴」包体积优化全记录
让我们为「民族图鉴」制定一个完整的包体积优化方案。
第一步:分析现状
在优化之前,我们需要知道当前的体积分布:
- 总共有多大?
- 代码占多少?资源占多少?
- 哪些资源最大?
可以通过构建分析工具来查看。
第二步:制定优化计划
根据现状分析,制定优化优先级:
| 优先级 | 优化项 | 预期收益 | 难度 |
|---|---|---|---|
| 高 | 图片压缩+WebP转换 | 30%~50%图片体积减少 | 低 |
| 高 | 开启代码混淆 | 20%~30%代码体积减少 | 中 |
| 中 | 图片尺寸优化 | 视情况而定 | 低 |
| 中 | 删除未使用资源 | 视情况而定 | 低 |
| 低 | 资源混淆 | 5%左右 | 中 |
| 低 | 动态特性 | 视拆分情况 | 高 |
第三步:具体实施
1. 图片优化
- 检查56张封面图的尺寸,统一到合适大小
- 将封面图转换为WebP格式,质量75
- 检查其他图片资源,进行压缩优化
2. 代码优化
- 在release构建中开启混淆
- 配置混淆规则,保留必要的类名和属性名
- 开启-remove-log移除调试代码
- 开启-compact压缩代码
3. 资源清理
- 检查所有资源文件,删除未使用的
- 检查rawfile目录,确认所有文件都在使用
4. 构建配置优化
- 开启资源混淆
- 检查构建配置,确保所有优化开关都已开启
第四步:验证效果
优化完成后,对比优化前后的体积:
- 总包体积减少了多少?
- 各部分减少了多少?
- 功能是否正常?(一定要充分测试)
预期效果
保守估计,经过以上优化,「民族图鉴」的包体积应该能减少30%以上。如果图片优化做得好,可能减少40%~50%。
📦 动态特性与按需加载
前面讲的都是静态的优化——把包做小。但有些功能,用户可能根本用不上,为什么要让他们下载呢?这就是动态特性和按需加载的思路。
什么是动态特性
简单说就是:把应用拆成多个模块,核心模块必须下载,非核心模块用户用到了再下载。
举个例子:
「民族图鉴」有答题、音乐、AI聊天这些功能。有些用户只用来看民族介绍,从来不用答题、不听音乐、不跟AI聊天。那这些功能对他们来说就是浪费空间。
如果把答题、音乐、AI聊天做成动态特性:
- 用户下载App时,只下载核心功能(首页、列表、详情)
- 用户点进答题页时,提示"需要下载答题模块,是否下载?"
- 用户同意后,再下载答题模块
- 下载完了直接进入答题页
这样一来,初始安装包可以小很多。
动态特性的适用场景
不是所有功能都适合做动态特性。适合的场景:
适合做动态特性的:
- 使用频率低的功能(比如设置里的高级功能)
- 体积大的功能(比如AR、3D模型)
- 可选的功能(比如用户可以选择装不装)
- 特定人群的功能(比如VIP专属功能)
不适合做动态特性的:
- 核心功能(用户一打开就要用的)
- 使用频率高的功能
- 启动时就需要的功能
- 体积很小的功能(下载的时间都比省的空间不值)
对于「民族图鉴」来说:
- 答题功能 → 可以考虑做动态特性
- 音乐功能 → 可以考虑做动态特性
- AI聊天功能 → 可以考虑做动态特性
- 民族列表/详情 → 核心功能,不能做
动态特性的优缺点
优点:
- 初始安装包更小
- 用户只下载需要的功能
- 可以按需发布新功能
- 灵活度高
缺点:
- 实现复杂度高
- 用户体验有影响(要等下载)
- 需要处理下载失败、网络不好等情况
- 测试更复杂
所以要不要做动态特性,要根据实际情况权衡。对于「民族图鉴」这种体量的应用,可能暂时不需要。但了解这个思路是有好处的。
按需加载的其他思路
不一定非要用系统的动态特性能力,代码层面也可以做按需加载。
路由懒加载:
页面级别的按需加载,进入这个页面时才加载对应的代码。
组件懒加载:
不常用的组件,用到时再加载。比如弹窗里的复杂组件,默认不加载,弹窗打开时再加载。
图片懒加载:
这个我们之前讲过,列表里的图片,滚动到可视区域时再加载。
数据懒加载:
不是一进页面就把所有数据都拉下来,而是需要时再拉。比如详情页的评论,默认不加载,用户滑到评论区时再加载。
这些按需加载的思路,既能提升性能,也能减少初始加载的资源消耗。
🗂️ 资源分包策略
除了动态特性,资源分包也是减小初始包体积的常用手段。简单说就是:把资源分成好几份,核心资源打包在一起,其他资源单独存放。
为什么要做资源分包
场景:
「民族图鉴」有56个民族的详情页,每个民族都有几张大图。全部加起来可能有几十MB。但用户一次只会看几个民族,不会56个全看完。
如果把所有图片都打在主包里:
- 安装包很大
- 用户下载慢
- 很多图片用户可能根本不会看
如果做资源分包:
- 主包里只放首页和列表需要的小图
- 详情页的大图单独打包
- 用户进入某个民族详情时,再下载对应民族的大图
这样初始包体积就小很多了。
资源分包的方式
按页面分:
每个页面的资源单独打包。进入页面前下载对应资源。
按模块分:
按功能模块打包,比如答题模块的资源、音乐模块的资源。
按质量分:
低清图打在主包,高清图单独放。用户需要时再下载高清版。
「民族图鉴」的分包建议:
- 主包:封面小图、图标、公共资源
- 详情大图包:56个民族的详情大图
- 音乐资源包:各民族的背景音乐
- 答题资源包:答题相关的图片和音频
资源分包的注意事项
-
加载时机:要提前预判用户需要什么,在用户用到前就开始加载
- 比如用户在列表页浏览时,可以预加载前几个详情页的图片
- 比如WiFi环境下,可以静默预加载更多资源
-
降级处理:资源加载失败怎么办?
- 显示占位图
- 提供重试按钮
- 网络不好时提示用户
-
缓存策略:下载过的资源要缓存起来,不要每次都重新下载
- 内存缓存
- 磁盘缓存
- 缓存有上限,定期清理
-
用户体验:不要让用户觉得"怎么老是在加载"
- 加载要有进度提示
- 能静默加载的就静默加载
- 核心路径不要让用户等
💡 关于分包的建议:
对于「民族图鉴」这样的应用,初期可以先不做资源分包。
先把静态优化做好(图片转WebP、压缩、代码混淆等)。
如果后续包体积还是太大,再考虑动态特性和资源分包。
不要上来就搞复杂的方案,简单方案能解决的问题就用简单方案。
✅ 包体积优化 Checklist
为了方便大家落地,我们整理了一份包体积优化Checklist。每次发布新版本前,可以对照检查一遍。
代码优化
- 开启代码混淆(-enable-property-obfuscation)
- 开启顶层混淆(-enable-toplevel-obfuscation)
- 开启文件名混淆(-enable-filename-obfuscation)
- 移除console.log等调试代码(-remove-log)
- 开启代码压缩(-compact)
- 按需引入,不导入整个模块
- 使用具名导出,便于Tree Shaking
- 避免副作用代码
图片优化
- 所有图片已压缩
- 照片类图片使用WebP格式
- 图标类图片使用合适的尺寸
- 图片尺寸不超过显示需要的2倍
- 删除未使用的图片资源
- 列表缩略图和详情大图分开存放
- 考虑使用矢量图(SVG)代替位图
资源优化
- 清理未使用的资源文件
- 开启资源混淆
- 多语言资源只保留需要的语种
- 删除不需要的分辨率资源
- 配置文件精简
- rawfile目录下的文件都在使用
字体优化
- 不使用自定义字体(使用系统字体)
- 如果使用自定义字体,做了子集化
- 字体文件已压缩
构建配置
- Release构建开启了所有优化选项
- 移除了debug相关的代码和资源
- 没有打包测试代码
- 原生库只包含需要的架构
监控与流程
- 建立了包体积基线
- 每个版本检查体积增长
- 体积增长超过阈值需要评审
- 定期做专项优化
「民族图鉴」当前状态对照:
| 优化项 | 状态 | 预计收益 |
|---|---|---|
| 开启代码混淆 | ⬜ 未做 | 减少20-30%代码体积 |
| 图片转WebP | ⬜ 未做 | 减少30-50%图片体积 |
| 清理未使用资源 | ⬜ 待检查 | 视情况而定 |
| 移除调试代码 | ⬜ 未做 | 少量减少 |
| 资源混淆 | ⬜ 未做 | 减少5%左右 |
建议优先完成前两项(代码混淆 + WebP转换),投入少、收益大。
💡 Checklist的使用方法:
- 每次发布前对照检查
- 已完成的打勾,未完成的安排时间做
- 定期回顾,更新清单
- 把包体积纳入发布标准
❓ 常见问题
Q1:图片压缩后变模糊了怎么办?
这是最常见的问题。压缩和质量本身就是矛盾的,关键是找到平衡点。
解决方法:
- 提高质量参数:如果JPEG质量70太模糊,试试80、85
- 换格式:有时候WebP在低质量下比JPEG更清晰
- 针对性压缩:不同图片用不同的质量参数,不要一刀切
- 检查尺寸:如果图片尺寸太大,先缩尺寸再压质量,效果更好
判断标准:
- 正常使用距离下看不出明显差异即可
- 不要放大了100%去对比像素级差异,用户不会那么看
- 可以做A/B测试,让用户判断是否可接受
Q2:混淆后应用崩溃了怎么办?
混淆后崩溃,通常是因为把不该混淆的东西混淆了。
常见原因:
- 反射调用:通过字符串类名/方法名调用的,混淆后名字变了就找不到了
- 序列化/反序列化:JSON解析时用属性名匹配,混淆后属性名变了
- 系统API回调:某些系统接口要求特定的方法名
排查方法:
- 查看崩溃日志,找到错误信息
- 根据错误信息定位是哪个类/方法出了问题
- 在混淆规则中添加keep规则,保留对应的类/方法/属性
预防措施:
- 开启混淆后充分测试,覆盖所有功能
- 重要的数据模型类,保留属性名
- 与原生交互的接口,保留类名和方法名
Q3:资源找不到了(Resources.NotFoundException)怎么办?
可能原因:
- 资源被误删:清理未使用资源时删错了
- 资源混淆问题:混淆后引用路径不对
- 资源路径错误:代码中写死了资源路径
排查方法:
- 确认资源文件是否存在
- 检查资源引用的写法是否正确
- 如果用了资源混淆,检查混淆规则是否正确
最佳实践:
- 使用
$r()引用资源,不要硬编码路径 - 删除资源前,先全局搜索确认没有引用
- 资源混淆在测试环境充分验证后再上线
Q4:WebP格式兼容性怎么样?
鸿蒙系统是支持WebP格式的。但如果你要适配旧版本,需要确认最低支持的系统版本。
对于「民族图鉴」来说,如果只需要支持较新的鸿蒙版本,WebP是完全没问题的。
兼容性方案:
- 如果需要兼容不支持WebP的设备,可以在构建时生成多套格式
- 或者就用JPEG/PNG,兼容性最好
Q5:为什么开启了Tree Shaking,体积没怎么变?
Tree Shaking的效果取决于代码结构。如果你的代码本身就没有多少未使用的部分,Tree Shaking的效果自然就不明显。
可能原因:
- 未使用的代码本来就少:项目小,代码都用上了
- 写法不利于Tree Shaking:比如大量使用default export、副作用代码
- 第三方库:很多第三方库没有做Tree Shaking优化
提升Tree Shaking效果的方法:
- 使用具名导出(named export),而不是默认导出(default export)
- 避免模块顶层的副作用代码
- 选择支持Tree Shaking的第三方库
- 按需引入,不要导入整个模块
Q6:包体积优化有什么"银弹"吗?
很遗憾,没有银弹。包体积优化是一项细致的工作,需要一项一项去抠。
但如果只选一个最重要的,那就是:图片优化。
对于大多数应用来说,图片都是体积最大的部分。把图片优化好,通常就能获得最大的收益。
Q7:优化到什么程度才算够?
这取决于你的应用类型和目标用户。
参考标准:
- 工具类App:尽量控制在10MB以内
- 内容类App:控制在50MB以内
- 游戏/视频类:几百MB也正常
对于「民族图鉴」这类工具应用,目标应该是:
- 基础版:10MB以内
- 完整版:20MB以内
更重要的是建立监控机制,防止版本迭代过程中包体积"偷偷"增长。
📝 小结
本文全面介绍了鸿蒙应用包体积优化的各种方法。让我们总结一下:
核心知识点
- 为什么优化:下载转化率、安装时间、存储空间、分发成本
- 安装包构成:资源(50%80%)、代码(10%30%)、原生库、其他
- 代码优化:Tree Shaking、混淆、压缩、按需引入
- 资源优化:图片压缩、格式选择(WebP)、删除未使用、资源混淆
- 图片优化三部曲:压缩尺寸 → 选择格式 → 平衡质量与体积
- 字体优化:子集化、按需加载
- 按需加载:动态特性、资源分包
- 优化流程:分析现状 → 制定计划 → 实施优化 → 验证效果
「民族图鉴」优化实践
针对「民族图鉴」项目,我们提出的优化方案:
- 最高优先级:56张封面图压缩+WebP转换(预计减少30%~50%图片体积)
- 高优先级:开启代码混淆+remove-log(预计减少20%~30%代码体积)
- 中优先级:图片尺寸优化、清理未使用资源
- 低优先级:资源混淆、动态特性
优化的正确姿势
包体积优化不是一次性的工作,而是持续的过程:
- 建立基线:记录每个版本的包体积
- 监控增长:版本发布时检查体积增长是否合理
- 定期优化:每隔一段时间做一次专项优化
- 团队意识:让团队成员都有包体积意识
记住:包体积是一点点涨上去的,也要一点点抠下来。每优化100KB,都是在为用户体验做贡献。
下一篇,我们将探讨功耗优化——如何让你的应用更省电,减少后台耗电,让用户的手机更持久。
更多推荐




所有评论(0)