HarmonyOS开发:包体积优化——打包体积优化

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%,多架构叠加更恐怖
  • 配置与索引:体积小但也不能忽视

优化策略全景

体积优化有四条主线:

  1. 资源压缩:图片转WebP、rawfile清理、冗余资源删除
  2. 代码压缩:混淆、Tree-shaking、无用代码删除
  3. 架构裁剪:只保留必要的CPU架构
  4. 动态加载:非核心功能拆成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在包体积优化方面有以下改进:

  1. ABC字节码压缩增强:新版方舟编译器的ABC字节码压缩率提升约15%,同样的代码编译后体积更小。

  2. 资源按设备密度打包:HarmonyOS 6支持根据目标设备密度只打包对应密度的图片资源。比如目标设备是xdpi,就只打包xdpi的图片,其他密度的图片不打入HAP。

  3. HSP共享库体积优化:多HAP工程中,HSP模块的体积计算方式优化。HSP只算一次体积,不再重复计入各个Feature模块。

  4. 构建缓存增量编译:hvigor 5.0支持构建缓存,增量编译只重新编译变化的模块,不仅加速构建,也减少了中间产物的体积。

  5. App Pack格式优化:HarmonyOS 6的APP包(.app格式,用于上架)支持更好的压缩算法,上架包体积比之前减小约10%。

总结

包体积优化不是一锤子买卖,是持续的过程。每次加新功能、加新资源,都要想想"这东西真的需要打进HAP吗?"

记住优化优先级:

  1. 先分析:搞清楚体积花在哪
  2. 砍资源:图片转WebP、删无用rawfile,效果最明显
  3. 砍架构:只保留arm64-v8a,立竿见影
  4. 开混淆:代码混淆+Tree-shaking,锦上添花
  5. 拆模块:非核心功能拆Feature按需加载,终极手段