在鸿蒙(HarmonyOS)生态中,跨端迁移(应用接续)是实现“人随场景走、服务随人走”的核心能力。它允许用户在手机上进行的操作(如编辑文档、观看视频、浏览网页),无缝流转至平板或智慧屏上继续,且保持上下文状态完全一致。

这一过程并非将运行内存直接搬运,而是由分布式任务调度分布式软总线应用状态保存/恢复机制三者协同完成的。以下是跨端迁移的底层原理及代码。

一、 核心运作机制

  1. 状态快照与序列化:源端设备(如手机)在发起迁移前,系统会回调 onContinue() 接口。开发者需在此接口中将当前页面的业务数据(如文档内容、光标位置、视频播放进度)序列化为轻量级状态数据(通常限制在 100KB 以内)。
  2. 软总线传输与任务拉起:分布式软总线负责设备发现、可信认证并建立加密通道,将状态数据传输至目标设备(如平板)。同时,分布式任务调度负责在目标设备上拉起对应的 UIAbility。
  3. 状态恢复与重建:目标设备的 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 媒体查询,在目标设备恢复数据时,自动将手机的“单列列表”切换为平板的“双列/三列分栏布局”。
  • 硬件能力接管:迁移后,应用可以感知目标设备的硬件特性。例如,从手机迁移到平板后,自动接管平板的键盘输入;从平板迁移到智慧屏时,自动切换为大屏遥控器焦点交互模式。

七、 企业级跨端迁移

  1. 状态快照的原子性:在 onContinue 中保存状态时,务必保证数据的原子性。如果涉及多个文件的并发写入,应加锁或采用事务机制,避免传递出“写了一半”的脏数据。
  2. 优雅降级与异常处理:分布式环境具有不确定性。必须完善 onFailedContinuation 的异常处理逻辑。当目标设备离线、版本不兼容或网络超时时,应在源端给出明确的 Toast 提示,并保留本地任务,避免用户操作丢失。
  3. 版本兼容性校验:在 onContinue 的 wantParam 中,系统会自动携带目标设备的 version。开发者应在此处进行版本号比对,若目标端应用版本过低不支持某些新字段,应进行数据裁剪或拒绝迁移,防止目标端解析崩溃。
  4. 隐私与权限前置校验:在发起迁移前,主动检查目标设备的安全等级。若当前页面包含敏感支付信息或健康数据,且目标设备为低安全等级的公共设备,应主动拦截迁移请求,保障用户隐私安全。
Logo

一站式 AI 云服务平台

更多推荐