Administrator
发布于 2026-09-02 / 2,007 阅读
0
0

鸿蒙动态化方案:从卡片到原子化服务到热更新

系列文章《鸿蒙开发从入门到专家》第 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. 灰度与回滚

动态化一旦出问题,影响的不只是某一个版本,而是当前所有在线用户。所以一定要做灰度:

机制

说明

用户分桶

按 deviceId 末位 hash 取桶

配置开关

服务端开关控制是否推送

客户端防降级

检测到异常立刻卸载新 HSP

回滚流程要演练过。建议在 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,客户端做埋点上报。

  • 命中实验的用户走新视图,其他人走旧视图。

六、动态化方案的安全风险

代码级动态化的高自由度意味着高风险,必须配套防护:

  1. 强校验签名:远端下发的 HSP 必须经过开发者私钥签名,客户端严格校验,未签名拒绝加载。

  2. HTTPS Pinning:下载源不能被中间人劫持。

  3. 回滚机制:版本号必须可比对,遇到异常版本能强制回退。

  4. 审计日志:记录每次动态化的触发、操作人、影响范围。

  5. 应急开关:服务端一键关闭所有动态化能力(紧急场景)。

合规上需要看应用所在行业的要求。金融/医疗类应用可能根本不允许代码级动态化,必须遵守。

七、案例:电商 App 的卡片 + 原子化 + 远端下发组合用法

一个典型电商 App 的动态化组合:

场景

形态

理由

桌面"今日特惠"

服务卡片

静态展示,点击进主 App

搜索直达"领券"

原子化服务

用户搜索无需下载

双11 主会场动态页面

远端 HSP + 远端配置

大促期间常改版,避免发版拥堵

商品图/活动图

远端 CDN

设计随时改,无需重发版

灰度策略

配置中心下发

千人千面展示

一个具体的会场页面示例:

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》(形式不限,思路相通)


评论