• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 跨端迁移
2
3## 概述
4
5在用户使用设备的过程中,当使用情境发生变化时(例如从室内走到户外或者周围有更适合的设备等),之前使用的设备可能已经不适合继续当前的任务,此时,用户可以选择新的设备来继续当前的任务,原设备可按需决定是否退出任务,这个就是跨端迁移的场景。常见的跨端迁移场景实例:在平板上播放的视频,迁移到智慧屏继续播放,从而获得更佳的观看体验;平板上的视频应用退出。在应用开发层面,跨端迁移指在A端运行的[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)迁移到B端上,完成迁移后,B端UIAbility继续任务,而A端UIAbility可按需决定是否退出。
6
7主要功能包括:
8
9- 支持用户自定义数据存储及恢复。
10
11- 支持页面路由信息和页面控件状态数据的存储及恢复。
12
13- 支持应用兼容性检测。
14
15- 支持应用根据实际使用场景动态设置迁移状态(默认迁移状态为 **ACTIVE** 激活状态)。例如,编辑类应用在编辑文本的页面下才需要迁移,其他页面不需要迁移,则可以通过[setMissionContinueState](../reference/apis-ability-kit/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextsetmissioncontinuestate10)进行控制。
16
17- 支持应用动态选择是否进行页面栈恢复(默认进行页面栈信息恢复)。例如,应用希望自定义迁移到其他设备后显示的页面,则可以通过[SUPPORT_CONTINUE_PAGE_STACK_KEY](../reference/apis-ability-kit/js-apis-app-ability-wantConstant.md#params)进行控制。
18
19- 支持应用动态选择迁移成功后是否退出迁移源端应用(默认迁移成功后退出迁移源端应用)。可以通过[SUPPORT_CONTINUE_SOURCE_EXIT_KEY](../reference/apis-ability-kit/js-apis-app-ability-wantConstant.md#params)进行控制。
20
21  > **说明:**
22  >
23  > 开发者可以开发具有迁移能力的应用,迁移的触发由系统应用完成。
24
25## 运作机制
26
27![hop-cross-device-migration](figures/hop-cross-device-migration.png)
28
291. 在源端,通过[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)的[onContinue()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncontinue)回调,开发者可以保存待迁移的业务数据。例如,在浏览器应用中完成跨端迁移,开发者需要使用onContinue()回调保存页面URL等业务内容。
302. 分布式框架提供了跨设备应用页面栈以及业务数据的保存和恢复机制,它负责将数据从源端发送到对端。
313. 在对端,同一UIAbility可以通过[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)(冷启动)和[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)(热启动)接口来恢复业务数据。
32
33## 跨端迁移流程
34
35以从对端的迁移入口发起迁移为例,跨端迁移流程如下图所示。
36
37![hop-cross-device-migration](figures/hop-cross-device-migration4.png)
38
39## 约束限制
40
41- 跨端迁移要求在同一[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)之间进行,也就是需要相同的`bundleName`、`abilityName`和签名信息。
42- 为了获得最佳体验,使用`wantParam`传输的数据需要控制在100KB以下。
43
44## 开发步骤
45
461. 在[module.json5配置文件](../quick-start/module-configuration-file.md)的abilities标签中配置跨端迁移标签`continuable`。
47
48   ```json
49   {
50     "module": {
51       // ...
52       "abilities": [
53         {
54           // ...
55           "continuable": true, // 配置UIAbility支持迁移
56         }
57       ]
58     }
59   }
60   ```
61
62   > **说明:**
63   >
64   > 根据需要配置应用启动模式类型,配置详情请参照[UIAbility组件启动模式](uiability-launch-type.md)。
65
662. 在源端UIAbility中实现onContinue()回调。
67
68    当[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)实例触发迁移时,[onContinue()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncontinue)回调在源端被调用,开发者可以在该接口中通过同步或异步的方式来保存迁移数据,实现应用兼容性检测,决定是否支持此次迁移。
69
70    1. 保存迁移数据:开发者可以将要迁移的数据通过键值对的方式保存在`wantParam`参数中。
71
72    2. (可选)检测应用兼容性:开发者可以在触发迁移时从`onContinue()`入参`wantParam.version`获取到迁移对端应用的版本号,与迁移源端应用版本号做兼容校验。应用在校验版本兼容性失败后,需要提示用户迁移失败的原因。
73
74       > **说明:**
75       >
76       > 如果迁移过程中的兼容性问题对于应用迁移体验影响较小或无影响,可以跳过该步骤。
77
78    3. 返回迁移结果:开发者可以通过`onContinue()`回调的返回值决定是否支持此次迁移,接口返回值详见[AbilityConstant.OnContinueResult](../reference/apis-ability-kit/js-apis-app-ability-abilityConstant.md#oncontinueresult)。
79
80     
81    `onContinue()`接口传入的`wantParam`参数中,有部分字段由系统预置,开发者可以使用这些字段用于业务处理。同时,应用在保存自己的`wantParam`参数时,也应注意不要使用同样的key值,避免被系统覆盖导致数据获取异常。详见下表:
82
83    | 字段|含义|
84    | ---- | ---- |
85    | version | 对端应用的版本号 |
86    | targetDevice | 对端设备的networkId |
87
88    ```ts
89    import { AbilityConstant, UIAbility } from '@kit.AbilityKit';
90    import { hilog } from '@kit.PerformanceAnalysisKit';
91    import { PromptAction } from '@kit.ArkUI';
92
93    const TAG: string = '[MigrationAbility]';
94    const DOMAIN_NUMBER: number = 0xFF00;
95
96    export default class MigrationAbility extends UIAbility {
97      // 在onContinue中准备迁移数据
98      onContinue(wantParam: Record<string, Object>):AbilityConstant.OnContinueResult {
99        let targetVersion = wantParam.version;
100        let targetDevice = wantParam.targetDevice;
101        hilog.info(DOMAIN_NUMBER, TAG, `onContinue version = ${targetVersion}, targetDevice: ${targetDevice}`);
102
103        // 应用可根据源端版本号设置支持接续的最小兼容版本号,源端版本号可从app.json5文件中的versionCode字段获取;防止目标端版本号过低导致不兼容。
104        let versionThreshold: number = -1; // 替换为应用自己支持兼容的最小版本号
105        // 兼容性校验
106        let promptAction: promptAction = uiContext.getPromptAction;
107        if (targetVersion < versionThreshold) {
108          // 建议在校验版本兼容性失败后,提示用户拒绝迁移的原因
109          promptAction.showToast({
110              message: '目标端应用版本号过低,不支持接续,请您升级应用版本后再试',
111              duration: 2000
112          })
113          // 在兼容性校验不通过时返回MISMATCH
114          return AbilityConstant.OnContinueResult.MISMATCH;
115        }
116
117        // 将要迁移的数据保存在wantParam的自定义字段(例如data)中
118        const continueInput = '迁移的数据';
119        wantParam['data'] = continueInput;
120
121        return AbilityConstant.OnContinueResult.AGREE;
122      }
123    }
124    ```
125
1263. 对端设备的UIAbility通过实现[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)接口,来恢复迁移数据和加载UI。
127
128    不同的启动方式下会调用不同的接口,详见下图。
129
130    ![hop-cross-device-migration](figures/hop-cross-device-migration5.png)
131
132    > **说明:**
133    > 1. 在应用迁移启动时,无论是冷启动还是热启动,都会在执行完onCreate()/onNewWant()后,触发[onWindowStageRestore()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonwindowstagerestore)生命周期函数,不执行[onWindowStageCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonwindowstagecreate)生命周期函数。
134    > 2. 开发者如果在`onWindowStageCreate()`中进行了一些应用启动时必要的初始化,那么迁移后需要在`onWindowStageRestore()`中执行同样的初始化操作,避免应用异常。
135
136    - 通过在onCreate()/onNewWant()回调中检查`launchReason`(CONTINUATION),可以判断此次启动是否由迁移触发。
137    - 开发者可以从[want](../reference/apis-ability-kit/js-apis-app-ability-want.md)中获取之前保存的迁移数据。
138    - 若开发者使用系统页面栈恢复功能,则需要在onCreate()/onNewWant()执行完成前,同步调用[restoreWindowStage()](../reference/apis-ability-kit/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextrestorewindowstage),来触发带有页面栈的页面恢复,详见[按需迁移页面栈](./hop-cross-device-migration.md#按需迁移页面栈)。
139
140    ```ts
141    import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
142    import { hilog } from '@kit.PerformanceAnalysisKit';
143
144    const TAG: string = '[MigrationAbility]';
145    const DOMAIN_NUMBER: number = 0xFF00;
146
147    export default class MigrationAbility extends UIAbility {
148      storage : LocalStorage = new LocalStorage();
149
150      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
151        hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onCreate');
152        if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
153          // 将上述保存的数据从want.parameters中取出恢复
154          let continueInput = '';
155          if (want.parameters !== undefined) {
156            continueInput = JSON.stringify(want.parameters.data);
157            hilog.info(DOMAIN_NUMBER, TAG, `continue input ${JSON.stringify(continueInput)}`);
158          }
159          // 触发页面恢复
160          this.context.restoreWindowStage(this.storage);
161        }
162      }
163
164      onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
165        hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant');
166        if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
167          // 将上述保存的数据从want.parameters中取出恢复
168          let continueInput = '';
169          if (want.parameters !== undefined) {
170            continueInput = JSON.stringify(want.parameters.data);
171            hilog.info(DOMAIN_NUMBER, TAG, `continue input ${JSON.stringify(continueInput)}`);
172          }
173          // 触发页面恢复
174          this.context.restoreWindowStage(this.storage);
175        }
176      }
177    }
178    ```
179
180## 可选配置迁移能力
181
182### 动态配置迁移能力
183
184从API version 10开始,提供了支持动态配置迁移能力的功能。即应用可以根据实际使用场景,在需要迁移时开启应用迁移能力;在业务不需要迁移时则可以关闭迁移能力。
185
186开发者可以通过调用[setMissionContinueState()](../reference/apis-ability-kit/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextsetmissioncontinuestate10)接口对迁移能力进行设置。默认状态下,应用的迁移能力为**ACTIVE**状态,即迁移能力开启,可以迁移。
187
188**设置迁移能力的时机**
189
190如果需要实现某些特殊场景,比如只在具体某个页面下支持迁移,或只在某个事件发生时才支持迁移,可以按照如下步骤进行配置。
191
1921. 在[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)的[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)生命周期回调中,关闭迁移能力。
193
194    ```ts
195    // MigrationAbility.ets
196    import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
197    import { hilog } from '@kit.PerformanceAnalysisKit';
198
199    const TAG: string = '[MigrationAbility]';
200    const DOMAIN_NUMBER: number = 0xFF00;
201
202    export default class MigrationAbility extends UIAbility {
203      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
204        // ...
205        this.context.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE, (result) => {
206          hilog.info(DOMAIN_NUMBER, TAG, `setMissionContinueState INACTIVE result: ${JSON.stringify(result)}`);
207        });
208        // ...
209      }
210    }
211    ```
212
213 2. 如果需要在具体某个页面中打开迁移能力,可以在页面的[onPageShow()](../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#onpageshow)函数中调用接口。
214
215    ```ts
216    // Page_MigrationAbilityFirst.ets
217    import { AbilityConstant, common } from '@kit.AbilityKit';
218    import { hilog } from '@kit.PerformanceAnalysisKit';
219
220    const TAG: string = '[MigrationAbility]';
221    const DOMAIN_NUMBER: number = 0xFF00;
222
223    @Entry
224    @Component
225    struct Page_MigrationAbilityFirst {
226      private context = this.getUIContext().getHostContext();
227      build() {
228        // ...
229      }
230      // ...
231      onPageShow(){
232        // 进入该页面时,将应用设置为可迁移状态
233        this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
234          hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', `setMissionContinueState ACTIVE result: ${JSON.stringify(result)}`);
235        });
236      }
237    }
238    ```
239
2403. 如果想要在某个组件的触发事件打开迁移能力,可以在该事件中调用。以Button组件的[onClick()](../reference/apis-arkui/arkui-ts/ts-universal-events-click.md#onclick)事件为例:
241
242    ```ts
243    // Page_MigrationAbilityFirst.ets
244    import { AbilityConstant, common } from '@kit.AbilityKit';
245    import { hilog } from '@kit.PerformanceAnalysisKit';
246    import { PromptAction } from '@kit.ArkUI';
247
248    const TAG: string = '[MigrationAbility]';
249    const DOMAIN_NUMBER: number = 0xFF00;
250
251    @Entry
252    @Component
253    struct Page_MigrationAbilityFirst {
254      private context = this.getUIContext().getHostContext();
255      let promptAction: promptAction = uiContext.getPromptAction;
256      build() {
257        Column() {
258          //...
259          List({ initialIndex: 0 }) {
260            ListItem() {
261              Row() {
262                //...
263              }
264              .onClick(() => {
265                // 点击该按钮时,将应用设置为可迁移状态
266                this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
267                  hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', `setMissionContinueState ACTIVE result: ${JSON.stringify(result)}`);
268                  promptAction.showToast({
269                    message: 'Success'
270                  });
271                });
272              })
273            }
274            //...
275          }
276          //...
277        }
278        //...
279      }
280    }
281    ```
282
283### 按需迁移页面栈
284
285
286> **说明:**
287>
288> 1. 当前仅支持router路由的页面栈自动恢复,暂不支持navigation路由的页面栈自动恢复。
289> 2. 若应用使用navigation路由,可以设置不进行默认页面栈迁移(配置[SUPPORT_CONTINUE_PAGE_STACK_KEY](../reference/apis-ability-kit/js-apis-app-ability-wantConstant.md#params)参数为`false`),并将需要迁移的页面(或页面栈)信息保存在want中传递,然后在目标端手动加载指定页面。
290
291[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)的迁移默认恢复页面栈。开发者需要在[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)执行完成前,调用[restoreWindowStage()](../reference/apis-ability-kit/js-apis-inner-application-uiAbilityContext.md#restore),向系统传入当前的窗口上下文,用于页面栈的加载恢复。`restoreWindowStage()`接口必须在同步方法中执行。如果在异步回调中执行该接口,会导致在应用拉起时页面有概率加载失败。
292
293以`onCreate()`为例:
294
295```ts
296import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
297
298export default class MigrationAbility extends UIAbility {
299  storage : LocalStorage = new LocalStorage();
300
301  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
302      // ...
303      // 同步执行结束前触发页面恢复
304      this.context.restoreWindowStage(this.storage);
305  }
306}
307```
308
309如果应用不想通过系统自动恢复页面栈,可以通过配置[SUPPORT_CONTINUE_PAGE_STACK_KEY](../reference/apis-ability-kit/js-apis-app-ability-wantConstant.md#params)参数为`false`关闭该功能。开发者需要在[onWindowStageRestore()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonwindowstagerestore)中,指定迁移后进入的页面。不指定迁移后进入的页面,会导致迁移拉起后显示空白页面。
310
311例如,`UIAbility`在对端恢复时不需要按照源端页面栈进行恢复,而是需要恢复到指定页面`Page_MigrationAbilityThird`。
312
313```ts
314// MigrationAbility.ets
315import { AbilityConstant, UIAbility, wantConstant } from '@kit.AbilityKit';
316import { hilog } from '@kit.PerformanceAnalysisKit';
317import { window } from '@kit.ArkUI';
318
319const TAG: string = '[MigrationAbility]';
320const DOMAIN_NUMBER: number = 0xFF00;
321
322export default class MigrationAbility extends UIAbility {
323  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
324    // ...
325    // 配置不使用系统页面栈恢复
326    wantParam[wantConstant.Params.SUPPORT_CONTINUE_PAGE_STACK_KEY] = false;
327    return AbilityConstant.OnContinueResult.AGREE;
328  }
329
330  onWindowStageRestore(windowStage: window.WindowStage): void {
331    // 不使用系统页面栈恢复时,需要在此处指定应用迁移后进入的页面
332    windowStage.loadContent('pages/page_migrationability/Page_MigrationAbilityThird', (err, data) => {
333      if (err.code) {
334        hilog.error(DOMAIN_NUMBER, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
335        return;
336      }
337    });
338  }
339}
340```
341
342### 按需退出
343
344[UIAbility](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md)的迁移默认迁移结束后退出源端应用。如果应用希望迁移后源端应用继续运行,或需要进行其他操作(如,保存草稿、清理资源等)后再自行触发退出,不想在迁移后立即自动退出源端应用,可以通过配置[SUPPORT_CONTINUE_SOURCE_EXIT_KEY](../reference/apis-ability-kit/js-apis-app-ability-wantConstant.md#params)参数为`false`设置源端迁移后不退出。
345
346示例:`UIAbility`设置迁移成功后,源端不需要退出迁移应用。
347
348```ts
349import { AbilityConstant, UIAbility, wantConstant } from '@kit.AbilityKit';
350import { hilog } from '@kit.PerformanceAnalysisKit';
351
352const TAG: string = '[MigrationAbility]';
353const DOMAIN_NUMBER: number = 0xFF00;
354
355export default class MigrationAbility extends UIAbility {
356  // ...
357  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
358    hilog.info(DOMAIN_NUMBER, TAG, `onContinue version = ${wantParam.version}, targetDevice: ${wantParam.targetDevice}`);
359    wantParam[wantConstant.Params.SUPPORT_CONTINUE_SOURCE_EXIT_KEY] = false;
360    return AbilityConstant.OnContinueResult.AGREE;
361  }
362}
363```
364
365### 支持同应用中不同Ability跨端迁移
366一般情况下,跨端迁移的双端是同Ability之间,但有些应用在不同设备类型下的同一个业务Ability名称不同(即异Ability),为了支持该场景下的两个Ability之间能够完成迁移,可以通过在[module.json5](../quick-start/module-configuration-file.md)文件的abilities标签中配置迁移类型continueType进行关联。
367需要迁移的两个Ability的continueType字段取值必须保持一致,示例如下:
368   > **说明:**
369   >
370   > continueType在本应用中要保证唯一,字符串以字母、数字和下划线组成,最大长度127个字节,不支持中文。
371   > continueType标签类型为字符串数组,如果配置了多个字段,当前仅第一个字段会生效。
372
373```json
374   // 设备A
375   {
376     "module": {
377       // ...
378       "abilities": [
379         {
380           // ...
381           "name": "Ability-deviceA",
382           "continueType": ['continueType1'], // continueType标签配置
383         }
384       ]
385     }
386   }
387
388   // 设备B
389   {
390     "module": {
391       // ...
392       "abilities": [
393         {
394           // ...
395           "name": "Ability-deviceB",
396           "continueType": ['continueType1'], // 与设备A相同的continueType标签
397         }
398       ]
399     }
400   }
401```
402
403### 支持同应用不同BundleName的Ability跨端迁移
404相同应用在不同设备类型下可能使用了不同的BundleName,该场景下如果需要支持应用跨端迁移,需要在不同BundleName的应用的module.json5配置文件中的abilities标签进行如下配置:
405
406- continueBundleName字段:分别添加对端应用的BundleName。
407- continueType字段:必须保持一致。
408
409   > **说明:**
410   >
411   > continueType在本应用中要保证唯一,字符串以字母、数字和下划线组成,最大长度127个字节,不支持中文。
412   > continueType标签类型为字符串数组,如果配置了多个字段,当前仅第一个字段会生效。
413
414示例如下:
415
416   不同BundleName的相同应用在设备A和设备B之间相互迁移,设备A应用的BundleName为com.demo.example1,设备B应用的BundleName为com.demo.example2417
418```JSON
419// 在设备A的应用配置文件中,continueBundleName字段配置包含设备B上应用的BundleName。
420{
421  "module": {
422    // ···
423    "abilities": [
424      {
425        "name": "EntryAbility",
426        // ···
427        "continueType": ["continueType"],
428        "continueBundleName": ["com.demo.example2"], // continueBundleName标签配置,com.demo.example2为设备B上应用的BundleName。
429
430      }
431    ]
432
433  }
434}
435```
436
437```JSON
438// 在设备B的应用配置文件中,continueBundleName字段配置包含设备A上应用的BundleName。
439{
440  "module": {
441    // ···
442    "abilities": [
443      {
444        "name": "EntryAbility",
445        // ···
446        "continueType": ["continueType"],
447        "continueBundleName": ["com.demo.example1"], // continueBundleName标签配置,com.demo.example1为设备A上应用的BundleName。
448
449      }
450    ]
451
452  }
453}
454
455```
456
457### 支持快速拉起目标应用
458默认情况下,发起迁移后不会立即拉起对端的目标应用,而是等待迁移数据从源端同步到对端后,才会拉起。为了发起迁移后能够立即拉起目标应用,做到及时响应,可以通过在continueType标签中添加“_ContinueQuickStart”后缀进行生效,这样待迁移数据从源端同步到对端后只恢复迁移数据即可,提升应用迁移体验。
459
460   ```json
461   {
462     "module": {
463       // ...
464       "abilities": [
465         {
466           // ...
467           "name": "EntryAbility"
468           "continueType": ['EntryAbility_ContinueQuickStart'], // 如果已经配置了continueType标签,可以在该标签值后添加'_ContinueQuickStart'后缀;如果没有配置continueType标签,可以使用AbilityName + '_ContinueQuickStart'作为continueType标签实现快速拉起目标应用
469         }
470       ]
471     }
472   }
473   ```
474配置快速拉起功能后,用户触发迁移,等待迁移数据返回的过程中,并行拉起应用,减小用户等待迁移启动时间。同时需注意,应用在迁移的提前启动时,首次触发迁移会收到`launchReason`为提前拉起 (PREPARE_CONTINUATION)的[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)请求。应用可以通过此`launchReason`,解决跳转、时序等问题,也可以在迁移快速启动时,增加loading界面。
475
476从API version 18开始,在快速拉起时含loading界面的应用,支持[获取应用跨端迁移快速拉起结果](../reference/apis-ability-kit/js-apis-app-ability-continueManager.md#continuemanageron)。根据快速拉起结果,应用可以进行相应操作,例如快速拉起成功时退出loading界面进入接续页面。
477
478快速拉起流程如下图所示。
479
480![hop-cross-device-migration](figures/continue_quick_start.png)
481
482配置了快速拉起的应用,触发迁移时会收到两次启动请求,区别如下:
483
484| 场景           | 生命周期函数                                | launchParam.launchReason                          |
485| -------------- | ------------------------------------------- | ------------------------------------------------- |
486| 第一次启动请求 | onCreate (冷启动)<br />或onNewWant (热启动) | AbilityConstant.LaunchReason.PREPARE_CONTINUATION |
487| 第二次启动请求 | onNewWant                                   | AbilityConstant.LaunchReason.CONTINUATION         |
488
489如果没有配置快速拉起,则触发迁移时会收到一次启动请求:
490
491| 场景         | 生命周期函数                                | launchParam.launchReason                  |
492| ------------ | ------------------------------------------- | ----------------------------------------- |
493| 一次启动请求 | onCreate (冷启动)<br />或onNewWant (热启动) | AbilityConstant.LaunchReason.CONTINUATION |
494
495配置快速拉起功能后,对应的[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)接口具体实现,示例代码如下:
496
497```ts
498import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
499import { hilog } from '@kit.PerformanceAnalysisKit';
500
501const TAG: string = '[MigrationAbility]';
502const DOMAIN_NUMBER: number = 0xFF00;
503
504export default class MigrationAbility extends UIAbility {
505  storage : LocalStorage = new LocalStorage();
506
507  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
508    hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onCreate');
509
510    // 1.已配置快速拉起功能,应用立即启动时触发应用生命周期回调
511    if (launchParam.launchReason === AbilityConstant.LaunchReason.PREPARE_CONTINUATION) {
512      //若应用迁移数据较大,可在此处添加加载页面(页面中显示loading等)
513      //可处理应用自定义跳转、时序等问题
514      // ...
515    }
516  }
517
518  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
519    hilog.info(DOMAIN_NUMBER, TAG, 'onNewWant');
520
521    // 1.已配置快速拉起功能,应用立即启动时触发应用生命周期回调
522    if (launchParam.launchReason === AbilityConstant.LaunchReason.PREPARE_CONTINUATION) {
523      //若应用迁移数据较大,可在此处添加加载页面(页面中显示loading等)
524      //可处理应用自定义跳转、时序等问题
525      // ...
526    }
527
528    // 2.迁移数据恢复时触发应用生命周期回调
529    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
530      // 将上述保存的数据从want.parameters中取出恢复
531      let continueInput = '';
532      if (want.parameters !== undefined) {
533        continueInput = JSON.stringify(want.parameters.data);
534        hilog.info(DOMAIN_NUMBER, TAG, `continue input ${JSON.stringify(continueInput)}`);
535      }
536      // 触发页面恢复
537      this.context.restoreWindowStage(this.storage);
538    }
539  }
540}
541```
542
543快速拉起目标应用时,应用的[onWindowStageCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonwindowstagecreate)和[onWindowStageRestore()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonwindowstagerestore)回调会被依次触发。通常在onWindowStageCreate()中,开发者会调用[loadContent()](../reference/apis-arkui/js-apis-window.md#loadcontent9)加载页面,该接口会抛出一个异步任务加载首页,该异步任务与onWindowStageRestore()无同步关系。如果在onWindowStageRestore()中使用UI接口(如路由接口),其调用时机可能早于首页加载。为保证正常加载顺序,可以使用[setTimeout()](../reference/common/js-apis-timer.md#settimeout)抛出异步任务执行相关操作。详细见示例代码。
544
545示例代码如下:
546
547```ts
548import { UIAbility } from '@kit.AbilityKit';
549import { hilog } from '@kit.PerformanceAnalysisKit';
550import { UIContext, window } from '@kit.ArkUI';
551
552export default class EntryAbility extends UIAbility {
553  private uiContext: UIContext | undefined = undefined;
554
555  // ...
556
557  onWindowStageCreate(windowStage: window.WindowStage): void {
558    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
559
560    windowStage.loadContent('pages/Index', (err) => {
561      if (err.code) {
562        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
563        return;
564      }
565      this.uiContext = windowStage.getMainWindowSync().getUIContext();
566      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
567    });
568  }
569
570  onWindowStageRestore(windowStage: window.WindowStage): void {
571    setTimeout(() => {
572      // 抛出异步任务执行路由,保证其执行位于首页加载之后。
573      this.uiContext?.getRouter().pushUrl({
574        url: 'pages/examplePage'
575      });
576    }, 0);
577  }
578
579  // ...
580}
581```
582
583## 跨端迁移中的数据迁移
584
585当前推荐两种不同的数据迁移方式,开发者可以根据实际使用需要进行选择。
586  > **说明:**
587  >
588  > 部分ArkUI组件支持通过配置`restoreId`的方式,在迁移后将特定状态恢复到对端设备。详情请见[分布式迁移标识](../reference/apis-arkui/arkui-ts/ts-universal-attributes-restoreId.md)。
589  >
590  > 如果涉及分布式数据对象迁移时应注意:
591  >
592  > API 11及以前版本涉及分布式数据对象迁移前,需要执行如下操作。
593  > 1. 申请`ohos.permission.DISTRIBUTED_DATASYNC`权限,配置方式请参见[声明权限](../security/AccessToken/declare-permissions.md)。
594  >
595  > 2. 在应用首次启动时弹窗向用户申请授权,使用方式请参见[向用户申请授权](../security/AccessToken/request-user-authorization.md)。
596
597### 使用wantParam迁移数据
598
599在需要迁移的数据较少(100KB以下)时,开发者可以选择在`wantParam`中增加字段进行数据迁移。示例如下:
600
601```ts
602import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
603import { hilog } from '@kit.PerformanceAnalysisKit';
604
605const TAG: string = '[MigrationAbility]';
606const DOMAIN_NUMBER: number = 0xFF00;
607
608export default class MigrationAbility extends UIAbility {
609  storage: LocalStorage = new LocalStorage();
610
611  // 源端保存
612  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
613    // 将要迁移的数据保存在wantParam的自定义字段(例如data)中
614    const continueInput = '迁移的数据';
615    wantParam['data'] = continueInput;
616    return AbilityConstant.OnContinueResult.AGREE;
617  }
618
619  // 对端恢复
620  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
621    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
622      // 将上述保存的数据取出恢复
623      let continueInput = '';
624      if (want.parameters !== undefined) {
625        continueInput = JSON.stringify(want.parameters.data);
626        hilog.info(DOMAIN_NUMBER, TAG, `continue input ${continueInput}`);
627      }
628      // 触发页面恢复
629      this.context.restoreWindowStage(this.storage);
630    }
631  }
632
633  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
634    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
635      let continueInput = '';
636      if (want.parameters !== undefined) {
637        continueInput = JSON.stringify(want.parameters.data);
638        hilog.info(DOMAIN_NUMBER, TAG, `continue input ${JSON.stringify(continueInput)}`);
639      }
640      // 触发页面恢复
641      this.context.restoreWindowStage(this.storage);
642    }
643  }
644}
645```
646
647### 使用分布式数据对象迁移数据
648
649当需要迁移的数据较大(100KB以上)或需要迁移文件时,可以使用[分布式数据对象](../reference/apis-arkdata/js-apis-data-distributedobject.md)。原理与接口说明详见[分布式数据对象跨设备数据同步](../database/data-sync-of-distributed-data-object.md)。
650
651  > **说明:**
652  >
653  > 自API 12起,由于直接使用[跨设备文件访问](../file-management/file-access-across-devices.md)实现文件的迁移难以获取文件同步完成的时间,为了保证更高的成功率,文件数据的迁移不建议继续通过该方式实现,推荐使用分布式数据对象携带资产的方式进行。开发者此前通过跨设备文件访问实现的文件迁移依然生效。
654
655#### 基础数据的迁移
656
657使用分布式数据对象,需要在源端[onContinue()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncontinue)接口中进行数据保存,并在对端的[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)接口中进行数据恢复。
658
659在源端,将需要迁移的数据保存到分布式数据对象[DataObject](../reference/apis-arkdata/js-apis-data-distributedobject.md#dataobject)中。
660
661- 在onContinue()接口中使用[create()](../reference/apis-arkdata/js-apis-data-distributedobject.md#distributeddataobjectcreate9)接口创建分布式数据对象,将所要迁移的数据填充到分布式数据对象数据中。
662- 调用[genSessionId()](../reference/apis-arkdata/js-apis-data-distributedobject.md#distributeddataobjectgensessionid)接口生成数据对象组网id,并使用该id调用[setSessionId()](../reference/apis-arkdata/js-apis-data-distributedobject.md#setsessionid9)加入组网,激活分布式数据对象。
663- 使用[save()](../reference/apis-arkdata/js-apis-data-distributedobject.md#save9)接口将已激活的分布式数据对象持久化,确保源端退出后对端依然可以获取到数据。
664- 将生成的`sessionId`通过`want`传递到对端,供对端激活同步使用。
665
666> **注意**
667>
668> 1. 分布式数据对象需要先激活,再持久化,因此必须在`setSessionId()`后调用save()接口。
669> 2. 对于源端迁移后需要退出的应用,为了防止数据未保存完成应用就退出,应采用`await`的方式等待save()接口执行完毕。从API 12 起,`onContinue()`接口提供了`async`版本供该场景使用。
670> 3. 当前,`wantParams`中`sessionId`字段在迁移流程中被系统占用,建议开发者在`wantParams`中定义其他key值存储该id,避免数据异常。
671
672示例代码如下:
673
674```ts
675// 导入模块
676import { distributedDataObject } from '@kit.ArkData';
677import { UIAbility, AbilityConstant } from '@kit.AbilityKit';
678import { BusinessError } from '@kit.BasicServicesKit';
679import { hilog } from '@kit.PerformanceAnalysisKit';
680
681const TAG: string = '[MigrationAbility]';
682const DOMAIN_NUMBER: number = 0xFF00;
683
684// 业务数据定义
685class ParentObject {
686  mother: string
687  father: string
688
689  constructor(mother: string, father: string) {
690    this.mother = mother
691    this.father = father
692  }
693}
694
695// 支持字符、数字、布尔值、对象的传递
696class SourceObject {
697  name: string | undefined
698  age: number | undefined
699  isVis: boolean | undefined
700  parent: ParentObject | undefined
701
702  constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined, parent: ParentObject | undefined) {
703    this.name = name
704    this.age = age
705    this.isVis = isVis
706    this.parent = parent
707  }
708}
709
710export default class MigrationAbility extends UIAbility {
711  d_object?: distributedDataObject.DataObject;
712
713  async onContinue(wantParam: Record<string, Object>): Promise<AbilityConstant.OnContinueResult> {
714    // ...
715    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
716    let source: SourceObject = new SourceObject("jack", 18, false, parentSource);
717
718    // 创建分布式数据对象
719    this.d_object = distributedDataObject.create(this.context, source);
720
721    // 生成数据对象组网id,激活分布式数据对象
722    let dataSessionId: string = distributedDataObject.genSessionId();
723    this.d_object.setSessionId(dataSessionId);
724
725    // 将组网id存在want中传递到对端
726    wantParam['dataSessionId'] = dataSessionId;
727
728    // 数据对象持久化,确保迁移后即使应用退出,对端依然能够恢复数据对象
729    // 从wantParam.targetDevice中获取到对端设备的networkId作为入参
730    await this.d_object.save(wantParam.targetDevice as string).then((result:
731      distributedDataObject.SaveSuccessResponse) => {
732      hilog.info(DOMAIN_NUMBER, TAG, `Succeeded in saving. SessionId: ${result.sessionId}`,
733        `version:${result.version}, deviceId:${result.deviceId}`);
734    }).catch((err: BusinessError) => {
735      hilog.error(DOMAIN_NUMBER, TAG, 'Failed to save. Error: ', JSON.stringify(err) ?? '');
736    });
737
738    return AbilityConstant.OnContinueResult.AGREE;
739  }
740}
741```
742
743对端在[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)中,通过加入与源端一致的分布式数据对象组网进行数据恢复。
744
745- 创建空的分布式数据对象,用于接收恢复的数据。
746- 从[want](../reference/apis-ability-kit/js-apis-app-ability-want.md)中读取分布式数据对象组网id。
747- 注册[on()](../reference/apis-arkdata/js-apis-data-distributedobject.md#onstatus9)接口监听数据变更。在收到`status`为`restore`的事件的回调中,实现数据恢复完毕时需要进行的业务操作。
748- 调用[setSessionId()](../reference/apis-arkdata/js-apis-data-distributedobject.md#setsessionid9)加入组网,激活分布式数据对象。
749
750> **注意**
751>
752> 1. 对端加入组网的分布式数据对象不能为临时变量,因为on()接口的回调可能在onCreate()/onNewWant()执行结束后才执行,临时变量被释放可能导致空指针异常。可以使用类成员变量避免该问题。
753> 2. 对端用于创建分布式数据对象的`Object`,其属性应在激活分布式数据对象前置为`undefined`,否则会导致新数据加入组网后覆盖源端数据,数据恢复失败。
754> 3. 应当在激活分布式数据对象之前,调用on()接口进行注册监听,防止错过`restore`事件导致数据恢复失败。
755
756示例代码如下:
757
758```ts
759import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
760import { distributedDataObject } from '@kit.ArkData';
761import { hilog } from '@kit.PerformanceAnalysisKit';
762
763const TAG: string = '[MigrationAbility]';
764const DOMAIN_NUMBER: number = 0xFF00;
765
766// 示例数据对象定义与上同
767export default class MigrationAbility extends UIAbility {
768  d_object?: distributedDataObject.DataObject;
769
770  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
771    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
772      // ...
773      // 调用封装好的分布式数据对象处理函数
774      this.handleDistributedData(want);
775    }
776  }
777
778  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
779    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
780      if (want.parameters !== undefined) {
781        // ...
782        // 调用封装好的分布式数据对象处理函数
783        this.handleDistributedData(want);
784      }
785    }
786  }
787
788  handleDistributedData(want: Want) {
789    // 创建空的分布式数据对象
790    let remoteSource: SourceObject = new SourceObject(undefined, undefined, undefined, undefined);
791    this.d_object = distributedDataObject.create(this.context, remoteSource);
792
793    // 读取分布式数据对象组网id
794    let dataSessionId = '';
795    if (want.parameters !== undefined) {
796      dataSessionId = want.parameters.dataSessionId as string;
797    }
798
799    // 添加数据变更监听
800    this.d_object.on("status", (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
801      hilog.info(DOMAIN_NUMBER, TAG, "status changed " + sessionId + " " + status + " " + networkId);
802      if (status == 'restored') {
803        if (this.d_object) {
804          // 收到迁移恢复的状态时,可以从分布式数据对象中读取数据
805          hilog.info(DOMAIN_NUMBER, TAG, "restored name:" + this.d_object['name']);
806          hilog.info(DOMAIN_NUMBER, TAG, "restored parents:" + JSON.stringify(this.d_object['parent']));
807        }
808      }
809    });
810
811    // 激活分布式数据对象
812    this.d_object.setSessionId(dataSessionId);
813  }
814}
815```
816
817#### 文件资产的迁移
818
819对于图片、文档等文件类数据,需要先将其转换为[资产`commonType.Asset`](../reference/apis-arkdata/js-apis-data-commonType.md#asset)类型,再封装到分布式数据对象中进行迁移。迁移实现方式与普通的分布式数据对象类似,下例中仅针对区别部分进行说明。
820
821在源端,将需要迁移的文件资产保存到分布式数据对象[DataObject](../reference/apis-arkdata/js-apis-data-distributedobject.md#dataobject)中。
822
823- 将文件资产拷贝到[分布式文件目录](application-context-stage.md#获取应用文件路径)下,相关接口与用法详见[基础文件接口](../file-management/app-file-access.md)。
824- 使用分布式文件目录下的文件创建`Asset`资产对象。
825- 将`Asset`资产对象作为分布式数据对象的根属性保存。
826
827随后,与普通数据对象的迁移的源端实现相同,可以使用该数据对象加入组网,并进行持久化保存。
828
829示例如下:
830
831```ts
832// 导入模块
833import { UIAbility, AbilityConstant } from '@kit.AbilityKit';
834import { distributedDataObject, commonType } from '@kit.ArkData';
835import { fileIo, fileUri } from '@kit.CoreFileKit';
836import { hilog } from '@kit.PerformanceAnalysisKit';
837import { BusinessError } from '@ohos.base';
838
839const TAG: string = '[MigrationAbility]';
840const DOMAIN_NUMBER: number = 0xFF00;
841
842// 数据对象定义
843class ParentObject {
844  mother: string
845  father: string
846
847  constructor(mother: string, father: string) {
848    this.mother = mother
849    this.father = father
850  }
851}
852
853class SourceObject {
854  name: string | undefined
855  age: number | undefined
856  isVis: boolean | undefined
857  parent: ParentObject | undefined
858  attachment: commonType.Asset | undefined // 新增资产属性
859
860  constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined,
861              parent: ParentObject | undefined, attachment: commonType.Asset | undefined) {
862    this.name = name
863    this.age = age
864    this.isVis = isVis
865    this.parent = parent
866    this.attachment = attachment;
867  }
868}
869
870export default class MigrationAbility extends UIAbility {
871  d_object?: distributedDataObject.DataObject;
872
873  async onContinue(wantParam: Record<string, Object>): Promise<AbilityConstant.OnContinueResult> {
874    // ...
875
876    // 1. 将资产写入分布式文件目录下
877    let distributedDir: string = this.context.distributedFilesDir; // 获取分布式文件目录路径
878    let fileName: string = '/test.txt'; // 文件名
879    let filePath: string = distributedDir + fileName; // 文件路径
880
881    try {
882      // 在分布式目录下创建文件
883      let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
884      hilog.info(DOMAIN_NUMBER, TAG, 'Create file success.');
885      // 向文件中写入内容(若资产为图片,可将图片转换为buffer后写入)
886      fileIo.writeSync(file.fd, '[Sample] Insert file content here.');
887      // 关闭文件
888      fileIo.closeSync(file.fd);
889    } catch (error) {
890      let err: BusinessError = error as BusinessError;
891      hilog.error(DOMAIN_NUMBER, TAG, `Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
892    }
893
894    // 2. 使用分布式文件目录下的文件创建资产对象
895    let distributedUri: string = fileUri.getUriFromPath(filePath); // 获取分布式文件Uri
896
897    // 获取文件参数
898    let ctime: string = '';
899    let mtime: string = '';
900    let size: string = '';
901    await fileIo.stat(filePath).then((stat: fileIo.Stat) => {
902      ctime = stat.ctime.toString(); // 创建时间
903      mtime = stat.mtime.toString(); // 修改时间
904      size = stat.size.toString(); // 文件大小
905    })
906
907    // 创建资产对象
908    let attachment: commonType.Asset = {
909      name: fileName,
910      uri: distributedUri,
911      path: filePath,
912      createTime: ctime,
913      modifyTime: mtime,
914      size: size,
915    }
916
917    // 3. 将资产对象作为分布式数据对象的根属性,创建分布式数据对象
918    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
919    let source: SourceObject = new SourceObject("jack", 18, false, parentSource, attachment);
920    this.d_object = distributedDataObject.create(this.context, source);
921
922    // 生成组网id,激活分布式数据对象,save持久化保存
923    // ...
924
925    return AbilityConstant.OnContinueResult.AGREE;
926  }
927}
928```
929
930对端需要先创建一个各属性为空的`Asset`资产对象作为分布式数据对象的根属性。在接收到[on()](../reference/apis-arkdata/js-apis-data-distributedobject.md#onstatus9)接口`status`为`restored`的事件的回调时,表示包括资产在内的数据同步完成,可以像获取基本数据一样获取到源端的资产对象。
931
932> **注意**
933>
934> 对端创建分布式数据对象时,`SourceObject`对象中的资产不能直接使用`undefined`初始化,需要创建一个所有属性初始值为空的Asset资产对象,使分布式对象可以识别出资产类型。
935
936```ts
937import { UIAbility, Want } from '@kit.AbilityKit';
938import { distributedDataObject, commonType } from '@kit.ArkData';
939import { hilog } from '@kit.PerformanceAnalysisKit';
940
941const TAG: string = '[MigrationAbility]';
942const DOMAIN_NUMBER: number = 0xFF00;
943
944export default class MigrationAbility extends UIAbility {
945  d_object?: distributedDataObject.DataObject;
946
947  handleDistributedData(want: Want) {
948    // ...
949    // 创建一个各属性为空的资产对象
950    let attachment: commonType.Asset = {
951      name: '',
952      uri: '',
953      path: '',
954      createTime: '',
955      modifyTime: '',
956      size: '',
957    }
958
959    // 使用该空资产对象创建分布式数据对象,其余基础属性可以直接使用undefined
960    let source: SourceObject = new SourceObject(undefined, undefined, undefined, undefined, attachment);
961    this.d_object = distributedDataObject.create(this.context, source);
962
963    this.d_object.on("status", (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
964      if (status == 'restored') {
965        if (this.d_object) {
966          // 收到监听的restored回调,表示分布式资产对象同步完成
967          hilog.info(DOMAIN_NUMBER, TAG, "restored attachment:" + JSON.stringify(this.d_object['attachment']));
968        }
969      }
970    });
971    // ...
972  }
973}
974```
975
976若应用想要同步多个资产,可采用两种方式实现
977
9781. 可将每个资产作为分布式数据对象的一个根属性实现,适用于要迁移的资产数量固定的场景。
9792. 可以将资产数组传化为`Object`传递,适用于需要迁移的资产个数会动态变化的场景(如,用户选择了不定数量的图片)。当前不支持直接将资产数组作为根属性传递。
980
981其中方式1的实现可以直接参照添加一个资产的方式添加更多资产。方式2的示例如下所示:
982
983```ts
984// 导入模块
985import { distributedDataObject, commonType } from '@kit.ArkData';
986import { UIAbility } from '@kit.AbilityKit';
987
988// 数据对象定义
989class SourceObject {
990  name: string | undefined
991  assets: Object | undefined // 分布式数据对象的中添加一个Object属性
992
993  constructor(name: string | undefined, assets: Object | undefined) {
994    this.name = name
995    this.assets = assets;
996  }
997}
998
999export default class MigrationAbility extends UIAbility {
1000  d_object?: distributedDataObject.DataObject;
1001
1002  // 该函数用于将资产数组转为Record
1003  GetAssetsWapper(assets: commonType.Assets): Record<string, commonType.Asset> {
1004    let wrapper: Record<string, commonType.Asset> = {}
1005    let num: number = assets.length;
1006    for (let i: number = 0; i < num; i++) {
1007      wrapper[`asset${i}`] = assets[i];
1008    }
1009    return wrapper;
1010  }
1011
1012  async onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
1013    // ...
1014
1015    // 创建了多个资产对象
1016    let attachment1: commonType.Asset = {
1017      // ...
1018    }
1019
1020    let attachment2: commonType.Asset = {
1021      // ...
1022    }
1023
1024    // 将资产对象插入资产数组
1025    let assets: commonType.Assets = [];
1026    assets.push(attachment1);
1027    assets.push(attachment2);
1028
1029    // 将资产数组转为Record Object,并用于创建分布式数据对象
1030    let assetsWrapper: Object = this.GetAssetsWapper(assets);
1031    let source: SourceObject = new SourceObject("jack", assetsWrapper);
1032    this.d_object = distributedDataObject.create(this.context, source);
1033
1034    // ...
1035  }
1036}
1037```
1038
1039## 验证指导
1040
1041为方便开发者验证已开发的可迁移应用,系统提供了一个全局任务中心demo应用`MissionCenter`作为迁移的入口。下面介绍通过安装全局任务中心来验证迁移的方式。
1042
1043> **说明:**
1044>
1045> 本文中的截图仅为参考,具体的显示界面请以实际使用的DevEco Studio和SDK的版本为准。
1046
10471. 编译安装全局任务中心。
1048
1049    1. 为了正确编译安装全局任务中心,开发者需要替换Full-SDK,具体操作可参见[替换指南](../faqs/full-sdk-switch-guide.md)。
1050
1051    2. 下载[MissionCenter_Demo](https://gitee.com/openharmony/ability_dmsfwk/tree/master/services/dtbschedmgr/test/missionCenterDemo/dmsDemo/entry/src/main)示例代码。
1052
1053    3. 编译工程文件。
1054
1055        1. 新建一个工程,找到对应的文件夹替换下载文件。
1056
1057            ![hop-cross-device-migration](figures/hop-cross-device-migration1.png)
1058
1059        2. 自动签名,编译安装。
1060        ​DevEco的自动签名模板默认签名权限为`normal`级。而本应用所需`ohos.permission.MANAGE_MISSIONS`权限为`system_core`级别。自动生成的签名无法获得足够的权限,所以需要将权限升级为`system_core`级别,然后签名。
1061            1. 将Sdk目录下的`openharmony\api版本 (如:10)\toolchains\lib\UnsignedReleasedProfileTemplate.json`文件中的`"apl":"normal"`改为`"apl":"system_core"`。
1062
1063            2. 点击 **file->Project Structure**。
1064
1065                ![hop-cross-device-migration](figures/hop-cross-device-migration2.png)
1066
1067            3. 点击 **Signing Configs**  点击 **OK**。
1068
1069                ![hop-cross-device-migration](figures/hop-cross-device-migration3.png)
1070
1071        3. 连接开发板运行生成demo。
1072
10732. 设备组网。
1074
1075    1. 打开A,B两设备的计算器。
1076    2. 点击右上角箭头选择B设备。
1077    3. 在B设备选择信任设备,弹出PIN码。
1078    4. 在A设备输入PIN码。
1079    5. 已组网成功,验证方法:在A设备输入数字,B设备同步出现则证明组网成功。
1080
10813. 发起迁移。
1082
1083    1. 在B设备打开多设备协同权限的应用,A设备打开全局任务中心demo,demo显示A设备名称和B设备名称。
1084    2. 点击B设备名称,列表显示B设备的应用卡片列表。
1085    3. 将要接续的应用卡片拖拽到A设备名称处,A设备应用被拉起。
1086
1087## 常见问题
1088
1089### Q1:迁移之后的应用,无法重新迁移回源端
1090
1091由于迁移状态的设置是Ability级别的,对端拉起的应用可能执行过自己的迁移状态设置命令(例如,冷启动时对端在[onCreate()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityoncreate)中设置了 **INACTIVE** ;热启动时对端已打开了不可迁移的页面,迁移状态为 **INACTIVE** 等情况)。为了保证迁移过后的应用依然具有可以迁移回源端的能力,应在 onCreate()/[onNewWant()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)的迁移调用判断中,将迁移状态设置为 **ACTIVE** 。
1092
1093```ts
1094// MigrationAbility.ets
1095import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
1096import { hilog } from '@kit.PerformanceAnalysisKit';
1097
1098const TAG: string = '[MigrationAbility]';
1099const DOMAIN_NUMBER: number = 0xFF00;
1100
1101export default class MigrationAbility extends UIAbility {
1102  // ...
1103  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
1104    // ...
1105    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
1106      // ...
1107      // 调用原因为迁移时,设置状态为可迁移,应对冷启动情况
1108      this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
1109        hilog.info(DOMAIN_NUMBER, TAG, `setMissionContinueState ACTIVE result: ${JSON.stringify(result)}`);
1110      });
1111    }
1112    // ...
1113  }
1114
1115  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
1116    // ...
1117    // 调用原因为迁移时,设置状态为可迁移,应对热启动情况
1118    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
1119      this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, (result) => {
1120        hilog.info(DOMAIN_NUMBER, TAG, `setMissionContinueState ACTIVE result: ${JSON.stringify(result)}`);
1121      });
1122    }
1123  }
1124  // ...
1125}
1126```
1127
1128### Q2:在onWindowStageRestore()中调用loadContent()没有生效
1129
1130如果应用没有配置关闭页面栈迁移能力,系统默认对应用的页面栈进行迁移与加载。这种情况下,如果开发者在生命周期函数[onWindowStageRestore()](../reference/apis-ability-kit/js-apis-app-ability-uiAbility.md#uiabilityonwindowstagerestore)中再次通过[loadContent()](../reference/apis-arkui/js-apis-window.md#loadcontent9)等方式触发指定页面加载,则这次加载不会生效,依然恢复页面栈中的页面。
1131
1132## 相关实例
1133
1134针对跨端迁移的开发,有以下相关实例可供参考:
1135
1136[跨端迁移随手记(ArkTS)(Public SDK)(API12)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/DistributedAppDev/DistributedJotNote)
1137