异步钩子写了但没跑,Ability创建前的准备位不是插槽
一个应用,冷启动时首页偶尔把远端开关当成 false,展示默认文案,几百毫秒之后又突然刷新成正确内容。日志里 AbilityStage.onCreate 跑了,UIAbility.onCreate 也跑了,但你在 AbilityStage 上新写的那个异步准备方法,一行日志都没有。没有报错,没有警告,hilog 干干净净,应用照常启动、照常白屏一帧、照常进首页。要不是你提前在方法体里埋了打点,这件事大概会被当成“接口还没通”放过去。
HarmonyOS 7(API 26)在 AbilityStage 上新增了 onAboutToCreateAbilityAsync,返回 Promise<void>,官方描述它“允许在首次创建 UIAbility 之前执行异步操作”。这个钩子的位置很诱人:它卡在 Ability 创建之前,正好适合放那种“首页第一帧就需要,但又不能同步阻塞”的初始化,比如读本地缓存、预热数据库连接、拉远端开关。但它的触发条件不是“你写了它就执行”,而是取决于你怎么声明它,以及同步版钩子是否同时存在。写错了,它就是一个没有日志的空方法。
异步钩子不是“写了就触发”,同步版的位置不能空着
AbilityStage 上有两个与 Ability 创建前准备相关的钩子:同步的 onAboutToCreateAbility(API 24 起)和异步的 onAboutToCreateAbilityAsync(API 26 起)。直觉上,有了异步版,只 override 异步版应该就够了,毕竟声明里写着“两者都 override 时只有异步版生效”。但实际行为不是这样。
一个经过验证的现场是:只 override 异步版,冷启动时 onAboutToCreateAbility 和 onAboutToCreateAbilityAsync 触发次数都是 0。同步版没跑,异步版也没跑,整条时间线只有 AbilityStage.onCreate 和 UIAbility.onCreate。你注释掉的同步版和只写异步版,在运行结果上是完全一样的。
异步钩子的触发依赖 AbilityStage 入口的正确注册,以及同步钩子的存在作为挂载点。只写异步版,框架没有找到可以承载这个异步准备位的同步锚点,它不会替你补一个。正确的结构是两个都 override,同步版返回 undefined 或者做最小同步检查,异步版承载真正的准备工作。
typescript
import { AbilityStage } from '@kit.AbilityKit';
export default class ProbeAbilityStage extends AbilityStage {
// 同步版不能省,它是异步版的挂载点
onAboutToCreateAbility(): void {
// 只做同步的、极轻量的检查
}
// 异步准备位:读缓存、预热连接
async onAboutToCreateAbilityAsync(): Promise<void> {
await this.bootstrap.prepare();
}
private bootstrap: StageBootstrap = new StageBootstrap();
}
同步版的方法体可以几乎是空的,但它必须存在。如果你在 module.json5 里没有注册 srcEntry 指向这个 AbilityStage 子类,两个钩子同样都不会执行,AbilityStage.onCreate 也不会走你自定义的逻辑。配置和声明是两道门,缺一道,异步钩子就只是一个写在文件里的普通方法。
初始化 Promise 没有成为 Ability 创建前的门槛,首页就会读到空配置
异步钩子能跑了,下一个问题不是“它有没有执行”,而是“它执行的结果有没有成为后续流程的等待条件”。
onAboutToCreateAbilityAsync 的语义是:框架拿到你返回的 Promise,等它兑现之后,再创建第一个 UIAbility。但如果你在这个钩子里启动了一个异步任务,却没有把它的 Promise 返回出去,或者返回了一个已经兑现的空 Promise,准备工作在跑,Ability 创建也在跑,两者并行。首页在 Ability 创建之后立刻读取配置,配置可能还没从缓存或远端回来,读到的是 undefined 或者默认值。几百毫秒后异步任务完成,数据写进去,页面才刷新成正确内容。
修正的核心是让共享的初始化 Promise 成为 Ability 创建前的明确门槛。不要只写一个 async 方法然后 await 一个内部任务就结束,要确保这个 Promise 被返回、被框架消费,并且多个 Ability 入口共享同一个 Promise,避免深链启动第二个 Ability 时重复拉配置。
typescript
class StageBootstrap {
private task?: Promise<BootstrapResult>;
prepare(): Promise<BootstrapResult> {
if (this.task) return this.task; // 多入口共享,只跑一次
this.task = this.doPrepare();
return this.task;
}
private async doPrepare(): Promise<BootstrapResult> {
try {
const remote = await this.fetchRemoteFlags(3000);
return { source: 'remote', flags: remote };
} catch (err) {
// 超时或失败:降级到本地快照,不无限阻塞
const cached = await this.readCachedFlags();
return cached
? { source: 'cache', flags: cached }
: { source: 'default', flags: DEFAULT_FLAGS };
}
}
}
if (this.task) return this.task 这一行解决的是“第二个 Ability 启动时重复请求”的问题。深链拉起另一个 UIAbility 时,AbilityStage 的异步准备位会再次被调用,如果每次都新建 Promise,远端配置会被拉两遍甚至多遍,两个 Ability 还可能拿到不同版本的结果。
没有超时和降级的异步准备,会把白屏换成一个更长的白屏
把初始化 Promise 返回给框架,意味着 Ability 创建会等它。如果这个 Promise 背后是一个没有超时的网络请求,弱网下 Promise 悬住,Ability 创建也跟着悬住,用户看到的白屏比同步初始化时更长,而且这次连 UIAbility.onCreate 的日志都不会出现。
异步准备位不是“异步了就可以不做超时”,恰恰因为它的结果直接阻塞 Ability 创建,超时和降级比同步场景更关键。Promise.race 或者 AbortController 让准备任务在一个可接受的窗口内结束,超时之后使用本地快照或默认值,把远端结果标记来源,等首屏渲染完成之后在后台更新,而不是让首屏去等一个不确定的远端。
typescript
private async doPrepare(): Promise<BootstrapResult> {
const timeout = new Promise<BootstrapResult>((resolve) => {
setTimeout(() => resolve({ source: 'default', flags: DEFAULT_FLAGS }), 800);
});
const remote = this.fetchRemoteFlags().then(
(flags): BootstrapResult => ({ source: 'remote', flags }),
() => ({ source: 'default', flags: DEFAULT_FLAGS })
);
return Promise.race([remote, timeout]);
}
800 毫秒不是精确值,但它是一个比“无限等”诚实的窗口。首屏用降级值先把结构渲染出来,远端结果回来之后,如果关键结构没有变化,只更新非结构性字段;如果关键结构变了,再走一次显式刷新,而不是让首页的开关状态在用户眼皮底下悄悄翻转。
准备位里只放“创建 Ability 之前必须完成的事”
onAboutToCreateAbilityAsync 的位置很靠前,靠前的位置最容易被当成“什么都能放”。网络 SDK 初始化、数据库建表、埋点启动、推送注册、日志上传,全部塞进去,Promise 的兑现时间被拉长,Ability 创建被推迟,首帧跟着推迟。
这个钩子应该只放那些第一个 Ability 创建之前必须就绪、且首屏结构依赖的最小集合。本地缓存的读取、首屏开关的恢复、数据库连接的最小可用打开,属于这个集合。远端配置的完整拉取、耗时迁移、非首屏模块预热、埋点上报,不属于。它们可以放在 AppStartup 的异步任务里,或者等首屏渲染完成之后再触发。
判断标准不是“这个任务重不重”,而是“如果它没有完成,第一个 Ability 还能不能创建、首屏还能不能渲染出一个正确的骨架”。能,就移出去;不能,才留下。留下的部分必须有超时和降级,移出去的部分必须有明确的触发时机和失败兜底。
排查顺序:先看钩子跑没跑,再看 Promise 有没有被门槛消费
下次异步准备位“写了没反应”或者“首页读到空配置”的时候,按这个顺序走,比反复改 doPrepare 里的逻辑快。
第一步,在 onAboutToCreateAbility 和 onAboutToCreateAbilityAsync 的方法体第一行各打一条带相对时间的日志,冷启动一次,看两条日志有没有出现。如果两条都没有,检查 module.json5 的 srcEntry 是否指向了正确的 AbilityStage 子类,以及同步版钩子是否被 override 了。只写异步版,两条都不会跑。
第二步,如果两条日志都出现了,但在 UIAbility.onCreate 里读取配置时拿到的还是空值,检查异步版的 Promise 有没有被 return 出去,StageBootstrap.prepare() 返回的是不是同一个共享 Promise,以及有没有在 prepare 内部提前 resolve 了一个空结果。
第三步,如果白屏时间比同步初始化还长,检查准备任务有没有超时和降级。Promise 悬住的时候,Ability 创建也在悬住,用户看到的是一个没有任何日志的白屏。
第四步,如果深链启动第二个 Ability 时配置被重复拉取,检查 StageBootstrap 的 task 字段是不是实例级共享的,AbilityStage 在同一个进程内应该只有一个实例,prepare 的 Promise 应该被复用而不是重新创建。
onAboutToCreateAbilityAsync 不是“异步版的 onCreate”,它是一个需要被框架消费的准备门槛。门槛要立起来,同步钩子得在,module.json5 得指对,返回的 Promise 得被框架等,超时之后得有降级。下一次首页把远端开关当成 false 又自己刷新的时候,先别怀疑远端接口不稳——打开 AbilityStage 看一眼,那个异步方法,到底有没有人真的在等它。

更多推荐

所有评论(0)