跨端迁移:实现应用状态在手机与平板间无缝流转(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: Record<string, 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)来提取数据并恢复 UI:
import { 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.get<Record<string, 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 快照,并将其写入 wantParam:
import { AbilityConstant, UIAbility } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onContinue(wantParam: Record<string, 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 中读取栈数据,存入 AppStorage:
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. 读取目标端传递过来的 Navigation 页面栈快照
if (Array.isArray(want.parameters?.['navPathStack'])) {
AppStorage.setOrCreate('NavPathInfo', want.parameters['navPathStack'] as Array<NavPathInfo>);
} 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: Array<NavPathInfo> = [];
@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 栈迁移开关设置为 true:
import { AbilityConstant, UIAbility } from '@kit.AbilityKit';
import wantConstant from '@ohos.app.ability.wantConstant';
export default class EntryAbility extends UIAbility {
onContinue(wantParam: Record<string, 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: Record<string, Object>): Promise<AbilityConstant.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: Record<string, 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。开发者应在此处进行版本号比对,若目标端应用版本过低不支持某些新字段,应进行数据裁剪或拒绝迁移,防止目标端解析崩溃。 - 隐私与权限前置校验:在发起迁移前,主动检查目标设备的安全等级。若当前页面包含敏感支付信息或健康数据,且目标设备为低安全等级的公共设备,应主动拦截迁移请求,保障用户隐私安全。
更多推荐


所有评论(0)