HarmonyOS 跨设备协同技术全景:用 CastDeck 拆解手机与 PC 的状态同步
HarmonyOS 跨设备协同技术全景:用 CastDeck 拆解手机与 PC 的状态同步
一、引子:一次翻页背后发生了什么
先抛一个问题。
在一个跨屏演讲应用里,演讲者用手机点了一下"下一页",讲台上的 PC 大屏随即翻到下一页。这个看似简单的动作背后,一条完整的技术链路被触发了:
- 手机 App 修改了"当前页"这个状态;
- 状态被写入一个跨设备共享的数据载体;
- 系统通过某种机制,把这次变更同步到了另一台设备;
- 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 里为了辅助判断,状态中带了 updatedBy 和 updatedAt,可用于识别"这次变更是谁、什么时候发起的",在需要时做更细的处理(比如忽略比本地更旧的变更)。对于演讲这种低并发、语义明确的场景,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 大屏翻页,全链路发生了什么:
- 应用层(手机):用户点击,
DeckController.next()修改currentIndex,打包成DeckState,调SyncBus.put()。 - 框架层:
put写入分布式 KV 库,autoSync触发同步。 - 基础层:分布式软总线把这次变更经由已建立的设备通道,传输到 PC 端的同名库。
- 框架层(PC):PC 端库收到变更,触发
on('dataChange')回调。 - 应用层(PC):回调解析出新的
DeckState,推送到AppStorage,大屏页面通过@Watch('stateTick')感知并刷新——翻页完成。
整条链路里,开发者真正编写的只有第 1 步和第 5 步(应用层),中间的传输、同步、组网全部由系统承担。这就是分层的意义:开发者聚焦业务状态的定义与消费,系统负责状态的跨设备流转。
九、结语:理解分层,才能用好协同
鸿蒙跨设备协同不是一个单一 API,而是一套自底向上的技术栈:软总线解决"设备怎么连",分布式数据管理解决"状态怎么同步",安全模型解决"数据能不能流、流向谁",应用层则决定"共享什么、如何呈现"。
对开发者而言,用好这套能力的关键,是理解每一层的职责边界:
- 知道软总线是透明的,就不会去手写设备连接;
- 知道
autoSync+dataChange是同步的核心机制,就能设计出干净的状态流转; - 知道可信组网是安全前提,就不会在"为什么不同步"上误判成代码问题;
- 知道跨端适配包含视觉校准,就不会把手机 UI 直接照搬到大屏。

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



所有评论(0)