页面起来了草稿是空的,接续的静默失败不在软总线上
一个很具体的场景。用户在手机笔记里写了半篇稿子,光标停在第三段中间,点了接续。平板上的应用确实被拉起来了,页面也跳到了编辑页,看起来一切正常。然后用户看了一眼屏幕:标题空的,正文空的,光标在开头。他以为是自己点错了,退回首页,重新点进那篇笔记,稿子还在——只是手机和平板之间那一段“现场”根本没搬过来。
你翻日志,onContinue 执行了,返回值是 AGREE,目标端 launchReason 也是 CONTINUATION。没有任何报错,hilog 干干净净。问题不是接续没触发,而是触发了,但数据在传递过程中被静默截断了,或者恢复的时机根本不对。接续的可靠性,不取决于你有没有写 onContinue,而取决于你有没有主动确认几个隐性契约:同步时序、双入口覆盖、数据体量、业务状态门控。
restoreWindowStage 挪进异步回调,页面恢复就静默失败了
目标端拿到数据之后,要把运行现场恢复到窗口里。这一步依赖 restoreWindowStage(),而它的约束写得很直白:必须在 onCreate 或 onNewWant 的同步执行流程中调用。官方文档的原话是,如果在异步回调中执行该接口,应用拉起时页面有概率加载失败。
一个很自然的错误写法是这样的:目标端 onCreate 里先发一个异步请求去拉数据,await 完成后再调 restoreWindowStage。代码能跑,编译不报错,但页面恢复会变得不可靠——有时候能起来,有时候停在白屏,而且没有任何异常抛出来告诉你“你调晚了”。
正确的顺序是把数据从 want.parameters 里取出来、暂存到 AppStorage 或 LocalStorage,然后立刻同步调用 restoreWindowStage。异步的数据补全放在页面就绪之后做,不要在恢复窗口之前插入 await。
typescript
// 错误:restoreWindowStage 被放进了异步回调
someAsyncFunc().then(() => {
this.context.restoreWindowStage(new LocalStorage());
});
// 正确:先同步恢复窗口,数据暂存,页面侧再消费
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason !== AbilityConstant.LaunchReason.CONTINUATION) {
return;
}
if (want.parameters !== undefined) {
AppStorage.setOrCreate('draftData', want.parameters['draftData'] as string);
}
this.context.restoreWindowStage(new LocalStorage()); // 同步,不要 await
}
AppStorage.setOrCreate 那一行也不是可选的。如果你只把数据从 want 里读出来存进一个普通变量,页面组件加载时可能还没来得及消费它。先落到应用级存储,页面 aboutToAppear 或 build 里去取,恢复链路才有一个稳定的交接点。
冷启动走 onCreate,热启动走 onNewWant,只写一处等于漏一半
目标端恢复数据的入口不止一个。如果应用在平板上还没有实例,系统走冷启动,恢复逻辑在 onCreate 里执行。如果应用已经以 singleton 模式在后台存在,接续触发的是热启动,走的是 onNewWant,onCreate 根本不会被调用。
很多人只在 onCreate 里写了恢复逻辑,调试的时候平板上应用是全新安装的,冷启动跑通了,就以为接续没问题。上线之后用户手机上用着、平板上也开着同一个应用,接续触发热启动,数据恢复那一段代码压根没执行,页面起来就是空的,日志里同样什么都没有。
两个入口都必须覆盖,而且不要复制两遍逻辑,抽一个公共方法:
typescript
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.handleContinuation(want, launchParam);
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.handleContinuation(want, launchParam);
}
private handleContinuation(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (launchParam.launchReason !== AbilityConstant.LaunchReason.CONTINUATION) {
return;
}
this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE, () => {});
if (want.parameters?.['draftData'] !== undefined) {
AppStorage.setOrCreate('draftData', want.parameters['draftData'] as string);
}
this.context.restoreWindowStage(new LocalStorage());
}
setMissionContinueState(ACTIVE) 那一行容易漏,但有用。源端在业务不稳定时会把接续状态置为 INACTIVE,目标端恢复时如果没把它拉回 ACTIVE,后续的接续入口会一直处于不可用状态。
wantParam 过了 100KB,系统不报错,直接丢
这是接续里最安静的一刀。onContinue 的 wantParam 通道有一条硬红线:传输的数据需要控制在 100KB 以下。超过之后,系统不会抛异常,不会回失败,它做的只是把数据丢掉。目标端页面正常拉起,你读 want.parameters 发现字段是 undefined,或者整个 parameters 都是空的。
一个表单草稿,如果只传 draftId 和光标位置,远远到不了 100KB。但如果你顺手把完整正文、甚至图片 base64 一起塞进去,序列化之后轻松越线。更麻烦的是,越线的表现和“源端压根没写”长得一模一样,排查方向很容易被带到“是不是软总线没通”上去。
wantParam 只放轻量现场:页面路径、条目 ID、滚动偏移、短文本、光标位置。正文、图片这类大块数据走分布式数据对象或分布式数据库,wantParam 里只带一个 sessionId 标识。源端在 onContinue 里把数据对象的 save 完成之后再返回 AGREE,目标端收到 restored 事件后再去消费正文。
在 onContinue 里加一行体量检查,比上线之后靠用户反馈来定位要省事得多:
typescript
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
const draft = AppStorage.get<string>('draftData') ?? '';
hilog.info(0x0000, 'Continue', 'draft bytes: %{public}d', draft.length);
if (draft.length > 80 * 1024) { // 留出余量
// 走分布式数据对象,只传 sessionId
wantParam['sessionId'] = this.createDistributedObject(draft);
return AbilityConstant.OnContinueResult.AGREE;
}
wantParam['draftData'] = draft;
return AbilityConstant.OnContinueResult.AGREE;
}
注意 onContinue 本身必须是同步的,而且要在 1 秒内返回。你可以在里面调 save() 这类异步接口,但不能 await 一个耗时操作再返回——超时或阻塞会直接影响接续能否触发。需要等待异步完成的场景,提前把状态准备好,onContinue 只负责同步打包和返回。
源端自己先变成了“不可接续”,接续图标压根不会亮
上面几条是“触发了但没过去”,还有一条是“压根没触发”。setMissionContinueState 控制的是当前任务是否对系统接续入口可见。页面正在加载、弹窗打开、文件上传中,业务状态本身不稳定,这时候如果系统发起接续,目标端恢复出来的现场大概率是错乱的。
规范做法是在这些不稳定阶段主动把状态置为 INACTIVE,等页面就绪、数据加载完成、弹窗关闭之后再拉回 ACTIVE。不是“设置一次就完事”,而是跟着业务状态走。
typescript
aboutToAppear(): void {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
ctx.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE);
this.loadDraft().finally(() => {
ctx.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE);
});
}
这个门控和前面的数据恢复是两件事。前者决定“能不能接”,后者决定“接过去之后对不对”。门控没做好,接续在错误的时刻被触发,数据传过去了,但用户看到的是一个尚未就绪的页面加上半截现场,体验比不接续更差。
还有一个排查时值得先确认的点:onRestoreData 是 FA 模型的回调,Stage 模型的 UIAbility 不会自动调用它。如果你照着旧资料在 onRestoreData 里写恢复逻辑,目标端永远收不到。Stage 模型认的是 onCreate / onNewWant 加 want.parameters,这一点对不上,后面所有代码都白写。
综上所述,接续的“静默”不是系统在故意藏错误,而是这条链路的设计把失败路径放在了你不一定会去看的地方:restoreWindowStage 挪进异步回调不报错,只是页面可能起不来;冷启动写了热启动没写不报错,只是数据不恢复;wantParam 超限不报错,只是数据被丢掉。把这四个契约主动确认一遍——同步恢复窗口、双入口覆盖、100KB 分流、不稳定状态门控——排查“页面起来了但草稿是空的”这类问题时,方向就不会被软总线带偏。下次再接续出问题,先在目标端打一行 Object.keys(want.parameters ?? {}),如果键在但值是空的,往体量上查;如果键压根不在,往源端 onContinue 的写入和热启动入口上查。
更多推荐


所有评论(0)