跨端迁移:实现应用状态在手机与平板间无缝流转(63)
在鸿蒙HarmonyOS生态中跨端迁移应用接续是实现“人随场景走、服务随人走”的核心能力。它允许用户在手机上进行的操作如编辑文档、观看视频、浏览网页无缝流转至平板或智慧屏上继续且保持上下文状态完全一致。这一过程并非将运行内存直接搬运而是由分布式任务调度、分布式软总线和应用状态保存/恢复机制三者协同完成的。以下是跨端迁移的底层原理及代码。一、 核心运作机制状态快照与序列化源端设备如手机在发起迁移前系统会回调onContinue()接口。开发者需在此接口中将当前页面的业务数据如文档内容、光标位置、视频播放进度序列化为轻量级状态数据通常限制在 100KB 以内。软总线传输与任务拉起分布式软总线负责设备发现、可信认证并建立加密通道将状态数据传输至目标设备如平板。同时分布式任务调度负责在目标设备上拉起对应的 UIAbility。状态恢复与重建目标设备的 UIAbility 被拉起时系统通过onCreate()或onNewWant()接口将迁移数据传递给应用。应用反序列化这些数据重新构建 UI 界面并恢复业务状态。二、 跨端迁移代码实战1. 前置配置开启迁移能力在应用的module.json5中必须将continuable标签配置为true否则系统会识别该应用无法迁移。同时需申请分布式相关权限{ module: { requestPermissions: [ { name: ohos.permission.DISTRIBUTED_DATASYNC }, { name: ohos.permission.GET_DISTRIBUTED_DEVICE_INFO } ], abilities: [ { name: EntryAbility, continuable: true } ] } }2. 源端实现保存业务状态在源端 UIAbility 中重写onContinue接口将当前需要恢复的状态打包到wantParam中import { UIAbility, AbilityConstant } from kit.AbilityKit; export default class EntryAbility extends UIAbility { // 当用户触发跨设备迁移时系统会调用此方法 onContinue(wantParam: Recordstring, Object): AbilityConstant.OnContinueResult { try { // 1. 获取当前业务状态如文档编辑内容、光标位置 const currentState { docId: DOC_20260105, content: Hello from Phone!, cursorPos: 18 }; // 2. 将状态数据写入 wantParam注意数据大小需控制在 100KB 以内 wantParam[migrationData] JSON.stringify(currentState); console.info(状态保存成功准备迁移); return AbilityConstant.OnContinueResult.AGREE; // 同意迁移 } catch (error) { console.error(状态保存失败:, error); return AbilityConstant.OnContinueResult.REJECT; // 拒绝迁移 } } }3. 目标端实现恢复业务状态在目标端 UIAbility 中通过判断启动原因LaunchReason来提取数据并恢复 UIimport { UIAbility, AbilityConstant, Want } from kit.AbilityKit; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 判断是否为跨设备迁移触发的冷启动 if (launchParam.launchReason AbilityConstant.LaunchReason.CONTINUATION) { const dataStr want.parameters?.[migrationData] as string; if (dataStr) { const restoredState JSON.parse(dataStr); console.info(成功恢复迁移状态:, restoredState); // 将恢复的数据存入全局状态如 AppStorage供 UI 页面读取 AppStorage.setOrCreate(resumeData, restoredState); } } } }4. UI 层读取状态并渲染在 ArkUI 页面中通过aboutToAppear生命周期读取恢复的数据实现无缝衔接import { AppStorage } from kit.ArkUI; Entry Component struct EditorPage { State textContent: string ; State cursorPosition: number 0; aboutToAppear() { // 读取目标端 Ability 传递过来的恢复数据 const resumeData AppStorage.getRecordstring, Object(resumeData); if (resumeData) { this.textContent resumeData[content] as string; this.cursorPosition resumeData[cursorPos] as number; } } build() { Column() { TextInput({ text: this.textContent, placeholder: 继续编辑... }) .onChange((value) { this.textContent value; }) } .width(100%) .height(100%) .padding(20) } }三、 双向回迁Reversible Continuation在某些场景下如用户在平板上编辑了一半发现还是手机方便用户希望任务能从目标设备“回退”到源设备。鸿蒙提供了continueAbilityReversibly机制允许源设备在迁移后保持后台存活并支持随时回迁。发起可逆迁移源端调用continueAbilityReversibly代替普通的continueAbility。处理回迁通知源端需重写onRemoteTerminated回调。当目标设备完成任务并结束或者用户主动回迁时源端会收到此通知从而重新接管 UI 焦点。1. 源端发起可逆迁移在源设备如手机上开发者需要调用continueAbilityReversibly接口来发起迁移。与普通的continueAbility不同该接口会保留源端应用的生命周期使其在后台挂起等待回迁。import { UIAbility } from kit.AbilityKit; export default class SourceAbility extends UIAbility { // 发起双向回迁 startReversibleContinuation(targetDeviceId: string) { try { // 调用可逆迁移接口传入目标设备的 deviceId this.continueAbilityReversibly(targetDeviceId); console.info(已发起可逆迁移源端应用进入后台挂起状态); } catch (err) { console.error(发起可逆迁移失败:, err); } } }2. 目标端执行回迁操作当用户在目标设备如平板上完成当前任务希望将应用流转回源设备时目标端应用只需调用reverseContinueAbility接口即可。import { UIAbility } from kit.AbilityKit; export default class TargetAbility extends UIAbility { // 用户在平板上点击“返回手机继续”按钮时触发 onReturnToSourceClick() { try { // 触发回迁流程系统会将控制权交还给源设备 this.reverseContinueAbility(); console.info(已发起回迁目标端应用即将销毁); } catch (err) { console.error(回迁失败:, err); } } }3. 源端监听回迁通知并恢复 UI当目标设备触发回迁后源设备会收到系统的回调通知。在早期的 HarmonyOS 架构如基于 Java/JS 的 FA 模型中开发者需要重写onRemoteTerminated方法来感知这一事件并重新激活 UI。import { UIAbility } from kit.AbilityKit; export default class SourceAbility extends UIAbility { // 重写 onRemoteTerminated 回调 onRemoteTerminated() { console.info(收到目标端回迁通知源端应用重新接管 UI); // 在此处执行恢复前台焦点的逻辑 // 例如刷新当前页面状态、恢复视频播放、重新获取焦点等 this.restoreUIState(); } private restoreUIState() { // 恢复业务状态和 UI 焦点 console.info(UI 焦点已恢复用户可继续操作); } }四、 完整页面栈Navigation Stack迁移对于复杂的多级页面应用仅迁移当前页面的状态是不够的用户期望在目标设备上能继续点击“返回”回到上一级页面。栈序列化在onContinue中开发者需要将当前的navPathStack或 Router 栈连同每个页面的状态一并序列化打包。栈重建目标设备在onCreate中解析数据后不仅要恢复当前页面的 UI还要通过代码自动执行pushPath将历史页面栈重新压入实现真正的“无缝接续”。1、 基于 Navigation 路由的页面栈迁移对于使用Navigation组件构建的应用系统目前暂不支持自动恢复页面栈开发者需要手动获取栈快照、通过Want传递并在目标端手动重建。1. 源端获取并序列化 Navigation 页面栈在源端的onContinue生命周期中获取当前的NavPathStack快照并将其写入wantParamimport { AbilityConstant, UIAbility } from kit.AbilityKit; export default class EntryAbility extends UIAbility { onContinue(wantParam: Recordstring, Object): AbilityConstant.OnContinueResult { // 1. 从全局状态中获取当前的 NavPathStack let pathStack AppStorage.get(navPathStack) as NavPathStack; let navPathInfo pathStack.getPathStack(); // 获取页面栈快照数组 // 2. 将页面栈信息写入 wantParam 传递给目标端 wantParam[navPathStack] navPathInfo; console.info(Navigation 页面栈已打包准备迁移); return AbilityConstant.OnContinueResult.AGREE; } }2. 目标端解析数据并恢复栈在目标端的onCreate或onNewWant中读取栈数据存入AppStorageimport { AbilityConstant, UIAbility, Want } from kit.AbilityKit; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { if (launchParam.launchReason AbilityConstant.LaunchReason.CONTINUATION) { // 1. 读取目标端传递过来的 Navigation 页面栈快照 if (Array.isArray(want.parameters?.[navPathStack])) { AppStorage.setOrCreate(NavPathInfo, want.parameters[navPathStack] as ArrayNavPathInfo); } else { AppStorage.setOrCreate(NavPathInfo, []); } // 2. 恢复窗口状态 this.context.restoreWindowStage(new LocalStorage()); } } }3. UI 层自动重建页面栈在Navigation根页面的onPageShow生命周期中判断是否存在迁移数据若存在则自动压入历史页面import { AppStorage } from kit.ArkUI; Entry Component struct IndexPage { StorageProp(NavPathInfo) navPathInfo: ArrayNavPathInfo []; StorageLink(navPathStack) pageStack: NavPathStack new NavPathStack(); onPageShow(): void { // 如果存在迁移过来的页面路径信息自动重建页面栈 if (this.navPathInfo this.navPathInfo.length 0) { this.navPathInfo.forEach((pathInfo) { this.pageStack.pushPathByName(pathInfo.name, pathInfo.param); }); console.info(Navigation 历史页面栈重建完成); } } }2、 基于 Router 路由的页面栈迁移对于使用传统Router组件的应用系统提供了更便捷的自动恢复机制开发者只需通过开关进行控制。1. 源端开启 Router 栈迁移开关在onContinue中将 Router 栈迁移开关设置为trueimport { AbilityConstant, UIAbility } from kit.AbilityKit; import wantConstant from ohos.app.ability.wantConstant; export default class EntryAbility extends UIAbility { onContinue(wantParam: Recordstring, Object): AbilityConstant.OnContinueResult { // 开启 Router 页面栈自动迁移 wantParam[wantConstant.Params.SUPPORT_CONTINUE_PAGE_STACK_KEY] true; console.info(Router 页面栈迁移开关已开启); return AbilityConstant.OnContinueResult.AGREE; } }2. 目标端处理窗口恢复与降级在目标端的onCreate中读取开关状态并在onWindowStageRestore中决定加载逻辑。如果由于特殊原因关闭了栈迁移系统会强制回落到首页import { AbilityConstant, UIAbility, Want } from kit.AbilityKit; import window from ohos.window; export default class EntryAbility extends UIAbility { private SUPPORT_CONTINUE_PAGE_STACK_KEY: boolean true; onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { if (launchParam.launchReason AbilityConstant.LaunchReason.CONTINUATION) { // 读取 Router 栈迁移开关 if (typeof want.parameters?.[ohos.extra.param.key.supportContinuePageStack] boolean) { this.SUPPORT_CONTINUE_PAGE_STACK_KEY want.parameters[ohos.extra.param.key.supportContinuePageStack] as boolean; } this.context.restoreWindowStage(new LocalStorage()); } } onWindowStageRestore(windowStage: window.WindowStage): void { // 如果 Router 栈迁移被关闭强制加载首页以防出现不兼容的子页面 if (!this.SUPPORT_CONTINUE_PAGE_STACK_KEY) { windowStage.loadContent(pages/Index, (err) { if (err.code) { console.error(加载首页失败:, err); return; } console.info(已回退并加载首页); }); } } }五、 突破 100KB 限制分布式对象与文件协同wantParam的传输大小被严格限制在 100KB 以内这对于包含高清图片、长富文本或视频进度的应用是远远不够的。混合架构将轻量级的状态如光标位置、当前页码、文档 ID通过wantParam传递将大体积内容如图片 Base64、视频流存入分布式键值数据库KV-Store或分布式文件系统DFS。按需拉取目标设备在接收到轻量级状态并渲染出基础 UI 后通过相同的SessionId或分布式文件 URI在后台静默拉取大体积数据实现“骨架屏秒开内容随后加载”的极致体验。1、 源端构建混合数据并发起迁移在源端的onContinue中将业务数据拆分为“轻量级状态”和“重量级内容”分别存入不同的分布式存储中并将对应的索引放入wantParam。import { AbilityConstant, UIAbility } from kit.AbilityKit; import { distributedKVStore } from kit.ArkData; export default class EntryAbility extends UIAbility { async onContinue(wantParam: Recordstring, Object): PromiseAbilityConstant.OnContinueResult { // 1. 轻量级状态直接放入 wantParam远小于 100KB const lightState { docId: DOC_20260623, cursorPos: 1024, currentPage: 5 }; wantParam[lightState] JSON.stringify(lightState); // 2. 重量级内容存入分布式 KV-Store如长富文本、Base64 图片 const kvManager distributedKVStore.createKVManager({ context: this.context, bundleName: com.example.app }); const kvStore await kvManager.getKVStore(rich_content_store); // 假设 heavyContent 是一段 500KB 的富文本 await kvStore.put(DOC_20260623_content, this.heavyContent); // 3. 仅将文档 ID 作为索引传递目标端通过此 ID 拉取数据 wantParam[contentRefId] DOC_20260623_content; return AbilityConstant.OnContinueResult.AGREE; } }2、 目标端接收轻量数据渲染骨架屏目标端在onCreate中优先读取轻量级状态立即构建基础 UI如展示骨架屏或占位符避免用户等待。import { AbilityConstant, UIAbility, Want } from kit.AbilityKit; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { if (launchParam.launchReason AbilityConstant.LaunchReason.CONTINUATION) { // 1. 优先解析轻量级状态立即驱动 UI 渲染 const lightStateStr want.parameters?.[lightState] as string; if (lightStateStr) { const lightState JSON.parse(lightStateStr); AppStorage.setOrCreate(lightState, lightState); console.info(轻量状态已恢复UI 骨架屏准备就绪); } // 2. 提取重量级内容的引用 ID const contentRefId want.parameters?.[contentRefId] as string; if (contentRefId) { AppStorage.setOrCreate(contentRefId, contentRefId); } } } }3、 UI 层后台静默拉取无缝填充内容在 ArkUI 页面中当基础 UI 渲染完成后利用aboutToAppear生命周期在后台静默拉取大体积数据实现平滑过渡。import { AppStorage } from kit.ArkUI; import { distributedKVStore } from kit.ArkData; Entry Component struct EditorPage { State isLoading: boolean true; State textContent: string ; StorageProp(lightState) lightState: Recordstring, Object {}; StorageProp(contentRefId) contentRefId: string ; async aboutToAppear() { // 1. 后台静默拉取重量级内容 try { const kvManager distributedKVStore.createKVManager({ context: getContext(this), bundleName: com.example.app }); const kvStore await kvManager.getKVStore(rich_content_store); // 2. 根据索引获取完整数据 const result await kvStore.get(this.contentRefId); this.textContent result.value as string; console.info(重量级内容拉取完成骨架屏替换为真实内容); } catch (err) { console.error(后台拉取内容失败:, err); } finally { // 3. 关闭加载状态完成无缝填充 this.isLoading false; } } build() { Column() { if (this.isLoading) { // 骨架屏 / 占位符 UI Text(正在同步内容...).fontSize(16).fontColor(Color.Gray) } else { // 真实内容 UI TextArea({ text: this.textContent }) } } .width(100%).height(100%).padding(20) } }六、 跨设备 UI 适配与交互接管手机和平板的屏幕尺寸、交互方式差异巨大。迁移不仅仅是数据的转移更是 UI 的重构。响应式布局利用 ArkUI 的GridRow/GridCol栅格系统和MediaQuery媒体查询在目标设备恢复数据时自动将手机的“单列列表”切换为平板的“双列/三列分栏布局”。硬件能力接管迁移后应用可以感知目标设备的硬件特性。例如从手机迁移到平板后自动接管平板的键盘输入从平板迁移到智慧屏时自动切换为大屏遥控器焦点交互模式。七、 企业级跨端迁移状态快照的原子性在onContinue中保存状态时务必保证数据的原子性。如果涉及多个文件的并发写入应加锁或采用事务机制避免传递出“写了一半”的脏数据。优雅降级与异常处理分布式环境具有不确定性。必须完善onFailedContinuation的异常处理逻辑。当目标设备离线、版本不兼容或网络超时时应在源端给出明确的 Toast 提示并保留本地任务避免用户操作丢失。版本兼容性校验在onContinue的wantParam中系统会自动携带目标设备的version。开发者应在此处进行版本号比对若目标端应用版本过低不支持某些新字段应进行数据裁剪或拒绝迁移防止目标端解析崩溃。隐私与权限前置校验在发起迁移前主动检查目标设备的安全等级。若当前页面包含敏感支付信息或健康数据且目标设备为低安全等级的公共设备应主动拦截迁移请求保障用户隐私安全。