HarmonyOS开发:动态特性包——Feature包配置
HarmonyOS开发:动态特性包——Feature包配置
📌 核心要点:Feature包是HarmonyOS按需安装的核心机制,将非核心功能拆分为独立模块,用户用到时才下载安装,大幅减小首次安装体积。
背景与动机
你的应用功能越来越多:首页、搜索、购物车、订单、支付、客服、直播、社区……全塞进一个HAP?首次安装包50MB+,用户还没用就先被下载大小劝退了。
而且仔细想想,用户打开应用的第一件事是什么?看首页。直播、社区、客服这些功能,可能80%的用户压根不会用。凭什么让他们为用不上的功能付出下载时间和存储空间?
这就是Feature包存在的意义。核心功能放Entry,随应用安装;扩展功能放Feature,用到时再下载。首次安装可能就5MB,用户感知好,留存率高。
但Feature包不是随便拆的。拆错了,模块间通信复杂、加载体验差、维护成本高。怎么拆、怎么配、怎么管理,是这篇文章要讲清楚的。
核心原理
Atomic Service与动态安装
HarmonyOS的动态安装体系基于Atomic Service(元服务)理念:
flowchart TD
A[用户安装应用] --> B[只安装Entry HAP]
B --> C[用户使用核心功能]
C --> D{触发扩展功能?}
D -->|否| C
D -->|是| E[检查Feature是否已安装]
E -->|已安装| F[直接跳转Feature]
E -->|未安装| G[从应用市场下载Feature]
G --> H{下载安装成功?}
H -->|是| F
H -->|否| I[提示用户重试]
F --> J[使用Feature功能]
J --> K{退出Feature?}
K -->|是| C
classDef start fill:#2ECC71,stroke:#25A55A,color:#fff
classDef process fill:#4A90D9,stroke:#2C5F8A,color:#fff
classDef decision fill:#F39C12,stroke:#D68910,color:#fff
classDef error fill:#FF6B6B,stroke:#CC5555,color:#fff
classDef success fill:#9B59B6,stroke:#8E44AD,color:#fff
class A,B,C start
class D,E,F,J process
class G,H,K decision
class I error
关键概念:
- Entry HAP:应用入口,必须安装,包含核心功能
- Feature HAP:扩展功能包,按需安装,不能独立运行
- HSP:共享模块,随Entry安装,供Entry和Feature共用
- 动态安装:Feature包在运行时按需从应用市场下载安装
Feature包设计原则
拆Feature包不是"想拆就拆",要遵循以下原则:
- 功能独立性:Feature模块的功能应该相对独立,不依赖其他Feature模块
- 体积合理性:Feature模块体积不宜过小(几百KB的不值得拆),也不宜过大(超过20MB拆分效果不明显)
- 使用频率:低频使用的功能才适合拆成Feature,高频使用的功能应该放Entry
- 通信简洁:Feature与Entry的通信要尽量简单,避免复杂的双向数据流
Feature包生命周期
flowchart LR
A[未安装] -->|下载安装| B[已安装]
B -->|用户使用| C[运行中]
C -->|退出| B
B -->|系统回收| D[已卸载]
D -->|再次使用| A
classDef uninstalled fill:#FF6B6B,stroke:#CC5555,color:#fff
classDef installed fill:#4A90D9,stroke:#2C5F8A,color:#fff
classDef running fill:#2ECC71,stroke:#25A55A,color:#fff
classDef recycled fill:#F39C12,stroke:#D68910,color:#fff
class A uninstalled
class B installed
class C running
class D recycled
Feature包有四种状态:未安装 → 已安装 → 运行中 → 已卸载(系统回收)。系统可能在存储不足时自动卸载不常用的Feature包,下次使用时需要重新下载。
代码实战
基础用法:创建Feature模块
第一步:在工程中创建Feature模块
// feature_live/src/main/module.json5
{
"module": {
"name": "feature_live",
"type": "feature", // Feature类型
"description": "$string:live_desc",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": false, // 关键:不随应用安装
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "LiveAbility",
"srcEntry": "./ets/liveability/LiveAbility.ets",
"description": "$string:LiveAbility_desc",
"icon": "$media:layered_image",
"label": "$string:LiveAbility_label",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
]
}
}
关键配置:
type: "feature"——声明为Feature模块deliveryWithInstall: false——不随应用安装,这是Feature的核心特征
第二步:在build-profile.json5中注册模块
// build-profile.json5
{
"app": {
"signingConfigs": [],
"compileSdkVersion": 12,
"compatibleSdkVersion": 10,
"products": [
{
"name": "default",
"signingConfig": "default"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [{ "name": "default", "applyToProducts": ["default"] }]
},
{
"name": "feature_live",
"srcPath": "./feature_live",
"targets": [{ "name": "default", "applyToProducts": ["default"] }]
},
{
"name": "shared_common",
"srcPath": "./shared_common",
"targets": [{ "name": "default", "applyToProducts": ["default"] }]
}
]
}
第三步:从Entry跳转到Feature
// entry/src/main/ets/pages/Index.ets
import { common, abilityManager } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { bundleManager } from '@kit.AbilityKit';
@Entry
@Component
struct IndexPage {
// 检查Feature模块是否已安装
async isFeatureInstalled(moduleName: string): Promise<boolean> {
try {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE
);
const installedModules = bundleInfo.hapModuleInfos?.map(m => m.name) || [];
return installedModules.includes(moduleName);
} catch (error) {
console.error(`检查模块安装状态失败: ${error}`);
return false;
}
}
// 跳转到直播Feature
async navigateToLive(): Promise<void> {
const moduleName = 'feature_live';
const isInstalled = await this.isFeatureInstalled(moduleName);
if (!isInstalled) {
// Feature未安装,需要先安装
console.info('直播模块未安装,正在安装...');
const installed = await this.installFeature(moduleName);
if (!installed) {
console.error('直播模块安装失败');
return;
}
}
// 跳转到Feature的Ability
const context = getContext(this) as common.UIAbilityContext;
const want = {
deviceId: '',
bundleName: 'com.example.myapp',
abilityName: 'LiveAbility',
moduleName: moduleName
};
try {
await context.startAbility(want);
console.info('成功跳转到直播模块');
} catch (err) {
const error = err as BusinessError;
console.error(`跳转失败: ${error.code} - ${error.message}`);
}
}
// 安装Feature模块
async installFeature(moduleName: string): Promise<boolean> {
try {
// 通过应用市场安装(需要AGC配置)
// 这里使用installer模块进行安装
const context = getContext(this) as common.UIAbilityContext;
const installer = await bundleManager.getBundleInstaller();
// 构造安装参数
const installParam: bundleManager.InstallParam = {
userId: 100,
installFlag: bundleManager.InstallFlag.NORMAL,
isKeepData: false
};
// 注意:实际安装需要从应用市场获取Feature的HAP路径
// 这里是示意代码,实际路径需要从AGC获取
const hapPaths = [`/data/storage/el2/base/haps/${moduleName}.hap`];
await installer.install(hapPaths, installParam);
console.info(`${moduleName} 安装成功`);
return true;
} catch (error) {
console.error(`${moduleName} 安装失败: ${error}`);
return false;
}
}
build() {
Column() {
Text('首页')
.fontSize(28)
.fontWeight(FontWeight.Bold)
Button('进入直播间')
.margin({ top: 20 })
.onClick(() => this.navigateToLive())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
进阶用法:Feature包安装状态管理
在实际项目中,你需要一个统一的Feature管理器来处理安装状态、下载进度、错误重试:
// shared_common/src/main/ets/manager/FeatureManager.ets
// Feature模块管理器
import { bundleManager } from '@kit.AbilityKit';
import { common } from '@kit.AbilityKit';
import { emitter } from '@kit.BasicServicesKit';
// Feature模块定义
interface FeatureModule {
name: string; // 模块名
displayName: string; // 显示名称
abilityName: string; // 入口Ability名
description: string; // 描述
estimatedSize: string; // 预估大小
requiredHsp: string[]; // 依赖的HSP模块
}
// 安装状态
enum FeatureInstallState {
NOT_INSTALLED = 'not_installed',
INSTALLING = 'installing',
INSTALLED = 'installed',
INSTALL_FAILED = 'install_failed'
}
// 安装进度事件
interface InstallProgressEvent {
moduleName: string;
state: FeatureInstallState;
progress: number; // 0-100
error?: string;
}
// 支持的事件ID
const EVENT_FEATURE_INSTALL = 10001;
export class FeatureManager {
private static instance: FeatureManager | null = null;
private context: common.UIAbilityContext | null = null;
private installStates: Map<string, FeatureInstallState> = new Map();
private installProgress: Map<string, number> = new Map();
// 注册所有Feature模块
private readonly featureRegistry: FeatureModule[] = [
{
name: 'feature_live',
displayName: '直播',
abilityName: 'LiveAbility',
description: '观看和参与直播',
estimatedSize: '8MB',
requiredHsp: ['shared_common']
},
{
name: 'feature_social',
displayName: '社区',
abilityName: 'SocialAbility',
description: '社区互动与内容分享',
estimatedSize: '12MB',
requiredHsp: ['shared_common']
},
{
name: 'feature_ai',
displayName: 'AI助手',
abilityName: 'AiAbility',
description: '智能对话与AI功能',
estimatedSize: '15MB',
requiredHsp: ['shared_common']
}
];
private constructor() {}
static getInstance(): FeatureManager {
if (!FeatureManager.instance) {
FeatureManager.instance = new FeatureManager();
}
return FeatureManager.instance;
}
init(context: common.UIAbilityContext): void {
this.context = context;
// 初始化所有模块的安装状态
this.refreshInstallStates();
}
// 刷新所有Feature的安装状态
async refreshInstallStates(): Promise<void> {
try {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE
);
const installedModules = new Set(
bundleInfo.hapModuleInfos?.map(m => m.name) || []
);
for (const feature of this.featureRegistry) {
const state = installedModules.has(feature.name)
? FeatureInstallState.INSTALLED
: FeatureInstallState.NOT_INSTALLED;
this.installStates.set(feature.name, state);
}
} catch (error) {
console.error(`刷新安装状态失败: ${error}`);
}
}
// 获取Feature安装状态
getInstallState(moduleName: string): FeatureInstallState {
return this.installStates.get(moduleName) || FeatureInstallState.NOT_INSTALLED;
}
// 获取安装进度
getInstallProgress(moduleName: string): number {
return this.installProgress.get(moduleName) || 0;
}
// 获取所有Feature列表(含状态)
getAllFeatures(): Array<FeatureModule & { state: FeatureInstallState; progress: number }> {
return this.featureRegistry.map(f => ({
...f,
state: this.getInstallState(f.name),
progress: this.getInstallProgress(f.name)
}));
}
// 安装Feature
async installFeature(moduleName: string): Promise<boolean> {
const feature = this.featureRegistry.find(f => f.name === moduleName);
if (!feature) {
console.error(`未注册的Feature: ${moduleName}`);
return false;
}
// 检查是否已安装
if (this.getInstallState(moduleName) === FeatureInstallState.INSTALLED) {
return true;
}
// 更新状态为安装中
this.installStates.set(moduleName, FeatureInstallState.INSTALLING);
this.installProgress.set(moduleName, 0);
this.emitProgress(moduleName, FeatureInstallState.INSTALLING, 0);
try {
// 模拟下载进度(实际应从应用市场获取)
for (let progress = 0; progress <= 100; progress += 10) {
await this.delay(200); // 模拟下载耗时
this.installProgress.set(moduleName, progress);
this.emitProgress(moduleName, FeatureInstallState.INSTALLING, progress);
}
// 执行安装
// 实际场景中,这里调用bundleManager的安装接口
// 或者通过应用市场的SDK触发安装
// 安装成功
this.installStates.set(moduleName, FeatureInstallState.INSTALLED);
this.installProgress.set(moduleName, 100);
this.emitProgress(moduleName, FeatureInstallState.INSTALLED, 100);
console.info(`Feature ${moduleName} 安装成功`);
return true;
} catch (error) {
this.installStates.set(moduleName, FeatureInstallState.INSTALL_FAILED);
this.emitProgress(moduleName, FeatureInstallState.INSTALL_FAILED, 0, String(error));
console.error(`Feature ${moduleName} 安装失败: ${error}`);
return false;
}
}
// 跳转到Feature
async navigateToFeature(moduleName: string): Promise<boolean> {
const feature = this.featureRegistry.find(f => f.name === moduleName);
if (!feature) return false;
// 确保已安装
const state = this.getInstallState(moduleName);
if (state !== FeatureInstallState.INSTALLED) {
const installed = await this.installFeature(moduleName);
if (!installed) return false;
}
// 跳转
if (!this.context) return false;
const want = {
deviceId: '',
bundleName: this.context.abilityInfo.bundleName,
abilityName: feature.abilityName,
moduleName: moduleName
};
try {
await this.context.startAbility(want);
return true;
} catch (error) {
console.error(`跳转Feature失败: ${error}`);
return false;
}
}
// 发送进度事件
private emitProgress(
moduleName: string,
state: FeatureInstallState,
progress: number,
error?: string
): void {
emitter.emit({ eventId: EVENT_FEATURE_INSTALL }, {
data: {
moduleName,
state,
progress,
error
}
});
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
完整示例:Feature管理UI
// entry/src/main/ets/pages/FeatureListPage.ets
import { FeatureManager, FeatureInstallState, EVENT_FEATURE_INSTALL } from '@ohos/shared_common';
import { emitter } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct FeatureListPage {
private featureManager: FeatureManager = FeatureManager.getInstance();
@State features: Array<{
name: string;
displayName: string;
description: string;
estimatedSize: string;
state: FeatureInstallState;
progress: number;
}> = [];
aboutToAppear(): void {
// 初始化FeatureManager
this.featureManager.init(getContext(this) as common.UIAbilityContext);
// 加载Feature列表
this.refreshFeatures();
// 监听安装进度
emitter.on({ eventId: EVENT_FEATURE_INSTALL }, (eventData) => {
this.refreshFeatures();
});
}
aboutToDisappear(): void {
emitter.off(EVENT_FEATURE_INSTALL);
}
refreshFeatures(): void {
this.features = this.featureManager.getAllFeatures();
}
// 获取状态文字
getStateText(state: FeatureInstallState): string {
switch (state) {
case FeatureInstallState.NOT_INSTALLED:
return '未安装';
case FeatureInstallState.INSTALLING:
return '安装中...';
case FeatureInstallState.INSTALLED:
return '已安装';
case FeatureInstallState.INSTALL_FAILED:
return '安装失败';
default:
return '未知';
}
}
// 获取按钮文字
getButtonText(state: FeatureInstallState): string {
switch (state) {
case FeatureInstallState.NOT_INSTALLED:
return '安装并打开';
case FeatureInstallState.INSTALLING:
return '安装中...';
case FeatureInstallState.INSTALLED:
return '打开';
case FeatureInstallState.INSTALL_FAILED:
return '重试';
default:
return '打开';
}
}
build() {
Column() {
// 标题
Text('更多功能')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// Feature列表
List() {
ForEach(this.features, (feature: typeof this.features[0]) => {
ListItem() {
Row() {
// 左侧信息
Column() {
Text(feature.displayName)
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text(feature.description)
.fontSize(14)
.fontColor('#999999')
.margin({ top: 4 })
Text(`${feature.estimatedSize} · ${this.getStateText(feature.state)}`)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 右侧按钮
Button(this.getButtonText(feature.state))
.fontSize(14)
.height(36)
.enabled(feature.state !== FeatureInstallState.INSTALLING)
.onClick(async () => {
if (feature.state === FeatureInstallState.INSTALLED) {
// 已安装,直接打开
await this.featureManager.navigateToFeature(feature.name);
} else {
// 未安装,先安装再打开
const installed = await this.featureManager.installFeature(feature.name);
if (installed) {
await this.featureManager.navigateToFeature(feature.name);
}
}
this.refreshFeatures();
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
.margin({ bottom: 12 })
})
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#F5F5F5')
}
}
踩坑与注意事项
坑1:Feature安装后跳转失败
Feature刚安装完,立即跳转可能失败。因为系统需要时间注册新安装的Ability,跳转太早会报"ability not found"。
解法:安装成功后等待一小段时间再跳转,或者先查询Ability是否已注册:
// 安装后等待Ability注册
async function waitAbilityReady(moduleName: string, abilityName: string): Promise<boolean> {
const maxRetries = 5;
const retryInterval = 500; // 500ms
for (let i = 0; i < maxRetries; i++) {
try {
const want = {
action: 'action.system.home',
entities: ['entity.system.home'],
moduleName: moduleName,
abilityName: abilityName
};
const abilities = await bundleManager.queryAbilityInfo(want);
if (abilities.length > 0) return true;
} catch (e) {
// 还没注册,继续等待
}
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
return false;
}
坑2:Feature依赖的HSP未安装
Feature模块依赖HSP共享模块,如果HSP没有随Entry安装,Feature安装后会运行失败。
解法:确保HSP的deliveryWithInstall: true,这样HSP会随Entry一起安装。Feature模块的oh-package.json5中声明的HSP依赖,必须和Entry一致。
坑3:系统自动卸载Feature导致体验中断
系统在存储不足时可能自动卸载不常用的Feature包。用户下次使用时需要重新下载,体验中断。
解法:
- 在Feature入口处检查安装状态,未安装时自动触发安装
- 安装过程给用户明确的进度提示
- 关键Feature可以设为
deliveryWithInstall: true,随应用安装
坑4:Feature模块间互相依赖
Feature A依赖Feature B,但Feature B没安装。这种跨Feature依赖会导致复杂的安装链。
解法:Feature模块之间不应该互相依赖。如果两个Feature有共享逻辑,把共享逻辑抽到HSP中。Feature只依赖HSP,不依赖其他Feature。
坑5:调试时Feature安装路径问题
开发调试时,Feature的HAP文件路径和发布时不同。调试时需要手动指定HAP路径,否则安装失败。
解法:调试时使用DevEco Studio的"Deploy"功能,IDE会自动处理所有模块的安装。命令行调试时,需要手动安装所有HAP:
# 安装Entry
hdc install entry/build/default/outputs/default/entry-default-signed.hap
# 安装HSP
hdc install shared_common/build/default/outputs/default/shared_common-default-signed.hsp
# 安装Feature
hdc install feature_live/build/default/outputs/default/feature_live-default-signed.hap
HarmonyOS 6适配说明
HarmonyOS 6对动态特性包做了以下增强:
-
Feature模块预加载:HarmonyOS 6支持Feature模块的预加载(Preload)。应用可以在空闲时预先下载Feature包但不安装,用户触发时秒级安装。这解决了"首次使用需要等待下载"的体验问题。
-
Feature模块卸载回调:当系统卸载Feature模块时,会通知应用,应用可以保存状态数据。用户下次使用时,可以恢复之前的操作状态。
-
多Feature并行安装:HarmonyOS 6支持同时下载和安装多个Feature包,大幅缩短批量安装时间。
-
Feature包增量更新:Feature包更新时支持增量下载,只下载变化的部分,减少下载量。
-
离线Feature管理:新增
bundleManager.getUninstalledModules()接口,可以查询已卸载但仍有残留数据的Feature模块,用于清理存储空间。
总结
Feature包是HarmonyOS应用架构的"减负"利器。核心功能随装随用,扩展功能按需加载——用户体验好,包体积小。
但Feature包不是万能的,它增加了架构复杂度。要不要拆、怎么拆,要看具体场景:
- 适合拆:低频功能、体积大、相对独立(如直播、AI助手、社区)
- 不适合拆:高频功能、体积小、强依赖主流程(如搜索、购物车)
记住:拆是为了更好的体验,不是为了拆而拆。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 概念不难,但安装流程、状态管理、模块通信需要仔细处理 |
| 使用频率 | ⭐⭐⭐ 中大型项目必用,小型项目可不用 |
| 重要程度 | ⭐⭐⭐⭐ 直接影响首次安装体验和用户留存 |
- 点赞
- 收藏
- 关注作者
评论(0)