请添加图片描述

1. 前言

站在玄关,鞋已穿好,一只手拎着包,另一只手打开手机。我想看一眼通勤清单:钥匙、工牌、雨伞,哪些已经勾选,哪些还没有确认。

这时候,一个很快出现的页面未必能帮上忙。如果标题和按钮已经显示,中间却暂时没有物品,我还得判断:清单本来就是空的,还是应用尚未读完?页面把自己的准备过程变成了用户需要理解的问题。

“随手备忘录”围绕这个动作组织功能。它是一个手机Stage应用,只有一个entry模块和一个主UIAbility,提供通勤、运动两个本地模板,支持勾选、保存、主题偏好,以及按需打开的静态物品图鉴。这里先固定通勤模板,沿用钥匙、工牌、雨伞这三项。

勾选状态以最近一次成功保存为准,不随日期自动重置。它记录用户的确认,也不意味着手机知道钥匙是否真的进了包。

讨论启动时,我会把三个时刻分开:页面框架出现、真实清单可读、用户第一次有效勾选。最后一个时刻要求操作落在真实清单上,而且界面确实响应了状态变化。空壳页面能够接受点击,并不足以满足这个条件。

请添加图片描述

本文关心的是第一个Ability创建之前的必要准备:如果首屏依赖异步读取,应该由谁等待读取完成?HarmonyOS 7.0的API26提供了一个明确入口。理解它的价值,需要先区分“发起工作”和“等待工作完成”。

2. 方案

一种自然的写法是在同步生命周期回调中发起读取,然后返回。读取继续进行,应用也继续创建界面。只要页面正确表达“准备中”,这可以成为合理的渐进展示方案。不过,后续生命周期并没有因此获得数据已经就绪的保证。

另一种方案是在首个Ability创建前等待必要读取完成,再继续创建界面。这样,页面接手时已经有清单可以消费。代价同样明确:这部分读取时间进入了创建Ability之前的等待路径。

我选择第二种方案作为本篇的主线,同时保留第一种作为对照。原因来自页面的用途:通勤清单很小,而用户打开它就是为了看清单。如果提前展示的只是标题和空白区域,它带来的价值相当有限。

这个选择不意味着所有初始化都应该提前完成。等待清单与等待整套物品图鉴,是两件不同的事。

工作是否属于本篇必要准备完成条件
取得当前模板标识是确定本次展示通勤模板
读取对应的保存状态是成功取得清单内容,形成可消费模型
加载静态物品图鉴否进入图鉴时再处理
准备图鉴装饰资源否随对应功能按需加载

“存储可读”容易引起误会。打开存储句柄,并不表示钥匙的勾选状态已经读到。这里的完成条件要一直延伸到首屏所需的数据已经就绪,否则只是把同一个竞态挪到了下一层。

请添加图片描述

两组对照必须使用相同数据源和页面实现,只改变等待方式。不能给一组读盘、给另一组读取预置内存,再把差异归因于生命周期接口。本篇也固定主题条件,环境同步留到后续讨论。

页面状态还要保留几个区别:首次没有保存数据、成功读取到空清单、读取失败,以及读取尚未完成。只有成功取得空数据,才有依据展示正常空清单。把这些情况都转换成空数组虽然方便,却会抹掉界面作出正确判断所需的信息。

显式等待首先解决顺序问题。它不会让磁盘自动变快,也不能单凭这一处修改宣称应用快启已经取得收益。

3. 实现

先把版本搞清楚。AbilityStage接口的关系如下:

能力或回调起始版本本文用途
应用快启基础机制API24理解初始化复用的背景
onAboutToCreateAbilityAPI24同步发起异步工作的对照入口
onLaunchFromHyperSnapAPI24快启路径的观察依据之一
onAboutToCreateAbilityAsync26.0.0在首个Ability创建前显式等待

AbilityStage管理Module级状态,主UIAbility承担界面实例的职责。这里讨论的是首个Ability创建前的准备,不能推导为每次应用回到前台都会重新执行。

新的回调返回Promise<void>。官方契约是:返回的Promise成功resolve后,后续生命周期回调才会继续执行;否则将被挂起。关键不在于函数名带有Async,而在于框架明确接纳了这个Promise作为继续执行的条件。

普通同步回调里也能发起异步工作,但两者没有相同的等待契约。尤其不能把AbilityStage.onCreate()改写成返回Promise,就假定系统会等它。

我倾向于先把准备过程放进一个普通函数,再决定由哪个生命周期入口调用它。这样,业务读取不会散落在AbilityStage、UIAbility和页面三个地方。

下面先定义应用内部的读取边界。ChecklistReader封装所选存储方案,undefined表示首次没有保存数据;读取失败通过拒绝Promise表达,成功读到空清单则保留空数组。

interface ChecklistItem {
  id: string;
  label: string;
  checked: boolean;
}

interface SavedChecklist {
  templateId: string;
  items: ChecklistItem[];
}

interface ChecklistReader {
  readCurrentTemplateId(): Promise<string>;
  readChecklist(templateId: string):
    Promise<SavedChecklist | undefined>;
}

interface ReadyModel {
  templateId: string;
  items: ChecklistItem[];
}

这里没有选择具体文件格式,也没有把业务读取接口包装成SDK接口。存储适配器负责读取和校验记录;准备函数只依赖这个边界。成功路径使用已保存的通勤清单,首次使用与恢复流程另行处理。

class StartupState {
  private model: ReadyModel | undefined = undefined;

  clear(): void {
    this.model = undefined;
  }

  publish(model: ReadyModel): void {
    this.model = model;
  }

  current(): ReadyModel | undefined {
    return this.model;
  }
}

async function prepareStartupData(
  reader: ChecklistReader,
  state: StartupState
): Promise<void> {
  state.clear();
  console.info('prepare.begin');

  const templateId = await reader.readCurrentTemplateId();
  const saved = await reader.readChecklist(templateId);
  if (saved === undefined) {
    throw new Error('First-use flow required');
  }
  if (saved.templateId !== templateId) {
    throw new Error('Template mismatch');
  }

  state.publish({ templateId, items: saved.items });
  console.info('prepare.ready');
}

这段成功路径刻意保留了失败信号。首次无数据不该变成“成功读取到零个物品”,模板不匹配也不能悄悄继续。抛出错误尚未提供恢复体验;下一篇会把这些结果转为明确的页面状态。

发布发生在两次读取完成之后。这样,消费者不会拿到“模板标识已更新、物品仍属于上一次读取”的半成品。clear()同时使本轮准备期间的状态保持未就绪。这里要求一次准备只有一个调用者,不在页面进入时重复调用;重试与并发提交的规则将在失败处理时补齐。

StartupState是应用自己的交接容器,并非响应式UI框架。同步对照路径需要在发布后通知页面更新;主方案则在准备完成后创建页面,并由页面建立后续交互状态。两条路径应共用同一套绑定逻辑。

请添加图片描述

将读取器和状态容器组装为同一个startup服务后,两个生命周期入口只剩下很薄的一层。下面两个方法分别放进对照构建的AbilityStage子类中。

// 方案A:只实现同步版本。
onAboutToCreateAbility(): void {
  console.info('sync.enter');
  startup.prepare().catch((error: Error) => {
    console.error(`prepare.failed: ${error.message}`);
  });
  console.info('sync.returning');
}

这里的catch只是记录失败,不是恢复策略。同步方法返回时,读取可能尚未完成,因此页面必须能够表达准备中,不能立即断言模型存在。

// 方案B:只实现异步版本。
async onAboutToCreateAbilityAsync(): Promise<void> {
  console.info('async.enter');
  await startup.prepare();
  console.info('async.returning');
}

startup.prepare()委托前面的prepareStartupData(reader, state)。异步回调本身不返回清单;它返回的是等待信号,清单通过共享的应用状态交接。先发布模型,再让回调成功完成,两件事缺一不可。

还有一个容易遗漏的规则:同时实现同步和异步版本时,只会生效异步版本。因此,不能把必要工作拆成“同步版本做一半、异步版本做另一半”,也不能把双实现当作自动回退方案。

请添加图片描述

这张时序图只需要表达已确定的等待关系。它不必把所有快启回调拼成一条精确时间线。应用快启的初始化复用与本篇异步入口如何衔接,应由对应路径的日志补证;开关设置成功也不等于本次启动已经命中快启。

4. 检验

只读代码,很容易相信await已经放在正确位置。但真正需要检验的是:等待是否覆盖了整个准备过程,以及页面有没有消费正确的数据。

先固定输入:通勤模板中,钥匙已勾选,工牌和雨伞未勾选。每轮开始前恢复这份已保存的数据;进入页面后勾选工牌,检查界面响应。下一轮仍恢复原始输入,避免上一次操作改变对照条件。

设备、SDK、系统版本、签名、构建类型和日志配置保持一致,交替运行两种入口。操作必须触发本篇研究的生命周期,不能将一次回前台混入重新创建Ability的样本。

在准备函数之外,增加入口触发、UIAbility.onCreate进入、页面绑定和首次有效勾选的事件。每轮附上实验ID,使日志、输入文件和录屏能够对应。UIAbility.onCreate是可观测检查点,不应被写成实例创建的精确瞬间。

成功路径需要检查的顺序可以表示为:

async.enter
  → prepare.begin
  → prepare.ready
  → async.returning
  → UIAbility.onCreate

这是验收所需的顺序关系,并非采集结果。async.returning记录的是函数即将完成的位置,Promise的实际settlement由运行时完成。页面绑定和真实上屏也要分开,不能因为一条日志出现就判定清单已经可读。

除了成功路径,还需要两个刻意收窄范围的实验。

实验操作需要核对的契约
成功resolve完成读取并发布模型后续生命周期继续时数据已就绪
受控未完成返回保持pending的Promise观察窗口内后续生命周期是否仍被挂起
同步异步双实现分别记录两个回调入口只触发异步版本,准备工作只执行一次

受控未完成的版本必须隔离。它可以使用下面的回调,观察结束后主动停止该构建,不能留在日常入口中:

async onAboutToCreateAbilityAsync(): Promise<void> {
  console.info('pending.enter');
  await new Promise<void>(() => {});
}

这个实验揭示了等待契约的另一面:没有成功完成,后面就不能按成功路径继续。不能期待系统在某个固定秒数后自动放行,也不能把AppStartup的超时配置移植成这里的系统保证。

请添加图片描述

同步方案也可能每次都很快读到数据,没有肉眼可见的空壳。这种结果应当保留。它说明当前条件下问题没有显现,不能证明同步回调获得了等待语义。若需要拉开观察窗口,可以单独加入受控延时;它验证顺序,不参与启动收益比较。

日志负责解释执行路径,录屏负责核对屏幕内容。两种时间未经对齐不能直接相减;首次有效勾选还包含人的反应时间。每轮记录应保留环境、输入、步骤、预期、实际、视频帧率和证据位置,而不是只留一张“成功”的截图。

目前这里给出的是检验方法与判定条件,没有可据此报告的设备测量值。尤其是快启路径,需要命中依据才能归类;无法确认的样本保留未分类。生命周期顺序正确、清单内容正确和启动耗时改善,是三项分别需要证据的结论。

5. 结尾

首屏依赖什么,就应该为这些依赖建立清楚的完成条件。对通勤清单而言,取得当前模板和成功读取对应物品,构成了这次准备的边界;物品图鉴没有理由挡在门口。

API26的异步准备回调让这个边界能够被生命周期识别。业务函数完成读取并发布模型,回调等待它成功完成,UIAbility随后消费结果。我们由此得到一份可以检验的契约,而不必依靠“读取通常很快”维持正确性。

前面的依赖清单、API版本表、时序关系和实验记录表,分别回答等待什么、使用哪个入口、如何交接、怎样核对。它们会继续沿用于后面的文章。

不过,成功路径只回答了准备顺利完成时怎么办。文件读不到、记录损坏,或者任务迟迟没有返回时,用户仍需要一个能够继续操作的入口。下一篇就沿着同一个准备函数,讨论怎样结束等待,同时保留足够的信息,让页面诚实地表达失败。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐