先说一个前几天真实遇到的问题。某开发者把应用从单模块拆成多模块,没动任何业务逻辑,只是把公共部分抽了几个共享包出来。结果release包一打,体积反而比原来大了将近一倍。折腾了一下午,最后发现原因特别基础——共享代码被打进了每个HAP,重复打包了。这类问题如果不懂鸿蒙的包结构,排查起来就是盲人摸象。
HarmonyOS(鸿蒙)应用包结构这件事,表面上看就是一堆后缀名:HAP、HAR、HSP、APP。但实际开发中,你遇到的大多数“包打不出来”“运行闪退”“上架被拒”的问题,根源都在这里。这篇文章就把鸿蒙的应用包结构从头到尾拆一遍,讲清楚每种包是干什么的、HAP内部长什么样、打包签名链路怎么走,以及多模块工程里最常见的包结构坑。
1. 为什么说HAP不是APK换个壳:三种交付单元的职责边界
我在不少技术群里看到刚转鸿蒙的开发者说:“HAP不就是APK改个后缀嘛,zip解压看里面的东西也差不多。”这个说法大方向没错——HAP确实是压缩包,也能解压看到资源文件。但如果只理解到这一层,后面做多模块工程的时候一定会吃大亏。
1.1 HAP:应用的基本交付单元,但一个应用通常不止一个HAP
HAP的全称是HarmonyOS Ability Package,是鸿蒙应用安装和运行的基本单位。一个应用可以由一个或多个HAP组成,每个HAP对应工程里的一个模块(Module)。
这里需要纠正一个习惯性认知。很多人以为一个应用就等于一个安装包,但在鸿蒙的模型里,应用和HAP是“一对多”关系。典型场景是这样的:
- 入口HAP:负责应用启动入口,包含入口Ability和首页页面;
- 功能HAP:按功能域拆分的模块,比如支付模块、直播模块、地图模块,各自独立成一个HAP;
- 元服务HAP:以元服务形式提供的轻量化服务入口,按需加载、用完即走。
每个HAP有自己的module.json5配置、自己的资源和代码,可以独立编译、独立签名。多个HAP合在一起,才构成一个完整的APP包交给市场分发。这个关系决定了你在多模块工程里怎么切工程结构,也决定了构建产物里会出现几个.hap文件。
1.2 HAR和HSP:同名“共享包”,机制完全是两回事
HAR(HarmonyOS Archive)和HSP(HarmonyOS Shared Package)中文都翻译成“共享包”,但工作方式完全不同。
HAR是静态共享包。它更像编译期的一堆“原材料”——代码、资源、第三方库在编译阶段被打包进宿主HAP里。引用HAR的模块,在构建时会把HAR的内容直接复制进自己的HAP。好处是简单可靠,坏处也显而易见:如果三个HAP都引用了同一个HAR,这个HAR的代码和资源就会存在三份,app体积直接膨胀。
HSP是动态共享包。它像运行时的一个“公共底座”,编译出来独立成包,不嵌入宿主HAP。多个HAP可以在运行时共享同一个HSP的代码和资源,HSP在应用安装包里只保留一份实体。适合承载真正需要跨模块复用的基础能力,比如网络层、数据库封装、工具库、公共UI组件。
明白了这两者的区别,前面说的“体积翻倍”问题就很好理解——那个人把应该做成HSP的公共模块做成了HAR,entry和feature各自打了一份进去。
1.3 选型判断清单:什么情况用HAR,什么情况用HSP
开发中我基本按这个逻辑决策:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 单模块小应用,内部代码简单 | 不拆分,直接写在HAP里 | 避免过度设计 |
| 多模块工程,公共代码被2个以上HAP引用 | HSP | 只留一份,包体小 |
| 给外部团队发布的纯代码库 | HAR | HSP的跨工程共享限制更多 |
| 需要独立升级的组件 | HSP | 可以单独更新 |
| 只在一个模块内部用到的工具类 | 直接放本地 | 连HAR都不要,省得维护 |
一个容易忽略的点:HAR如果被多个HAP引用,虽然包体膨胀,但不会导致功能错误;改成HSP之后如果管理不好版本,反而会出现运行时接口不一致的问题。所以选型不能只看包体,要看团队能不能hold住版本控制。
2. 逐一拆解HAP内部:目录、配置文件和资源匹配逻辑
HAP既然是zip包,理论上你可以把它后缀改成.zip直接解压看。我第一次干这事是在排查一个资源不生效的问题,解压完看到里面的东西,才发现很多“诡异问题”其实结构上就能看出来。
2.1 一个典型HAP目录树,每个节点是干什么的
以Stage模型的entry模块为例,构建产物里的HAP解压后大致长这样:
com.example.demo.hap ├── module.json5 ├── ets/ │ ├── entryability/ │ │ └── EntryAbility.abc │ └── pages/ │ ├── Index.abc │ └── Second.abc ├── resources/ │ ├── base/ │ │ ├── element/ │ │ │ ├── color.json │ │ │ ├── float.json │ │ │ └── string.json │ │ ├── media/ │ │ │ ├── icon.png │ │ │ └── startIcon.png │ │ └── profile/ │ │ ├── main_pages.json │ │ └── module.json5_backup(对应资源型配置) │ ├── en_US/ │ │ ├── element/ │ │ │ └── string.json │ │ └── media/ │ └── rawfile/ ├── libs/ │ └── arm64-v8a/ │ └── libnative.so └── resources.index- module.json5:模块的配置文件,所有Ability、权限、设备类型声明都在这里。它决定了一个HAP的“身份”。
- ets目录:ArkTS源码编译后的字节码所在位置。.abc文件(Ability Byte Code)就是可执行产物,你在工程里看到的.ets源码在包里是看不到的。
- resources目录:应用资源,按限定词拆分子目录。base是默认资源,en_US/zh_CN这种是特定语言资源,dark/light是深色模式资源。
- resources.index:资源索引文件,运行时资源管理框架靠它快速定位资源。
- libs目录:native库(.so),按CPU架构分目录。
注意:HAP目录里没有Android那样的AndroidManifest.xml,也没有classes.dex。鸿蒙的配置体系是module.json5 + 资源索引,代码体系是.abc字节码。整个包的结构跟APK差别很大,这就是为什么拿APK的思维去理解鸿蒙会卡壳的原因。
2.2 module.json5里那些关键字段,搞懂一半就能避免大半配置错误
module.json5是HAP里最值得逐字段读一遍的文件。我挑几个坑最多的字段展开说。
{ "module": { "name": "entry", "type": "entry", "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": ["phone", "tablet"], "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:main_pages", "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", "icon": "$media:icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "entities": ["entity.system.home"], "actions": ["action.system.home"] } ] } ] } }type字段,必须是entry或feature。entry是应用入口模块,feature是功能模块。一个APP包里有且只有一个entry类型的HAP,但可以有多个feature类型的HAP。如果你的工程里build的时候提示“entry module not found”或者“multiple entry module”,基本就是type配错了。
deviceTypes字段,声明这个模块支持哪些设备形态。这里最容易出现的问题是:入口的skills配了桌面图标,但deviceTypes里没包含那个设备类型,导致应用在某种设备上看不到图标。我见过一个案例,应用在平板上怎么都装不上,最后发现它的deviceTypes只有["phone"]。
mainElement字段,指向入口Ability。对entry模块来说,它决定应用启动时先拉起哪个Ability。如果这里填的名字和abilities数组里对不上,编译可能不报错,但运行时会闪退。
还有一个在开发工具里不太显眼的字段:deliveryWithInstall和installationFree。前者表示HAP是否随应用安装,后者表示是否支持免安装(元服务场景)。对普通应用来说,deliveryWithInstall保持true即可,改成false会导致HAP不随应用下发。
2.3 resources目录的资源匹配逻辑:为什么你改了英文不生效
resources目录是所有“看似诡异、实则规则明确”问题的重灾区。它的子目录命名有严格格式:限定词_限定词...,比如en_US、zh_CN、dark、land、en_US_dark。
系统运行时会根据设备当前的语言、区域、深浅色、屏幕方向等条件,优先去最匹配的限定词目录找资源,找不到就往上一级找,直到base兜底。例如你的英语用户看到的中文文案,大概率是resources/en_US/element/string.json里缺少对应key,系统只能回落去base取中文。
这个机制本身不复杂,但开发中容易踩到两个细节:
- 限定词目录的拼写必须符合规范。IDE里右键新建资源目录会引导你选择,手写容易把en-US写成en_US才是对的,注意是下划线不是中划线。
- 同一资源的key在不同限定词目录中必须一致。base目录里定好了
app_name,en_US目录里如果笔误写成了app_namee,英文环境下这个key就找不到,直接显示key原文。
3. 从工程目录到安装包:打包签名链路的完整过程
理解了HAP内部结构,再看构建过程就容易了。一个鸿蒙工程从源码到可安装的APP包,要经历依赖解析、模块编译、打包合并、签名校验四步。
3.1 一次完整打包经历的四个阶段
第一阶段是依赖解析。DevEco Studio同步工程时会读取每个模块的oh-package.json5,把声明的HAR、HSP依赖拉下来。这里同步失败或依赖找不到,后面编译必然报错。
第二阶段是模块编译。ArkTS源码被编译成.abc字节码,资源文件生成索引。这一步最耗时,也是绝大多数编译报错的产生地——类型错误、语法错误、资源引用错误都在这个阶段暴露。
第三阶段是打包合并。每个模块独立打包成HAP或HSP。多模块工程里,这个阶段你会在build目录下看到按模块生成的多个产物。
第四阶段是签名与组装。所有HAP/HSP经过签名后,组装成APP包,生成pack.info描述文件。这个文件记录应用包含哪些HAP/HSP、版本信息、包名等元数据,相当于整个APP包的“清单”。
构建产物路径需要留意:模块产物的HAP在各自模块的build目录下,最终APP包在工程根目录的build/outputs下。很多人在模块目录里翻半天找不到APP包,就是这个原因。
3.2 调试签名与发布签名为什么不能混用
签名机制是打包链路里“坑密度”最高的环节。鸿蒙的签名体系涉及三样东西:密钥库(.p12文件)、证书文件(.cer)、Profile文件(.p7b)。
开发调试阶段,DevEco Studio会自动生成一个调试证书,配置到build-profile.json5里,你不需要管太多。但发布上架前,这个调试签名就不能用了——应用市场要求使用正式证书签名,否则应用市场上传时直接校验失败。
签名混用导致的典型报错是Signing certificate mismatch,校验不通过会让“本地能装、市场拒收”的诡异局面出现。还有个容易被忽略的点:Profile文件绑定了包名(bundleName)和设备标识(UDID)。测试机多了之后调试Profile可能把设备加满,新设备装不上,需要重新生成Profile,而不是去改代码。
提示:发布签名一旦配置到build-profile.json5里,就不要随意更换密钥库。如果密钥库丢了或密码忘了,证书无法重签,只能走证书找回流程,非常麻烦。建议把.jks/.p12和密码单独加密备份。
3.3 用hdc命令验证包安装后的真实结构
打包成功不等于包结构没问题。我习惯装完包之后用hdc命令再确认一遍。
hdc install com.example.demo.hap hdc shell bm dump -n com.example.demobm dump输出里可以看到应用包含哪些module、每个module对应HAP的存放路径。如果应然是两个HAP+一个HSP,这里只会看到两个HAP路径,就要回去查HSP是不是没打进去。
还有一个验证技巧:把HAP直接用压缩软件打开看不完整,活动资源会索引错乱,但看目录层级、module.json5结构、resources目录分组还是够用的。做资源国际化验证时,直接对比不同限定词目录里的文件是否齐全,比在设备上切语言反复试要高效得多。
4. 多模块工程里最容易踩的两个包结构坑
多模块工程是包结构知识的主考场。我见过不算少的工程,依赖关系一复杂,包就出问题。这里挑两个发生率最高、排查链路也最典型的坑,完整走一遍。
4.1 典型多模块工程的目录组织方式
先看一个常见布局:
AppScope/ ├── app.json5 entry/ feature_cart/ feature_user/ shared_common/ (HSP类型) shared_network/ (HSP类型) build-profile.json5 oh-package.json5 hvigorfile.tsAppScope/app.json5里配置的是应用全局信息,比如bundleName、版本号。各个业务模块是独立的HAP,shared开头的模块是HSP。
在DevEco Studio中,“模块是HAP还是HSP”是在创建模块时决定的,但也可以通过模块下的build-profile.json5里调整。容易阴人的是:一个工程里只能有一个entry类型模块,其他业务模块必须是feature类型。如果你从现有工程复制了一个模块改成业务模块,忘记把type从entry改成feature,构建时会直接报“multiple entry modules”。
4.2 坑一:HAR被重复打入多个HAP,包体膨胀到怀疑人生
回到文章开头那个案例。某开发者把公共工具抽成了common_har(HAR),entry和feature_cart都在引用它。构建后APP包体积飙到原来的1.8倍。
排查链路是这样的:先看构建日志,输出里会列出每个HAP产物的大小。发现entry-default.hap有8MB,feature_cart-default.hap有6MB,但两个模块的业务代码加起来不超过5MB,直觉告诉我是公共部分重复了。
把两个HAP分别解压,在各自的ets目录里发现了完全相同的类文件路径——common_har的内容真的被复制了两份。这个现象对HAR来说是正常的,是它的工作方式。
修复方案是把这个公共工具从HAR改成HSP:新建shared_common模块,原代码迁移,然后让entry和feature_cart的oh-package.json5里依赖shared_common。再打包,APP包里HSP只有一份实体,体积直接降了一半多。
提示:HAR不是不能用,而是要用对地方。只被一个HAP引用的代码、页面级组件库,用HAR反而简单。最怕的是“一个公共库被三四个HAP一起引”——这种必然重复打包。
4.3 坑二:模块依赖方向写反导致编译时序错乱
第二个坑发生在跨模块跳转上。业务上需要在feature_cart里跳转到entry的订单详情页,开发者直接在feature_cart的oh-package.json5里加了对entry的依赖。
构建时开始没什么问题,但改了几次代码后,hvigor偶尔报出莫名其妙的任务执行失败,错误信息指向没有明确的源文件位置,只说是“task A depends on task B which depends on task A”。这是典型的循环依赖。
依赖方向在包结构上的含义是:被依赖的模块必须先编译完成。feature依赖entry,意味着entry要等feature编译完,但如果entry内部又有代码引用了feature,就成了鸡生蛋问题,构建时序自然乱。
正确的做法:跨模块共享的类型、跳转路由常量、基础页面组件,一律下沉到一个共享模块(HSP或HAR),依赖关系从“业务模块互依”改成“业务模块共同依赖共享层”。这样构建图从网状变成树状,时序稳定,增量编译也更快。
5. 自己动手:验证包结构健康度的几个实操办法
最后分享几个不依赖IDE图形界面、命令行就能完成的包体检方法。这些东西熟练了,排查包相关问题会快很多。
5.1 从构建日志读出关键信息
构建完成后,控制台输出的日志里隐藏着核心数据。重点看三类信息:
- 生成了哪些HAP/HSP:日志会列出产物路径。数一下HAP数量,跟预期对比。少了说明有模块没参与打包,多了说明有冗余模块。
- 每个产物的大小:DevEco Studio的构建面板或命令行输出会标注产物大小。横向对比各模块体积,异常膨胀的模块优先怀疑重复打包。
- 签名信息:日志末尾有签名相关的输出,检查签名的bundleName是否和AppScope/app.json5里的一致。
5.2 常见报错关键词与排查方向
| 报错关键词 | 可能原因 | 检查入口 |
|---|---|---|
| multiple entry modules | 工程里有多个entry类型模块 | 各模块build-profile.json5 |
| Signing certificate mismatch | 证书bundleName与工程配置不一致 | build-profile.json5的signingConfigs |
| Failed to find module | 依赖未同步或oh-package里路径写错 | oh-package.json5 + Sync工程 |
| resources not found | 资源key缺失或限定词目录放错 | 对应resources目录 |
| pack info invalid | APP包元数据异常 | pack.info与app.json5版本信息 |
5.3 体积异常模块的“三件套”检查法
发现某个HAP体积异常,按顺序做三件事:
先解压HAP看libs目录,确认有没有打进多余的.so;再看resources目录,有没有把只在调试期用的大图、测试资源带进来;最后看ets目录,如果有大量重复的公共类路径,基本就是HAR重复打入。
我在实际项目中靠这三板斧解决过几次“明明没加功能包怎么变大了”的疑惑。多数时候结果都是同一个:开发者把一段公共代码以HAR形式塞给了多个业务模块,编译期复制了太多份。
包结构这东西,初学时觉得就是几个名词,等到真正做多模块、上架、排查线上问题的时候才体会到它的分量。我个人的习惯是:每新建一个模块,先问一句它是HAP还是HSP,依赖关系画出来是不是树状的,公共代码到底放哪里。这三个问题想清楚了,包结构层面的大多数坑基本就绕开了。最后再分享一个小技巧:改完模块依赖关系后,别急着编译,先在工程根目录跑一次完整的Sync,等到依赖解析日志完全干净了再Build。跳过这一步,经常会在编译中途才爆出依赖问题,排查起来费的时间反而更多。