HarmonyOS 跨设备协同技术全景:用 CastDeck 拆解手机与 PC 的状态同步

一、引子:一次翻页背后发生了什么

先抛一个问题。

在一个跨屏演讲应用里,演讲者用手机点了一下"下一页",讲台上的 PC 大屏随即翻到下一页。这个看似简单的动作背后,一条完整的技术链路被触发了:

  1. 手机 App 修改了"当前页"这个状态;
  2. 状态被写入一个跨设备共享的数据载体;
  3. 系统通过某种机制,把这次变更同步到了另一台设备;
  4. PC 端监听到变更,刷新了大屏 UI。

这条链路里,涉及设备如何互相发现、如何建立可信连接、数据如何跨设备流转、状态如何保持一致、UI 如何跨形态适配等一系列问题。HarmonyOS 把这些能力分层封装,让开发者能以较低的成本实现协同。

这篇文章,就自顶向下把这套技术栈拆开讲清楚,并用跨屏演讲台 CastDeck 作为贯穿始终的案例。
在这里插入图片描述
在这里插入图片描述

二、整体分层:协同能力的技术栈

鸿蒙的跨设备协同能力,大致可以分为三层:

在这里插入图片描述

越往下越靠近系统、对开发者越透明;越往上越靠近业务、需要开发者做设计决策。理解这个分层,是理解整个协同体系的钥匙。下面从底往上讲。

三、基础层:分布式软总线(DSoftBus)

分布式软总线是整个协同大厦的地基。它要解决的是一个最根本的问题:一堆异构设备(手机、平板、PC、手表),凭什么能像插在同一条总线上的模块一样互相通信?

3.1 它做了三件事

其一,设备发现。 软总线在同一局域网 / 近场范围内,自动发现其他运行鸿蒙的设备。开发者不需要手写扫描逻辑,系统持续维护一张"附近有哪些可用设备"的动态列表。

其二,组网与连接。 发现设备后,软总线负责建立设备间的连接。它屏蔽了底层的传输方式差异——不管两台设备是通过 Wi-Fi、蓝牙还是其他链路互联,上层看到的都是一条统一的逻辑通道。这就是"软"总线的含义:用软件抽象,把物理链路的异构性隐藏掉。

其三,数据传输。 建立连接后,软总线提供高带宽、低时延的数据传输能力,供上层的分布式数据管理、分布式文件系统等使用。

3.2 对开发者意味着什么

关键在于:这一层几乎对开发者透明。 你不会直接调用软总线的 API 去"连接某台设备"。你只是使用上层的分布式数据库或分布式对象,而软总线在幕后完成了设备发现和连接。

这正是鸿蒙的设计哲学:把通信管道这类与业务无关的复杂度,彻底沉到系统层。 在 CastDeck 里,我从未写过一行"连接 PC"的代码——我只是打开了一个分布式数据库,软总线负责让两端"在同一条总线上"。

四、框架层:分布式数据管理

有了软总线这条"路",接下来的问题是:状态数据如何在设备间流转并保持一致? 这由分布式数据管理层负责,CastDeck 用的是其中的分布式键值数据库 distributedKVStore

4.1 为什么选分布式 KV

演讲的核心状态很简单——当前第几页、是否计时、计时起点。这类结构化程度不高、读写频繁、需要多端共享的状态,用键值库存储最合适:把整份状态序列化成 JSON,存进一个 key 即可。

export interface DeckState {
  currentIndex: number;  // 当前页
  running: boolean;      // 是否计时
  startedAt: number;     // 计时起点
  updatedBy: string;     // 更新来源设备
  updatedAt: number;     // 更新时间戳
}

4.2 建立分布式库

const config: distributedKVStore.KVManagerConfig = {
  context: context,
  bundleName: 'com.example.castdeck'
};
this.kvManager = distributedKVStore.createKVManager(config);

const options: distributedKVStore.Options = {
  createIfMissing: true,
  autoSync: true,                                          // 关键:自动同步
  kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
  securityLevel: distributedKVStore.SecurityLevel.S1
};
this.kvStore = await this.kvManager.getKVStore(STORE_ID, options);

这里有几个决定同步行为的关键配置,值得逐个说明。

kvStoreType: SINGLE_VERSION(单版本):表示同一个 key 只保留最新一份值,不做多版本冲突合并。演讲状态是"最新即正确"的语义——谁最后操作,就以谁为准——所以单版本正合适。如果是协同文档那种需要合并双方编辑的场景,就要考虑设备协同版本(Device Collaboration)等其他类型。

autoSync: true(自动同步):开启后,任一端对库的写入会自动同步到组网内的其他设备,无需手动触发 sync()。这是实现"手机翻页、PC 跟随"的核心开关。

securityLevel: S1(安全等级):分布式数据必须声明安全等级,系统据此决定数据可以在什么安全级别的设备间流转。等级越高,对设备的安全要求越严。演讲状态不敏感,S1 足够。
在这里插入图片描述

4.3 同步模型:写入即广播,变更即回调

分布式 KV 的同步是双向的,围绕"写"和"订阅变更"两个动作:

写入端:任一设备调 put 写入状态:

await this.kvStore.put('deck_state', JSON.stringify(state));

autoSync 下,这次写入会经由软总线广播到组网内其他设备的同名库。

订阅端:所有设备通过 on('dataChange') 订阅变更,收到远端(或本地)的数据变化:

this.kvStore.on('dataChange',
  distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
  (data: distributedKVStore.ChangeNotification) => {
    const entries = [...(data.insertEntries ?? []), ...(data.updateEntries ?? [])];
    for (const e of entries) {
      if (e.key === 'deck_state') {
        const state = JSON.parse(e.value.value as string) as DeckState;
        this.notify(state);   // 通知 UI 刷新
      }
    }
  });

SUBSCRIBE_TYPE_ALL 表示既订阅本地变更也订阅远端变更。这样无论状态是被本机改的还是被对端改的,回调逻辑统一——这对写出"来源无关"的刷新代码非常重要。

4.4 一致性与冲突

单版本库在多端并发写入同一 key 时,遵循"最后写入者胜出"(Last-Write-Wins)。CastDeck 里为了辅助判断,状态中带了 updatedByupdatedAt,可用于识别"这次变更是谁、什么时候发起的",在需要时做更细的处理(比如忽略比本地更旧的变更)。对于演讲这种低并发、语义明确的场景,LWW 已经足够。

五、安全层:可信组网与权限

跨设备数据流转,绕不开安全。鸿蒙在这里设了两道关卡。

5.1 权限声明

分布式数据同步需要显式申请 ohos.permission.DISTRIBUTED_DATASYNC 权限,在 module.json5 中声明,并在运行时向用户请求:

"requestPermissions": [
  {
    "name": "ohos.permission.DISTRIBUTED_DATASYNC",
    "reason": "$string:reason_distributed",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  }
]
const atManager = abilityAccessCtrl.createAtManager();
await atManager.requestPermissionsFromUser(context, ['ohos.permission.DISTRIBUTED_DATASYNC']);

5.2 可信组网:同账号是前提

比权限更根本的一道关卡是可信组网。设备之间要能同步数据,前提是它们处于一个可信的分布式网络中——通常意味着登录同一华为账号、并处于可互信的连接环境。这是系统层的安全边界:你的数据不会莫名其妙地流到一台陌生设备上。
在这里插入图片描述

这一点在实际开发中有重要含义:分布式同步的成败,不仅取决于代码,还取决于设备是否满足可信组网的前提。 一台真机加一个 IDE 模拟器,往往无法构成可信组网——两端即使都成功打开了分布式库,也各自为战、互不同步。这不是代码 bug,而是安全模型的必然结果。理解这一层,能避免在"为什么不同步"上误判方向。

六、应用层之一:状态同步的设计模式

系统能力再强,也要靠合理的应用层设计才能用好。CastDeck 在状态同步上用了几个值得复用的模式。

6.1 单一状态对象

不要把"当前页"“是否计时”"计时起点"拆成多个 key 分别同步——那样容易出现"翻页同步了、计时没同步"的中间不一致态。而是把它们打包成一个 DeckState 对象,作为一个整体同步。一次写入即一次完整状态快照,天然避免了字段间的不一致。

6.2 时间戳信号驱动刷新

在应用内,我用 AppStorage 把状态暴露为响应式变量,页面用 @StorageProp 订阅。这里有个技巧:额外维护一个每次变更都更新的时间戳 stateTick

private static publish(): void {
  AppStorage.setOrCreate('curIndex', this.curIndex);
  AppStorage.setOrCreate('running', this.running);
  AppStorage.setOrCreate('stateTick', Date.now());   // 变更信号
}

页面 @Watch('stateTick') 监听这个信号,任何一次状态变更(无论翻页、计时还是重置)都会触发统一的刷新回调,而不必为每个字段分别写监听逻辑。这让 UI 刷新代码干净且不易漏。

6.3 同步层可插拔 + 本地降级

最重要的一个设计:把"状态怎么流转"抽象成一个独立的同步层(SyncBus),上层业务不关心底层是分布式还是本地。

static async put(state: DeckState): Promise<void> {
  const json = JSON.stringify(state);
  if (this.distributedReady && this.kvStore !== null) {
    try { await this.kvStore.put(KEY_STATE, json); return; }
    catch (e) { /* 落到本地兜底 */ }
  }
  AppStorage.setOrCreate('deckStateJson', json);   // 本地降级
  this.notify(state);
}

分布式库初始化失败(如设备不满足可信组网)时,自动降级为本地存储。这个设计的价值在于:它让应用在"协同能力不完整"时依然可用,且上层业务代码一行都不用改。这是一种防御性的架构——为能力的缺失预留退路,是跨设备应用工程化的重要一环。

七、应用层之二:跨端 UI 适配

协同的另一半是"多端适配"——同一套代码,要在手机竖屏和 PC 大屏上都表现良好。鸿蒙的 ArkUI 提供了一系列技术手段。
在这里插入图片描述

7.1 弹性布局优先

不写死尺寸,是跨端适配的第一原则。CastDeck 全程使用 layoutWeight、百分比宽高、Flex 自动换行,让内容根据可用空间自适应。例如演讲台的遥控按钮用 layoutWeight(1) 均分宽度,手机上紧凑、PC 上舒展,无需两套布局。

7.2 设备类型驱动的角色分配

通过 deviceInfo.deviceType 识别当前设备形态,据此推荐角色:

const t = deviceInfo.deviceType;   // 'phone' / 'tablet' / '2in1' ...

大屏形态(PC / 2in1)默认推荐"演示大屏"角色,手机默认"遥控/提词器"角色。这是"多端各司其职"在代码层的直接体现——同一个应用,根据运行设备的形态,呈现不同的功能重心。

对应地,module.json5 里声明支持的设备类型:

"deviceTypes": ["phone", "tablet", "2in1"]

7.3 安全区与全屏适配

不同设备的"非安全区"不同——手机有刘海和底部指示条,PC 有窗口边框。CastDeck 通过 getWindowAvoidArea 获取这些区域高度,存入全局状态,页面统一避让:

const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('safeTop', px2vp(top.topRect.height));

7.4 视觉标准的差异化

一个容易被忽视的适配点:大屏和手机的视觉标准不同。 大屏是给远处观众看的,必须高对比、大字号;手机是近距离私人查看,可以有更细腻的层次。CastDeck 的大屏演示页把要点文字用近白色加粗、字号拉到 34pt,而手机端的辅助信息可以用更柔和的灰蓝。跨端适配不只是布局弹性,还包括视觉参数按设备重新校准。

八、把三层串起来:一次翻页的完整旅程

现在回到开头那个问题——手机点"下一页",PC 大屏翻页,全链路发生了什么:

  1. 应用层(手机):用户点击,DeckController.next() 修改 currentIndex,打包成 DeckState,调 SyncBus.put()
  2. 框架层put 写入分布式 KV 库,autoSync 触发同步。
  3. 基础层:分布式软总线把这次变更经由已建立的设备通道,传输到 PC 端的同名库。
  4. 框架层(PC):PC 端库收到变更,触发 on('dataChange') 回调。
  5. 应用层(PC):回调解析出新的 DeckState,推送到 AppStorage,大屏页面通过 @Watch('stateTick') 感知并刷新——翻页完成。

整条链路里,开发者真正编写的只有第 1 步和第 5 步(应用层),中间的传输、同步、组网全部由系统承担。这就是分层的意义:开发者聚焦业务状态的定义与消费,系统负责状态的跨设备流转。

九、结语:理解分层,才能用好协同

鸿蒙跨设备协同不是一个单一 API,而是一套自底向上的技术栈:软总线解决"设备怎么连",分布式数据管理解决"状态怎么同步",安全模型解决"数据能不能流、流向谁",应用层则决定"共享什么、如何呈现"。

对开发者而言,用好这套能力的关键,是理解每一层的职责边界

  • 知道软总线是透明的,就不会去手写设备连接;
  • 知道 autoSync + dataChange 是同步的核心机制,就能设计出干净的状态流转;
  • 知道可信组网是安全前提,就不会在"为什么不同步"上误判成代码问题;
  • 知道跨端适配包含视觉校准,就不会把手机 UI 直接照搬到大屏。
    在这里插入图片描述

CastDeck 是一个小应用,但它完整地穿过了这套技术栈的每一层。当你理解了这一整条从软总线到 UI 的链路,"手机翻页、大屏跟随"就不再是魔法,而是一套清晰、可掌控的工程。这,正是把跨设备协同从"看起来很酷"做到"真正可靠"的基础。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐