HarmonyOS开发:包体积优化------打包体积优化
核心要点:HAP体积直接影响用户下载意愿和安装成功率,从资源压缩、代码混淆到动态加载,系统性地把体积砍下来,是每个上架应用必须过的关。
背景与动机
你的应用HAP包50MB,用户一看下载大小就劝退了。应用市场也劝退------很多渠道对包体积有硬性要求,超了就不给推荐位。
你可能觉得50MB也不大啊?但你要知道,很多用户的手机存储就剩几百MB,你的应用占了50MB,更新两次就满了。用户的选择很简单------卸载。
更扎心的是,你分析了一下这50MB,发现真正有用的代码可能就5MB,剩下45MB全是图片、没用上的三方库、重复的资源文件。这些"水分"不挤掉,你的应用就是在浪费用户的空间。
包体积优化不是锦上添花,是必须做的。而且要从项目一开始就做,别等到上线前才想起来------那时候改不动了。
核心原理
HAP体积构成分析
一个HAP包里到底装了什么?先搞清楚体积分布,才能有的放矢:
#mermaid-svg-mYVOQeCbfBFLhslh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mYVOQeCbfBFLhslh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mYVOQeCbfBFLhslh .error-icon{fill:#552222;}#mermaid-svg-mYVOQeCbfBFLhslh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mYVOQeCbfBFLhslh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mYVOQeCbfBFLhslh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mYVOQeCbfBFLhslh .marker.cross{stroke:#333333;}#mermaid-svg-mYVOQeCbfBFLhslh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mYVOQeCbfBFLhslh p{margin:0;}#mermaid-svg-mYVOQeCbfBFLhslh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mYVOQeCbfBFLhslh .cluster-label text{fill:#333;}#mermaid-svg-mYVOQeCbfBFLhslh .cluster-label span{color:#333;}#mermaid-svg-mYVOQeCbfBFLhslh .cluster-label span p{background-color:transparent;}#mermaid-svg-mYVOQeCbfBFLhslh .label text,#mermaid-svg-mYVOQeCbfBFLhslh span{fill:#333;color:#333;}#mermaid-svg-mYVOQeCbfBFLhslh .node rect,#mermaid-svg-mYVOQeCbfBFLhslh .node circle,#mermaid-svg-mYVOQeCbfBFLhslh .node ellipse,#mermaid-svg-mYVOQeCbfBFLhslh .node polygon,#mermaid-svg-mYVOQeCbfBFLhslh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mYVOQeCbfBFLhslh .rough-node .label text,#mermaid-svg-mYVOQeCbfBFLhslh .node .label text,#mermaid-svg-mYVOQeCbfBFLhslh .image-shape .label,#mermaid-svg-mYVOQeCbfBFLhslh .icon-shape .label{text-anchor:middle;}#mermaid-svg-mYVOQeCbfBFLhslh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mYVOQeCbfBFLhslh .rough-node .label,#mermaid-svg-mYVOQeCbfBFLhslh .node .label,#mermaid-svg-mYVOQeCbfBFLhslh .image-shape .label,#mermaid-svg-mYVOQeCbfBFLhslh .icon-shape .label{text-align:center;}#mermaid-svg-mYVOQeCbfBFLhslh .node.clickable{cursor:pointer;}#mermaid-svg-mYVOQeCbfBFLhslh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mYVOQeCbfBFLhslh .arrowheadPath{fill:#333333;}#mermaid-svg-mYVOQeCbfBFLhslh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mYVOQeCbfBFLhslh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mYVOQeCbfBFLhslh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mYVOQeCbfBFLhslh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mYVOQeCbfBFLhslh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mYVOQeCbfBFLhslh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mYVOQeCbfBFLhslh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mYVOQeCbfBFLhslh .cluster text{fill:#333;}#mermaid-svg-mYVOQeCbfBFLhslh .cluster span{color:#333;}#mermaid-svg-mYVOQeCbfBFLhslh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mYVOQeCbfBFLhslh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mYVOQeCbfBFLhslh rect.text{fill:none;stroke-width:0;}#mermaid-svg-mYVOQeCbfBFLhslh .icon-shape,#mermaid-svg-mYVOQeCbfBFLhslh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mYVOQeCbfBFLhslh .icon-shape p,#mermaid-svg-mYVOQeCbfBFLhslh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mYVOQeCbfBFLhslh .icon-shape .label rect,#mermaid-svg-mYVOQeCbfBFLhslh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mYVOQeCbfBFLhslh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mYVOQeCbfBFLhslh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mYVOQeCbfBFLhslh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-mYVOQeCbfBFLhslh .total>*{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .total span{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .total tspan{fill:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .big>*{fill:#F39C12!important;stroke:#D68910!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .big span{fill:#F39C12!important;stroke:#D68910!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .big tspan{fill:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .mid>*{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .mid span{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .mid tspan{fill:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .small>*{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .small span{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .small tspan{fill:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .detail>*{fill:#9B59B6!important;stroke:#8E44AD!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .detail span{fill:#9B59B6!important;stroke:#8E44AD!important;color:#fff!important;}#mermaid-svg-mYVOQeCbfBFLhslh .detail tspan{fill:#fff!important;} HAP总体积
资源文件 40-60%
代码产物 20-30%
原生库 .so 10-20%
配置与索引 5-10%
图片资源
rawfile文件
国际化资源
ABC字节码
SourceMap
arm64-v8a
x86_64
armeabi-v7a
典型HAP体积分布:
- 资源文件:占大头,40-60%,图片是重灾区
- 代码产物:ABC字节码+SourceMap,20-30%
- 原生库:.so文件,10-20%,多架构叠加更恐怖
- 配置与索引:体积小但也不能忽视
优化策略全景
体积优化有四条主线:
- 资源压缩:图片转WebP、rawfile清理、冗余资源删除
- 代码压缩:混淆、Tree-shaking、无用代码删除
- 架构裁剪:只保留必要的CPU架构
- 动态加载:非核心功能拆成Feature模块按需下载
代码实战
基础用法:HAP体积分析
先分析,再优化。不知道体积花在哪,优化就是瞎折腾。
typescript
// scripts/analyze-hap.ets
// HAP体积分析工具
import { zlib } from 'zlib';
interface FileSizeInfo {
path: string;
size: number; // 字节
sizeFormatted: string; // 可读格式
percentage: string; // 占比
category: string; // 分类
}
// 分析HAP包内容
function analyzeHap(hapPath: string): FileSizeInfo[] {
const results: FileSizeInfo[] = [];
// 解压HAP(HAP本质是ZIP)
const tempDir = './temp_hap_analysis';
execSync(`unzip -o "${hapPath}" -d "${tempDir}"`);
// 递归遍历所有文件
function walkDir(dir: string, basePath: string = ''): void {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walkDir(fullPath, relativePath);
} else {
const stat = fs.statSync(fullPath);
results.push({
path: relativePath,
size: stat.size,
sizeFormatted: formatSize(stat.size),
percentage: '', // 后面计算
category: categorizeFile(relativePath)
});
}
}
}
walkDir(tempDir);
// 计算总大小和占比
const totalSize = results.reduce((sum, f) => sum + f.size, 0);
for (const item of results) {
item.percentage = ((item.size / totalSize) * 100).toFixed(2) + '%';
}
// 按大小排序
results.sort((a, b) => b.size - a.size);
// 清理临时目录
execSync(`rm -rf "${tempDir}"`);
return results;
}
// 文件分类
function categorizeFile(filePath: string): string {
if (filePath.startsWith('resources/base/media/')) return '图片资源';
if (filePath.startsWith('resources/base/profile/')) return '配置文件';
if (filePath.startsWith('resources/base/element/')) return '字符串资源';
if (filePath.startsWith('resources/rawfile/')) return 'Rawfile';
if (filePath.startsWith('ets/')) return '代码产物';
if (filePath.startsWith('libs/')) return '原生库';
if (filePath.endsWith('.json') || filePath.endsWith('.json5')) return '配置';
return '其他';
}
// 格式化文件大小
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
// 输出分析报告
function printReport(results: FileSizeInfo[]): void {
const totalSize = results.reduce((sum, f) => sum + f.size, 0);
console.log(`\n===== HAP体积分析报告 =====`);
console.log(`总大小: ${formatSize(totalSize)}\n`);
// 按分类汇总
const categories = new Map<string, number>();
for (const item of results) {
categories.set(item.category, (categories.get(item.category) || 0) + item.size);
}
console.log('--- 分类汇总 ---');
for (const [cat, size] of categories) {
const pct = ((size / totalSize) * 100).toFixed(1);
console.log(` ${cat}: ${formatSize(size)} (${pct}%)`);
}
// TOP 20 大文件
console.log('\n--- TOP 20 大文件 ---');
const top20 = results.slice(0, 20);
for (const item of top20) {
console.log(` ${item.path}: ${item.sizeFormatted} (${item.percentage})`);
}
// 优化建议
console.log('\n--- 优化建议 ---');
const imageFiles = results.filter(f => f.path.match(/\.(png|jpg|jpeg|gif)$/i));
const totalImageSize = imageFiles.reduce((sum, f) => sum + f.size, 0);
if (totalImageSize > 1024 * 1024) {
console.log(` ⚠️ 图片资源共 ${formatSize(totalImageSize)},建议转WebP格式`);
}
const soFiles = results.filter(f => f.path.endsWith('.so'));
if (soFiles.length > 0) {
const architectures = new Set(soFiles.map(f => {
const match = f.path.match(/libs\/([^/]+)/);
return match ? match[1] : 'unknown';
}));
if (architectures.size > 2) {
console.log(` ⚠️ 包含 ${architectures.size} 种CPU架构,建议只保留arm64-v8a`);
}
}
}
进阶用法:资源压缩与代码压缩
资源压缩------图片转WebP:
typescript
// scripts/compress-images.ets
// 图片资源压缩工具
import { execSync } from 'child_process';
// 批量转换PNG/JPG为WebP
function convertToWebP(resourceDir: string, quality: number = 80): void {
const imageExtensions = ['.png', '.jpg', '.jpeg'];
let convertedCount = 0;
let savedSize = 0;
function walkDir(dir: string): void {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath);
continue;
}
const ext = path.extname(entry.name).toLowerCase();
if (!imageExtensions.includes(ext)) continue;
// 跳过.9图(Nine-patch)
if (entry.name.endsWith('.9.png')) continue;
const originalSize = fs.statSync(fullPath).size;
const webpPath = fullPath.replace(ext, '.webp');
// 使用cwebp转换
try {
execSync(`cwebp -q ${quality} "${fullPath}" -o "${webpPath}"`);
const newSize = fs.statSync(webpPath).size;
if (newSize < originalSize) {
// 删除原文件
fs.unlinkSync(fullPath);
savedSize += (originalSize - newSize);
convertedCount++;
console.log(`✅ ${entry.name}: ${formatSize(originalSize)} → ${formatSize(newSize)}`);
} else {
// WebP反而更大,保留原文件
fs.unlinkSync(webpPath);
console.log(`⏭️ ${entry.name}: WebP更大,保留原格式`);
}
} catch (error) {
console.warn(`❌ 转换失败: ${entry.name}`);
}
}
}
walkDir(resourceDir);
console.log(`\n转换完成: ${convertedCount} 个文件,节省 ${formatSize(savedSize)}`);
}
构建配置优化:
typescript
// build-profile.json5
{
"app": {
"products": [
{
"name": "default",
"output": {
"artifactName": "MyApp",
"module": {
"entry": {
"compress": {
"ark": true, // ArkTS字节码压缩
"resources": true // 资源压缩
}
}
}
}
}
]
}
}
typescript
// entry/hvigorfile.ts
import { hapTasks, OhosPluginId } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks,
plugins: [
{
pluginId: OhosPluginId.HAP,
apply() {
// Release构建时启用代码压缩
this.task('assembleHap', (task) => {
if (task.buildType === 'release') {
task.setMinifyEnabled(true); // 开启代码压缩
task.setShrinkResources(true); // 开启资源压缩
}
});
}
}
]
}
混淆配置:
typescript
// entry/obfuscation-rules.txt
# 代码混淆配置
# 开启基础混淆
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
# 保留规则(不混淆的内容)
-keep-file-name
EntryAbility
MainAbility
# 保留属性名(被反射调用的)
-keep-property-name
onCreate
onDestroy
onWindowStageCreate
# 保留全局名称(被动态引用的)
-keep-global-name
Logger
NetworkUtil
# SourceMap保留(调试用,发布时可删除)
# -enable-source-map
完整示例:多维度体积优化方案
把所有优化手段整合到一起:
typescript
// scripts/optimize-package.ets
// 包体积优化主脚本
import { hapTasks, OhosPluginId } from '@ohos/hvigor-ohos-plugin';
// 优化配置
interface OptimizeConfig {
compressImages: boolean; // 图片压缩
imageQuality: number; // WebP质量
stripSoDebug: boolean; // 去除.so调试符号
excludeArchitectures: string[]; // 排除的CPU架构
enableObfuscation: boolean; // 代码混淆
removeSourceMap: boolean; // 删除SourceMap
cleanRawfile: string[]; // 清理的rawfile
}
const defaultConfig: OptimizeConfig = {
compressImages: true,
imageQuality: 80,
stripSoDebug: true,
excludeArchitectures: ['x86_64', 'armeabi-v7a'], // 只保留arm64-v8a
enableObfuscation: true,
removeSourceMap: true,
cleanRawfile: ['test_data/', 'docs/', 'templates/']
};
// 自定义优化构建插件
class PackageOptimizePlugin {
private config: OptimizeConfig;
constructor(config: OptimizeConfig = defaultConfig) {
this.config = config;
}
apply(pluginContext: any): void {
// 构建后优化
pluginContext.registerAfterTask('assembleHap', (taskOutput: any) => {
const hapPath = taskOutput.outputPath;
console.log('===== 开始包体积优化 =====');
// 1. 图片压缩
if (this.config.compressImages) {
console.log('[优化] 图片压缩中...');
this.compressImagesInHap(hapPath);
}
// 2. .so文件优化
if (this.config.stripSoDebug) {
console.log('[优化] 去除.so调试符号...');
this.stripSoDebugSymbols(hapPath);
}
// 3. 排除多余架构
if (this.config.excludeArchitectures.length > 0) {
console.log('[优化] 排除多余CPU架构...');
this.removeArchitectures(hapPath, this.config.excludeArchitectures);
}
// 4. 删除SourceMap
if (this.config.removeSourceMap) {
console.log('[优化] 删除SourceMap...');
this.removeSourceMapFiles(hapPath);
}
// 输出优化结果
const finalSize = fs.statSync(hapPath).size;
console.log(`===== 优化完成,最终大小: ${formatSize(finalSize)} =====`);
});
}
private compressImagesInHap(hapPath: string): void {
// 在构建产物中查找并压缩图片
const outputDir = path.dirname(hapPath);
const resourcesDir = path.join(outputDir, 'resources');
if (fs.existsSync(resourcesDir)) {
// 调用图片压缩工具
convertToWebP(resourcesDir, this.config.imageQuality);
}
}
private stripSoDebugSymbols(hapPath: string): void {
// 使用strip工具去除.so中的调试符号
const outputDir = path.dirname(hapPath);
const libsDir = path.join(outputDir, 'libs');
if (!fs.existsSync(libsDir)) return;
const soFiles = this.findFiles(libsDir, '.so');
for (const soFile of soFiles) {
try {
execSync(`llvm-strip --strip-debug "${soFile}"`);
const newSize = fs.statSync(soFile).size;
console.log(` ${path.basename(soFile)}: ${formatSize(newSize)}`);
} catch (error) {
console.warn(` strip失败: ${soFile}`);
}
}
}
private removeArchitectures(hapPath: string, archs: string[]): void {
const outputDir = path.dirname(hapPath);
const libsDir = path.join(outputDir, 'libs');
if (!fs.existsSync(libsDir)) return;
for (const arch of archs) {
const archDir = path.join(libsDir, arch);
if (fs.existsSync(archDir)) {
const size = this.getDirSize(archDir);
execSync(`rm -rf "${archDir}"`);
console.log(` 删除 ${arch}: 节省 ${formatSize(size)}`);
}
}
}
private removeSourceMapFiles(hapPath: string): void {
const outputDir = path.dirname(hapPath);
const mapFiles = this.findFiles(outputDir, '.map');
let savedSize = 0;
for (const mapFile of mapFiles) {
savedSize += fs.statSync(mapFile).size;
fs.unlinkSync(mapFile);
}
if (savedSize > 0) {
console.log(` 删除SourceMap: 节省 ${formatSize(savedSize)}`);
}
}
private findFiles(dir: string, ext: string): string[] {
const results: string[] = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...this.findFiles(fullPath, ext));
} else if (entry.name.endsWith(ext)) {
results.push(fullPath);
}
}
return results;
}
private getDirSize(dir: string): number {
let size = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
size += this.getDirSize(fullPath);
} else {
size += fs.statSync(fullPath).size;
}
}
return size;
}
}
// 注册优化插件
export default {
system: hapTasks,
plugins: [
new PackageOptimizePlugin()
]
}
踩坑与注意事项
坑1:WebP兼容性问题
WebP在HarmonyOS上是原生支持的,但如果你用了第三方图片加载库,它可能不支持WebP。另外,.9图(Nine-patch)不能转WebP。
解法:转WebP前排除.9图。验证第三方库是否支持WebP格式。如果不确定,先小范围测试。
坑2:混淆导致运行时崩溃
代码混淆把类名、属性名改了,但反射调用、动态引用的地方还在用原名------运行时就崩了。这是最常见的混淆问题。
解法 :混淆配置中用-keep-property-name和-keep-global-name保留被动态引用的名称。混淆后一定要跑一遍全量测试,不能只测主流程。
坑3:去除.so调试符号导致崩溃分析困难
strip掉.so的调试符号后,崩溃堆栈里只有地址没有函数名,定位问题很痛苦。
解法:保留一份未strip的.so文件用于调试。发布包用strip版本,调试时用unstrip版本配合addr2line工具还原堆栈。
坑4:只保留arm64导致模拟器无法运行
开发阶段用模拟器调试,模拟器通常是x86_64架构。如果你把x86_64的.so删了,模拟器就跑不了。
解法:开发阶段保留所有架构,只在发布包中裁剪。通过构建变体控制:
typescript
// build-profile.json5
{
"app": {
"products": [
{
"name": "default", // 开发用,保留所有架构
"output": { /* ... */ }
},
{
"name": "release", // 发布用,只保留arm64
"output": {
"module": {
"entry": {
"nativeLib": {
"filter": {
"excludes": ["x86_64", "armeabi-v7a"]
}
}
}
}
}
}
]
}
}
坑5:资源压缩误删被动态引用的资源
shrinkResources会自动删除"未使用"的资源,但它判断"未使用"的依据是代码中是否有静态引用。如果你通过字符串拼接动态引用资源(比如$r('app.media.' + iconName)),它会被误删。
解法 :在resources目录下创建keep.xml文件,声明需要保留的资源:
xml
<!-- resources/base/keep.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@media/icon_*,@media/bg_*" />
HarmonyOS 6适配说明
HarmonyOS 6在包体积优化方面有以下改进:
-
ABC字节码压缩增强:新版方舟编译器的ABC字节码压缩率提升约15%,同样的代码编译后体积更小。
-
资源按设备密度打包:HarmonyOS 6支持根据目标设备密度只打包对应密度的图片资源。比如目标设备是xdpi,就只打包xdpi的图片,其他密度的图片不打入HAP。
-
HSP共享库体积优化:多HAP工程中,HSP模块的体积计算方式优化。HSP只算一次体积,不再重复计入各个Feature模块。
-
构建缓存增量编译:hvigor 5.0支持构建缓存,增量编译只重新编译变化的模块,不仅加速构建,也减少了中间产物的体积。
-
App Pack格式优化:HarmonyOS 6的APP包(.app格式,用于上架)支持更好的压缩算法,上架包体积比之前减小约10%。
总结
包体积优化不是一锤子买卖,是持续的过程。每次加新功能、加新资源,都要想想"这东西真的需要打进HAP吗?"
记住优化优先级:
- 先分析:搞清楚体积花在哪
- 砍资源:图片转WebP、删无用rawfile,效果最明显
- 砍架构:只保留arm64-v8a,立竿见影
- 开混淆:代码混淆+Tree-shaking,锦上添花
- 拆模块:非核心功能拆Feature按需加载,终极手段