系列文章《鸿蒙开发从入门到专家》第 35 篇 · 高级
动态化是每个移动操作系统都绕不开的话题:装个 App 等半天,发个版更要等几天。鸿蒙在动态化上给出了几条独有的路径——服务卡片、原子化服务、远端下发的 HSP。和 Android 的"插件化"+“Tinker 热修"比,鸿蒙的设计更像"操作系统层就把这件事想通了”。
这一篇把鸿蒙动态化相关的几种技术形态整理清楚,包括它们各自适用场景、关键技术点、以及容易踩的坑。
一、动态化的两个层次
先做个分层,避免混淆:
资源/配置级动态化:图片、字符串、JSON 配置可以不下发代码就更新。
代码级动态化:界面逻辑、组件树可以从远端下发。
鸿蒙官方推荐的方式是资源级,而代码级动态化需要走严格的审核流程。下面对各方案逐一展开。
二、服务卡片:桌面的微入口
服务卡片(Service Widget)是桌面/锁屏/控制中心上的一个"小窗口"。它不是一个完整应用,是一个运行在卡片进程里的轻量 Ability。开发者提交的是一个 WidgetCard 类型的 Ability + 对应的 widget 描述文件。
1. 一个最小卡片
// widget/src/main/ets/WidgetCard.ets
@Entry
@Component
struct WidgetCard {
build() {
Column() {
Text('温度')
.fontSize(14)
.fontColor('#666')
Text('23°C')
.fontSize(28)
.fontWeight(FontWeight.Bold)
Text('晴')
.fontSize(12)
.fontColor('#999')
}
.padding(12)
.backgroundColor('#FFF')
.borderRadius(12)
}
}
卡片配置 widget/src/main/resources/base/profile/weather_widget.json:
{
"src": ["WidgetCard"],
"windowSize": "2x2",
"refreshTime": 1800,
"isDynamic": false
}
refreshTime 表示系统建议刷新间隔(秒),不是真正的定时器——卡片运行时长受限,频繁刷会耗电。
2. 卡片与主应用的关系
卡片是独立的 Ability,有自己的进程和权限。最常用的通信方式:
URL 跳转:卡片点击通过
routerEvent跳到应用主界面。应用间共享:通过
sharedPreferences共享数据。后台服务:卡片需要数据时调用主应用的服务。
3. 卡片的限制
运行时长通常不超过 5 秒,能做的逻辑不能太重。
不支持所有 ArkUI 组件,部分动效/手势 API 不可用。
单个应用卡片数受限(一般 5 张以内)。
卡片里的图片/网络请求要走主应用代理,不能直接 fetch。
适合"看一眼就走"的场景:天气、待办、股票、记账、新闻摘要。不适合复杂交互。
三、原子化服务:免安装的小程序
原子化服务是鸿蒙最特殊的一种形态。它不是"应用",但可以独立存在;用户不需要下载安装包,通过搜索、扫码、推荐卡片就能用。
1. 项目结构
atomicService/
src/
main/
ets/
pages/
Index.ets
resources/
base/profile/
form_config.json
form_config.json:
{
"forms": [
{
"name": "demo",
"description": "示例卡片",
"type": "Service",
"src": "./pages/Index.ets",
"window": {
"designWidth": 720,
"designHeight": 480
}
}
]
}
标识为 type: Service 即可被识别为原子化服务。
2. 入口发现
原子化服务的发现路径很丰富:
负一屏 / 服务中心。
全局搜索。
桌面推荐的"小艺建议"。
浏览器/短信里的链接跳转。
扫码、碰一碰。
这意味着开发者要做好入口引导——主 App 第一次启动时主动推荐用户"添加到桌面"“添加到负一屏”,后期留存数据会非常好看。
3. 与主 App 的关系
原子化服务和主 App 可以是同一个 bundleName(轻量集成),也可以是独立项目后通过 url scheme 互通:
// 主 App 启动时
import { wantConstant } from '@kit.AbilityKit';
const want = {
bundleName: 'com.example.myapp',
abilityName: 'EntryAbility',
uri: 'atomic://path/to/service',
};
startAbility(want);
注意:开发原子化服务不代表能"绕过应用商店审核",所有动态内容还是要走合规。
四、远端下发的 HSP:真正的动态化
下面这一种是真正的"代码级动态化"——服务端下发一个新的 HSP,用户运行时拉取并加载。这是鸿蒙相比 Android 的一大优势:系统级支持,不像 Tinker 要修 dex 那样。
1. 远端 HSP 的能力范围
远端 HSP 可以包含:
ArkTS 组件。
自定义组件库。
业务逻辑。
资源(限图片、字符串)。
不可以包含:
新的权限声明。
新的 Ability 注册。
新的 provider/subscribe 类。
系统级 API 调用。
这意味着远端 HSP 主要解决"UI/逻辑变更"而不是"权限新增"。
2. 拉取并安装 HSP
import { bundleManager } from '@kit.AppKit';
import { rpc } from '@kit.RemoteCommunicationKit';
async function fetchAndInstallHsp(url: string): Promise<boolean> {
// 1. 下载 HSP
const fileResp = await rpc.fetch(url);
const buffer = await fileResp.arrayBuffer();
// 2. 写入沙盒
const file = await getContext().filesDir + '/update.hsp';
// ...省略写入
// 3. 调用系统能力安装
await bundleManager.install(file, { installFlag: 1 });
return true;
}
成功后,新 HSP 会被加载到应用进程;调用方用 import('feature-new') 异步加载。
3. 灰度与回滚
动态化一旦出问题,影响的不只是某一个版本,而是当前所有在线用户。所以一定要做灰度:
回滚流程要演练过。建议在 CI 里搞一套"灰度发布→监控→回滚"的剧本,能让运维和开发都熟悉。
五、资源级动态化:图片、配置、A/B
代码级动态化需要严格控制,但资源/配置级动态化可以很灵活。
1. 远端配置中心
常见做法是把"活动页文案、优惠券信息、灰度参数"做成 JSON 配置下发:
// util/RemoteConfig.ets
import { http } from '@kit.NetworkKit';
const cached = new Map<string, string>();
export async function fetchConfig(key: string, fallback: string): Promise<string> {
if (cached.has(key)) return cached.get(key)!;
try {
const resp = await http.request(`https://config.example.com/${key}`);
const value = JSON.parse(resp.result).value;
cached.set(key, value);
return value;
} catch {
return fallback;
}
}
2. 图片 CDN 切边
应用内不放图片意味着包体积小、运营修改快。所有图片都从 CDN 拉:
Image(`https://cdn.example.com/banner/${bannerId}.webp`)
.width('100%')
.objectFit(ImageFit.Contain)
HAP/HSP 内只保留兜底图(如离线 Logo)。
3. A/B 实验
把界面/流程做 A/B 测试的常见方案:
同一个接口返回不同
viewType,前端按类型渲染。服务端下发实验分组 ID,客户端做埋点上报。
命中实验的用户走新视图,其他人走旧视图。
六、动态化方案的安全风险
代码级动态化的高自由度意味着高风险,必须配套防护:
强校验签名:远端下发的 HSP 必须经过开发者私钥签名,客户端严格校验,未签名拒绝加载。
HTTPS Pinning:下载源不能被中间人劫持。
回滚机制:版本号必须可比对,遇到异常版本能强制回退。
审计日志:记录每次动态化的触发、操作人、影响范围。
应急开关:服务端一键关闭所有动态化能力(紧急场景)。
合规上需要看应用所在行业的要求。金融/医疗类应用可能根本不允许代码级动态化,必须遵守。
七、案例:电商 App 的卡片 + 原子化 + 远端下发组合用法
一个典型电商 App 的动态化组合:
一个具体的会场页面示例:
import { fetchConfig } from '../util/RemoteConfig';
@Entry
@Component
struct BigSalePage {
@State banners: string[] = [];
async aboutToAppear() {
this.banners = JSON.parse(await fetchConfig('bigsale.banners', '[]'));
}
build() {
Column() {
// 顶部 Hero 区,从 CDN 拉
Image('https://cdn.example.com/bigsale/hero.webp')
.width('100%')
.height(200)
// 商品分区,根据配置切视图
ForEach(this.banners, (banner) => {
Image(banner)
.width('100%')
.objectFit(ImageFit.Cover)
})
}
}
}
整个页面几乎没有"硬编码"——所有内容都可以从远端拉,即使代码不变也能通过配置变更完成活动切换。
小结
动态化不是一招鲜,是组合拳:
资源/配置 随时改。
卡片 用作桌面前台触点。
原子化服务 做轻量入口。
远端 HSP 满足代码级变更。
但每一种都有适用边界,强动态化一定要配套灰度、回滚、安全。下篇我们顺着这条思路往下走——动态化解决了"主包不变给老用户、新能力给新用户"的能力引入,但用户是从哪儿来的?下一篇讲鸿蒙应用怎么和小程序、快应用整合做拉新促活。
参考阅读
鸿蒙开发者文档《服务卡片开发指南》
鸿蒙开发者文档《原子化服务介绍》
鸿蒙开发者文档《应用热更新机制说明》
《Design for the Long Run》(形式不限,思路相通)