鸿蒙应用接续开发指导
一、概述
1.1 什么是应用接续
应用接续(App Continuation)是HarmonyOS提供的一项跨设备协同能力,允许用户在一个设备上操作某个应用时,能够在另一个设备上快速切换并继续该应用的任务,实现跨设备无缝体验。
通过应用接续,用户可以:
- 从手机阅读文章切换到平板,继续从中断位置阅读
- 在PC编辑文档后,用手机继续编辑任务
- 在手机上观看视频,无缝切换到平板继续播放
1.2 核心特性
应用接续基于HarmonyOS的分布式状态同步技术,具备以下核心特性:
- 数据无缝同步:支持存储及恢复自定义数据(业务内容)
- 页面状态保持:支持存储及恢复页面路由信息和控制状态
- 版本兼容检测:提供应用版本兼容性检测机制
- 灵活配置:支持动态设置迁移状态、选择页面栈恢复、控制源端应用退出行为
二、适用场景
2.1 推荐接续场景
| 应用类型 | 推荐场景 | 同步内容 |
|---|---|---|
| 浏览器 | 网页内容详情页 | 浏览进度、页面位置 |
| 备忘录 | 详情页 | 浏览进度、编辑内容 |
| 新闻应用 | 文章阅读页 | 阅读进度、滚动位置 |
| 视频应用 | 播放页 | 播放进度、播放列表 |
| 音乐应用 | 播放页和歌单页 | 播放进度、当前歌曲 |
| 阅读应用 | 小说阅读页 | 阅读进度、章节位置 |
| 办公应用 | 编辑页面 | 编辑内容、格式状态 |
| 地图应用 | 路线查询和导航界面 | 当前路线、导航状态 |
三、前置条件
3.1 系统要求
- 系统版本:HarmonyOS NEXT Developer Preview0及以上
- 设备要求:
- 双端登录同一华为账号
- 打开Wi-Fi和蓝牙
- 建议接入同一局域网
3.2 应用要求
- 接续只能在同一应用(UIAbility)的不同设备实例间触发
- onContinue回调中wantParam传输数据需控制在100KB以下
- 对于超过100KB的大数据,建议使用分布式数据对象进行同步
四、应用接续架构
4.1 整体架构流程
应用接续涉及源端设备和目标端设备的协同工作:
sequenceDiagram
participant 用户
participant 源端设备
participant 系统服务
participant 目标端设备
用户->>源端设备: 触发接续操作
源端设备->>源端设备: 调用onContinue()
源端设备->>源端设备: 保存状态数据到wantParam
源端设备->>源端设备: 版本兼容性检测
源端设备->>系统服务: 返回AGREE/MISMATCH
alt 版本兼容
系统服务->>目标端设备: 传输wantParam数据
目标端设备->>目标端设备: 调用onCreate()/onNewWant()
目标端设备->>目标端设备: 判断LaunchReason.CONTINUATION
目标端设备->>目标端设备: 从want.parameters恢复数据
目标端设备->>目标端设备: 调用onWindowStageRestore()
目标端设备->>目标端设备: 恢复页面栈和UI状态
目标端设备->>用户: 展示接续后的界面
else 版本不兼容
系统服务->>用户: 提示版本不兼容
end
4.2 生命周期调用流程
不同启动模式下,应用接续的生命周期函数调用顺序有所不同:
sequenceDiagram
participant 系统
participant UIAbility(源端)
participant UIAbility(目标端)
Note over 系统,UIAbility(目标端): 源端保存数据阶段
系统->>UIAbility(源端): 触发接续
UIAbility(源端)->>UIAbility(源端): onContinue(wantParam)
UIAbility(源端)->>UIAbility(源端): 保存数据到wantParam
UIAbility(源端)->>UIAbility(源端): 检查版本兼容性
UIAbility(源端)-->>系统: 返回 OnContinueResult.AGREE
Note over 系统,UIAbility(目标端): 目标端恢复数据阶段(冷启动)
系统->>UIAbility(目标端): onCreate(want, launchParam)
UIAbility(目标端)->>UIAbility(目标端): 检查 launchReason == CONTINUATION
UIAbility(目标端)->>UIAbility(目标端): 从 want.parameters 获取数据
系统->>UIAbility(目标端): onWindowStageRestore(windowStage)
UIAbility(目标端)->>UIAbility(目标端): 恢复页面栈和UI状态
Note over UIAbility(目标端): ⚠️ 不执行 onWindowStageCreate()
Note over 系统,UIAbility(目标端): 目标端恢复数据阶段(单实例热启动)
系统->>UIAbility(目标端): onNewWant(want, launchParam)
UIAbility(目标端)->>UIAbility(目标端): 检查 launchReason == CONTINUATION
UIAbility(目标端)->>UIAbility(目标端): 从 want.parameters 获取数据
系统->>UIAbility(目标端): onWindowStageRestore(windowStage)
UIAbility(目标端)->>UIAbility(目标端): 恢复页面栈和UI状态
五、开发步骤
5.1 Step 1:开启应用接续能力
在module.json5的abilities配置中启用接续能力:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"continuable": true, // 开启应用接续
"launchType": "singleton"
}
]
}
}
5.2 Step 2:配置应用启动模式
HarmonyOS提供三种启动模式,影响应用接续的生命周期调用:
| 启动模式 | 说明 | 目标端恢复接口 | 适用场景 |
|---|---|---|---|
| singleton(单实例) | 系统中只存在一个该UIAbility实例 | onNewWant() | 大多数应用 |
| multiton(多实例) | 每次启动创建新实例 | onCreate() | 支持多窗口的应用 |
| specified(指定实例) | 运行时决定是否创建新实例 | onCreate() 或 onNewWant() | 复杂业务场景 |
{
"module": {
"abilities": [
{
"launchType": "singleton" // 或 "multiton" / "specified"
}
]
}
}
5.3 Step 3:源端保存数据(onContinue)
在源端UIAbility中实现onContinue()接口,完成数据保存和版本检测:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[ContinuationAbility]';
const DOMAIN = 0xFF00;
export default class EntryAbility extends UIAbility {
/**
* 源端保存接续数据
* @param wantParam 用于保存数据的参数对象
* @returns OnContinueResult 返回是否同意接续
*/
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
hilog.info(DOMAIN, TAG, '应用接续开始,保存数据');
// 1. 版本兼容性检测
const targetVersion = wantParam.version as string;
const currentVersion = '1.0.0'; // 当前应用版本
if (targetVersion !== currentVersion) {
hilog.warn(DOMAIN, TAG, `版本不匹配: 源端=${currentVersion}, 目标端=${targetVersion}`);
return AbilityConstant.OnContinueResult.MISMATCH; // 版本不兼容,拒绝接续
}
// 2. 保存业务数据(控制在100KB以内)
wantParam['currentPage'] = 'detailPage';
wantParam['articleId'] = '12345';
wantParam['scrollPosition'] = 1280; // 滚动位置
wantParam['readingProgress'] = 65; // 阅读进度
wantParam['lastUpdateTime'] = Date.now();
// 3. 保存复杂对象数据
const userData = {
userId: 'user001',
bookmarks: ['pos1', 'pos2'],
settings: {
fontSize: 16,
theme: 'dark'
}
};
wantParam['userData'] = JSON.stringify(userData);
hilog.info(DOMAIN, TAG, `数据保存完成,大小约 ${JSON.stringify(wantParam).length} 字节`);
return AbilityConstant.OnContinueResult.AGREE; // 同意接续
}
}
关键要点:
wantParam.version:获取目标端应用版本,用于兼容性检测- 返回值:
AGREE:同意接续,系统将数据传输到目标端MISMATCH:版本不兼容,拒绝接续REJECT:拒绝接续
- 数据大小限制:wantParam总大小需控制在100KB以下
5.4 Step 4:目标端恢复数据
5.4.1 冷启动场景(onCreate)
多实例模式或首次启动时,通过onCreate()恢复数据:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[ContinuationAbility]';
const DOMAIN = 0xFF00;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, 'UIAbility onCreate');
// 1. 判断是否为接续启动
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
hilog.info(DOMAIN, TAG, '接续启动,开始恢复数据');
// 2. 从want.parameters获取接续数据
const params = want.parameters;
if (params) {
const currentPage = params['currentPage'] as string;
const articleId = params['articleId'] as string;
const scrollPosition = params['scrollPosition'] as number;
const readingProgress = params['readingProgress'] as number;
// 3. 恢复复杂对象数据
const userDataStr = params['userData'] as string;
if (userDataStr) {
const userData = JSON.parse(userDataStr);
hilog.info(DOMAIN, TAG, `用户数据: ${JSON.stringify(userData)}`);
}
// 4. 保存到全局状态或缓存,供页面使用
AppStorage.setOrCreate('continuationData', {
currentPage,
articleId,
scrollPosition,
readingProgress
});
hilog.info(DOMAIN, TAG, `数据恢复完成: 文章=${articleId}, 进度=${readingProgress}%`);
}
} else {
hilog.info(DOMAIN, TAG, '正常启动');
}
}
/**
* 接续场景下的页面栈恢复
* ⚠️ 注意:接续时不会调用onWindowStageCreate()
*/
onWindowStageRestore(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, TAG, 'onWindowStageRestore 开始');
// 1. 加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, `加载页面失败: ${JSON.stringify(err)}`);
return;
}
// 2. 获取接续数据
const continuationData = AppStorage.get<object>('continuationData');
// 3. 触发页面恢复逻辑
if (continuationData) {
hilog.info(DOMAIN, TAG, '触发页面状态恢复');
// 通知页面进行UI状态恢复
AppStorage.setOrCreate('shouldRestore', true);
}
hilog.info(DOMAIN, TAG, 'onWindowStageRestore 完成');
});
}
}
5.4.2 单实例热启动场景(onNewWant)
单实例模式下,若应用已运行,通过onNewWant()恢复数据:
export default class EntryAbility extends UIAbility {
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, 'UIAbility onNewWant');
// 1. 判断是否为接续启动
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
hilog.info(DOMAIN, TAG, '接续启动(热启动),开始恢复数据');
// 2. 从want.parameters获取接续数据
const params = want.parameters;
if (params) {
const currentPage = params['currentPage'] as string;
const articleId = params['articleId'] as string;
const scrollPosition = params['scrollPosition'] as number;
// 3. 更新全局状态
AppStorage.setOrCreate('continuationData', {
currentPage,
articleId,
scrollPosition
});
// 4. 触发页面更新
AppStorage.setOrCreate('shouldRestore', true);
hilog.info(DOMAIN, TAG, `热启动数据恢复完成`);
}
}
}
// onWindowStageRestore() 实现同上
}
5.5 Step 5:页面层数据恢复
在页面代码中监听恢复标志,执行UI状态恢复:
import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct ArticleDetailPage {
@StorageLink('shouldRestore') shouldRestore: boolean = false;
@StorageLink('continuationData') continuationData: object | undefined = undefined;
@State articleId: string = '';
@State scrollPosition: number = 0;
private scroller: Scroller = new Scroller();
aboutToAppear() {
// 监听接续恢复
if (this.shouldRestore && this.continuationData) {
this.restorePageState();
AppStorage.setOrCreate('shouldRestore', false); // 重置标志
}
}
/**
* 恢复页面状态
*/
private restorePageState() {
const data = this.continuationData as Record<string, object>;
// 恢复业务数据
this.articleId = data['articleId'] as string;
this.scrollPosition = data['scrollPosition'] as number;
hilog.info(0xFF00, '[ArticlePage]', `恢复文章ID: ${this.articleId}`);
// 恢复滚动位置
setTimeout(() => {
this.scroller.scrollTo({
xOffset: 0,
yOffset: this.scrollPosition,
animation: { duration: 300 }
});
hilog.info(0xFF00, '[ArticlePage]', `恢复滚动位置: ${this.scrollPosition}`);
}, 100);
}
build() {
Column() {
Scroll(this.scroller) {
// 文章内容
Text(`文章内容 - ID: ${this.articleId}`)
.fontSize(16)
.padding(20)
}
.height('100%')
}
}
}
六、高级特性
6.1 动态设置迁移状态
可在运行时动态控制是否允许应用接续:
import { wantConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
// 检查当前业务状态
const isUploading = this.checkUploadStatus();
if (isUploading) {
hilog.warn(DOMAIN, TAG, '正在上传文件,暂不支持接续');
return AbilityConstant.OnContinueResult.REJECT;
}
// 检查敏感数据
const hasSensitiveData = this.checkSensitiveData();
if (hasSensitiveData) {
hilog.warn(DOMAIN, TAG, '包含敏感数据,拒绝接续');
return AbilityConstant.OnContinueResult.REJECT;
}
// 正常接续
wantParam['data'] = 'business data';
return AbilityConstant.OnContinueResult.AGREE;
}
private checkUploadStatus(): boolean {
// 检查上传状态逻辑
return false;
}
private checkSensitiveData(): boolean {
// 检查敏感数据逻辑
return false;
}
}
6.2 选择性恢复页面栈
默认情况下,系统会自动恢复页面栈。如需自定义页面栈恢复逻辑:
onWindowStageRestore(windowStage: window.WindowStage): void {
const continuationData = AppStorage.get<object>('continuationData');
const data = continuationData as Record<string, object>;
// 根据业务需求决定加载哪个页面
const targetPage = data['currentPage'] as string;
switch (targetPage) {
case 'detailPage':
windowStage.loadContent('pages/ArticleDetail', (err) => {
// 恢复详情页
});
break;
case 'editPage':
windowStage.loadContent('pages/Editor', (err) => {
// 恢复编辑页
});
break;
default:
windowStage.loadContent('pages/Index', (err) => {
// 默认首页
});
}
}
6.3 接续后源端应用行为控制
可配置接续完成后,源端应用是否退出:
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
// 保存数据
wantParam['data'] = 'business data';
// 配置接续后源端不退出(默认会退出)
wantParam['ohos.extra.param.key.supportContinueSourceExit'] = false;
return AbilityConstant.OnContinueResult.AGREE;
}
七、数据管理最佳实践
7.1 数据大小控制
100KB限制示例:
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
const businessData = this.getBusinessData();
const dataStr = JSON.stringify(businessData);
const dataSize = new TextEncoder().encode(dataStr).length;
if (dataSize > 100 * 1024) { // 超过100KB
hilog.warn(DOMAIN, TAG, `数据过大: ${dataSize} 字节,使用分布式对象`);
// 仅传递数据标识
wantParam['dataId'] = businessData.id;
wantParam['useDistributedObject'] = true;
// 使用分布式数据对象同步大数据
this.syncViaDistributedObject(businessData);
} else {
wantParam['data'] = dataStr;
}
return AbilityConstant.OnContinueResult.AGREE;
}
7.2 分布式数据对象
对于大数据(>100KB),推荐使用分布式数据对象:
import { distributedDataObject } from '@kit.ArkData';
export default class EntryAbility extends UIAbility {
private distributedObject?: distributedDataObject.DataObject;
// 源端:创建分布式对象
private syncViaDistributedObject(data: object) {
this.distributedObject = distributedDataObject.create(this.context, data);
this.distributedObject.setSessionId('session_' + Date.now());
hilog.info(DOMAIN, TAG, '分布式对象已创建');
}
// 目标端:订阅分布式对象
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
const params = want.parameters;
if (params && params['useDistributedObject']) {
const dataId = params['dataId'] as string;
this.subscribeDistributedObject(dataId);
}
}
}
private subscribeDistributedObject(dataId: string) {
this.distributedObject = distributedDataObject.create(this.context, {});
this.distributedObject.setSessionId('session_' + dataId);
this.distributedObject.on('change', (sessionId, fields) => {
hilog.info(DOMAIN, TAG, '分布式对象数据已同步');
// 处理同步的数据
});
}
}
八、完整示例
8.1 阅读应用接续示例
场景:用户在手机上阅读小说,切换到平板继续阅读,保持章节和滚动位置。
EntryAbility.ets(完整代码)
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[ReadingAppAbility]';
const DOMAIN = 0xFF00;
const APP_VERSION = '1.0.0';
interface ReadingData {
bookId: string;
chapterId: string;
scrollY: number;
readingProgress: number;
lastUpdateTime: number;
}
export default class EntryAbility extends UIAbility {
// ==================== 源端:保存数据 ====================
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
hilog.info(DOMAIN, TAG, '=== 开始应用接续 ===');
// 1. 版本兼容性检测
const targetVersion = wantParam.version as string;
if (targetVersion !== APP_VERSION) {
hilog.error(DOMAIN, TAG, `版本不匹配: 当前=${APP_VERSION}, 目标=${targetVersion}`);
return AbilityConstant.OnContinueResult.MISMATCH;
}
// 2. 获取当前阅读数据
const readingData: ReadingData = {
bookId: AppStorage.get<string>('currentBookId') || '',
chapterId: AppStorage.get<string>('currentChapterId') || '',
scrollY: AppStorage.get<number>('scrollPosition') || 0,
readingProgress: AppStorage.get<number>('readingProgress') || 0,
lastUpdateTime: Date.now()
};
// 3. 检查数据有效性
if (!readingData.bookId || !readingData.chapterId) {
hilog.warn(DOMAIN, TAG, '无有效阅读数据,取消接续');
return AbilityConstant.OnContinueResult.REJECT;
}
// 4. 序列化并保存数据
wantParam['readingData'] = JSON.stringify(readingData);
wantParam['appVersion'] = APP_VERSION;
// 5. 计算数据大小
const dataSize = JSON.stringify(wantParam).length;
hilog.info(DOMAIN, TAG, `数据保存完成,大小: ${dataSize} 字节`);
hilog.info(DOMAIN, TAG, `书籍: ${readingData.bookId}, 章节: ${readingData.chapterId}, 进度: ${readingData.readingProgress}%`);
if (dataSize > 100 * 1024) {
hilog.warn(DOMAIN, TAG, '⚠️ 数据超过100KB,建议使用分布式对象');
}
return AbilityConstant.OnContinueResult.AGREE;
}
// ==================== 目标端:恢复数据(冷启动) ====================
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, 'onCreate 调用');
// 判断启动原因
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
hilog.info(DOMAIN, TAG, '=== 接续启动(冷启动) ===');
this.restoreData(want);
} else {
hilog.info(DOMAIN, TAG, '正常启动');
}
}
// ==================== 目标端:恢复数据(单实例热启动) ====================
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, 'onNewWant 调用');
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
hilog.info(DOMAIN, TAG, '=== 接续启动(热启动) ===');
this.restoreData(want);
}
}
// ==================== 恢复数据通用方法 ====================
private restoreData(want: Want): void {
const params = want.parameters;
if (!params) {
hilog.error(DOMAIN, TAG, 'want.parameters 为空');
return;
}
try {
// 解析接续数据
const readingDataStr = params['readingData'] as string;
const readingData: ReadingData = JSON.parse(readingDataStr);
// 保存到全局状态
AppStorage.setOrCreate('currentBookId', readingData.bookId);
AppStorage.setOrCreate('currentChapterId', readingData.chapterId);
AppStorage.setOrCreate('scrollPosition', readingData.scrollY);
AppStorage.setOrCreate('readingProgress', readingData.readingProgress);
AppStorage.setOrCreate('isContinuation', true); // 标记为接续启动
hilog.info(DOMAIN, TAG, '✅ 数据恢复成功');
hilog.info(DOMAIN, TAG, `书籍ID: ${readingData.bookId}`);
hilog.info(DOMAIN, TAG, `章节ID: ${readingData.chapterId}`);
hilog.info(DOMAIN, TAG, `滚动位置: ${readingData.scrollY}px`);
hilog.info(DOMAIN, TAG, `阅读进度: ${readingData.readingProgress}%`);
} catch (error) {
hilog.error(DOMAIN, TAG, `数据恢复失败: ${JSON.stringify(error)}`);
}
}
// ==================== 页面栈恢复 ====================
onWindowStageRestore(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, TAG, 'onWindowStageRestore 开始');
// 加载阅读页面
windowStage.loadContent('pages/ReadingPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, `页面加载失败: ${JSON.stringify(err)}`);
return;
}
hilog.info(DOMAIN, TAG, '✅ 阅读页面加载完成');
// 通知页面进行UI恢复
AppStorage.setOrCreate('shouldRestoreUI', true);
});
}
// ==================== 正常启动的页面创建 ====================
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, TAG, 'onWindowStageCreate 开始(正常启动)');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, `页面加载失败: ${JSON.stringify(err)}`);
return;
}
hilog.info(DOMAIN, TAG, '首页加载完成');
});
}
}
ReadingPage.ets(页面恢复代码)
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[ReadingPage]';
const DOMAIN = 0xFF00;
@Entry
@Component
struct ReadingPage {
// 状态管理
@StorageLink('isContinuation') isContinuation: boolean = false;
@StorageLink('shouldRestoreUI') shouldRestoreUI: boolean = false;
@StorageLink('currentBookId') bookId: string = '';
@StorageLink('currentChapterId') chapterId: string = '';
@StorageLink('scrollPosition') scrollY: number = 0;
@StorageLink('readingProgress') progress: number = 0;
// 本地状态
@State content: string = '';
private scroller: Scroller = new Scroller();
// ==================== 生命周期 ====================
aboutToAppear() {
hilog.info(DOMAIN, TAG, '页面即将显示');
// 检查是否需要恢复UI
if (this.isContinuation && this.shouldRestoreUI) {
hilog.info(DOMAIN, TAG, '=== 开始恢复UI状态 ===');
this.restoreUIState();
// 重置恢复标志
AppStorage.setOrCreate('shouldRestoreUI', false);
AppStorage.setOrCreate('isContinuation', false);
} else {
hilog.info(DOMAIN, TAG, '正常页面初始化');
this.loadDefaultContent();
}
}
// ==================== UI状态恢复 ====================
private restoreUIState() {
hilog.info(DOMAIN, TAG, `恢复书籍: ${this.bookId}, 章节: ${this.chapterId}`);
// 1. 加载章节内容
this.loadChapterContent(this.bookId, this.chapterId);
// 2. 延迟恢复滚动位置(等待内容渲染)
setTimeout(() => {
this.scroller.scrollTo({
xOffset: 0,
yOffset: this.scrollY,
animation: {
duration: 500,
curve: Curve.EaseInOut
}
});
hilog.info(DOMAIN, TAG, `✅ 滚动位置已恢复: ${this.scrollY}px, 进度: ${this.progress}%`);
}, 300);
}
// ==================== 内容加载 ====================
private loadChapterContent(bookId: string, chapterId: string) {
// 模拟加载章节内容
this.content = `这是书籍 ${bookId} 的章节 ${chapterId} 的内容...\n\n` +
'第一段内容...\n'.repeat(50);
hilog.info(DOMAIN, TAG, '章节内容加载完成');
}
private loadDefaultContent() {
this.content = '欢迎使用阅读应用';
}
// ==================== UI构建 ====================
build() {
Column() {
// 标题栏
Row() {
Text('阅读中')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text(`进度: ${this.progress}%`)
.fontSize(14)
.margin({ left: 20 })
}
.width('100%')
.padding(16)
.backgroundColor('#f0f0f0')
// 内容区域
Scroll(this.scroller) {
Column() {
Text(this.content)
.fontSize(16)
.lineHeight(28)
.padding(20)
}
}
.width('100%')
.layoutWeight(1)
.onScroll((xOffset: number, yOffset: number) => {
// 实时保存滚动位置
AppStorage.setOrCreate('scrollPosition', yOffset);
})
}
.width('100%')
.height('100%')
}
}
九、常见问题与解决方案
9.1 onWindowStageCreate不执行
问题:接续启动时,onWindowStageCreate()未被调用,导致初始化逻辑缺失。
原因:接续启动时,系统调用onWindowStageRestore(),而不调用onWindowStageCreate()。
解决方案:
// ❌ 错误做法:只在onWindowStageCreate中初始化
onWindowStageCreate(windowStage: window.WindowStage): void {
this.initializeApp(); // 接续时不会执行
}
// ✅ 正确做法:在onWindowStageRestore中也执行初始化
onWindowStageRestore(windowStage: window.WindowStage): void {
this.initializeApp(); // 确保接续时也初始化
windowStage.loadContent('pages/Index', (err) => {
// 页面加载
});
}
9.2 数据超过100KB
问题:wantParam数据超过100KB,导致接续失败。
解决方案:
- 精简数据:仅传输必要的状态标识
- 使用分布式数据对象:大数据通过分布式对象同步
- 数据压缩:对JSON数据进行压缩
// 方案1:仅传输ID
wantParam['dataId'] = businessData.id; // 仅几个字节
// 方案2:数据压缩(示例)
const compressedData = this.compressData(largeData);
wantParam['compressedData'] = compressedData;
9.3 版本兼容性处理
问题:不同版本应用间接续失败。
解决方案:
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
const targetVersion = wantParam.version as string;
const currentMajorVersion = APP_VERSION.split('.')[0];
const targetMajorVersion = targetVersion.split('.')[0];
// 仅检查主版本号
if (currentMajorVersion !== targetMajorVersion) {
return AbilityConstant.OnContinueResult.MISMATCH;
}
// 次版本号不同时,提供降级数据
wantParam['dataVersion'] = currentMajorVersion;
wantParam['data'] = this.getCompatibleData(targetVersion);
return AbilityConstant.OnContinueResult.AGREE;
}
9.4 页面状态丢失
问题:接续后,页面部分状态未恢复。
解决方案:
// 使用AppStorage/PersistentStorage保存关键状态
aboutToAppear() {
if (this.isContinuation) {
// 恢复所有必要状态
this.fontSize = AppStorage.get('fontSize') || 16;
this.theme = AppStorage.get('theme') || 'light';
this.bookmarks = AppStorage.get('bookmarks') || [];
// 触发UI更新
this.restoreUIState();
}
}
十、性能优化建议
10.1 数据序列化优化
// ❌ 低效:多次序列化
wantParam['data1'] = JSON.stringify(obj1);
wantParam['data2'] = JSON.stringify(obj2);
// ✅ 高效:一次性序列化
const combinedData = { data1: obj1, data2: obj2 };
wantParam['allData'] = JSON.stringify(combinedData);
10.2 异步加载优化
private async restoreUIState() {
// 先恢复轻量数据,快速展示界面
this.restoreLightweightState();
// 异步加载重数据
Promise.all([
this.loadChapterContent(this.bookId, this.chapterId),
this.loadUserPreferences(),
this.loadBookmarks()
]).then(() => {
hilog.info(DOMAIN, TAG, '所有数据加载完成');
this.scroller.scrollTo({ yOffset: this.scrollY });
});
}
10.3 滚动位置恢复优化
// ❌ 立即恢复可能失败(内容未渲染)
this.scroller.scrollTo({ yOffset: this.scrollY });
// ✅ 延迟恢复,确保内容已渲染
setTimeout(() => {
this.scroller.scrollTo({ yOffset: this.scrollY });
}, 300);
// ✅ 最佳:监听内容加载完成
onContentLoaded() {
this.scroller.scrollTo({ yOffset: this.scrollY });
}
十一、测试与调试
11.1 日志输出
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Continuation]';
const DOMAIN = 0xFF00;
// 源端日志
hilog.info(DOMAIN, TAG, `[源端] 保存数据: ${JSON.stringify(data)}`);
// 目标端日志
hilog.info(DOMAIN, TAG, `[目标端] 恢复数据: ${JSON.stringify(data)}`);
// 性能日志
const startTime = Date.now();
// ... 操作 ...
hilog.info(DOMAIN, TAG, `耗时: ${Date.now() - startTime}ms`);
11.2 测试清单
- 单实例模式接续测试
- 多实例模式接续测试
- 冷启动接续测试
- 热启动接续测试
- 版本兼容性测试
- 大数据(接近100KB)接续测试
- 网络断开场景测试
- 快速切换设备测试
- UI状态完整恢复验证
- 性能测试(接续耗时<3秒)
十二、总结
应用接续是HarmonyOS提供的强大跨设备协同能力,通过合理使用onContinue()、onCreate()、onNewWant()和onWindowStageRestore()等核心接口,可实现应用在不同设备间的无缝流转。
关键要点:
- 配置
continuable: true启用接续能力 - 在
onContinue()中保存数据,控制在100KB以内 - 在
onCreate()/onNewWant()中恢复数据 - 在
onWindowStageRestore()中恢复页面和UI状态 - 注意接续时不调用
onWindowStageCreate() - 使用分布式数据对象处理大数据同步
- 做好版本兼容性检测
- 充分测试各种启动模式和场景
遵循以上指导,即可为用户打造流畅的跨设备协同体验。