一个应用,冷启动时首页偶尔把远端开关当成 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 看一眼,那个异步方法,到底有没有人真的在等它。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐