随手备忘录手记 01|HarmonyOS 7.0新特性实验:启动前的必要准备【共创稿事节】【鸿蒙心迹】

1. 前言
站在玄关,鞋已穿好,一只手拎着包,另一只手打开手机。我想看一眼通勤清单:钥匙、工牌、雨伞,哪些已经勾选,哪些还没有确认。
这时候,一个很快出现的页面未必能帮上忙。如果标题和按钮已经显示,中间却暂时没有物品,我还得判断:清单本来就是空的,还是应用尚未读完?页面把自己的准备过程变成了用户需要理解的问题。
“随手备忘录”围绕这个动作组织功能。它是一个手机Stage应用,只有一个entry模块和一个主UIAbility,提供通勤、运动两个本地模板,支持勾选、保存、主题偏好,以及按需打开的静态物品图鉴。这里先固定通勤模板,沿用钥匙、工牌、雨伞这三项。
勾选状态以最近一次成功保存为准,不随日期自动重置。它记录用户的确认,也不意味着手机知道钥匙是否真的进了包。
讨论启动时,我会把三个时刻分开:页面框架出现、真实清单可读、用户第一次有效勾选。最后一个时刻要求操作落在真实清单上,而且界面确实响应了状态变化。空壳页面能够接受点击,并不足以满足这个条件。

本文关心的是第一个Ability创建之前的必要准备:如果首屏依赖异步读取,应该由谁等待读取完成?HarmonyOS 7.0的API26提供了一个明确入口。理解它的价值,需要先区分“发起工作”和“等待工作完成”。
2. 方案
一种自然的写法是在同步生命周期回调中发起读取,然后返回。读取继续进行,应用也继续创建界面。只要页面正确表达“准备中”,这可以成为合理的渐进展示方案。不过,后续生命周期并没有因此获得数据已经就绪的保证。
另一种方案是在首个Ability创建前等待必要读取完成,再继续创建界面。这样,页面接手时已经有清单可以消费。代价同样明确:这部分读取时间进入了创建Ability之前的等待路径。
我选择第二种方案作为本篇的主线,同时保留第一种作为对照。原因来自页面的用途:通勤清单很小,而用户打开它就是为了看清单。如果提前展示的只是标题和空白区域,它带来的价值相当有限。
这个选择不意味着所有初始化都应该提前完成。等待清单与等待整套物品图鉴,是两件不同的事。
| 工作 | 是否属于本篇必要准备 | 完成条件 |
|---|---|---|
| 取得当前模板标识 | 是 | 确定本次展示通勤模板 |
| 读取对应的保存状态 | 是 | 成功取得清单内容,形成可消费模型 |
| 加载静态物品图鉴 | 否 | 进入图鉴时再处理 |
| 准备图鉴装饰资源 | 否 | 随对应功能按需加载 |
“存储可读”容易引起误会。打开存储句柄,并不表示钥匙的勾选状态已经读到。这里的完成条件要一直延伸到首屏所需的数据已经就绪,否则只是把同一个竞态挪到了下一层。

两组对照必须使用相同数据源和页面实现,只改变等待方式。不能给一组读盘、给另一组读取预置内存,再把差异归因于生命周期接口。本篇也固定主题条件,环境同步留到后续讨论。
页面状态还要保留几个区别:首次没有保存数据、成功读取到空清单、读取失败,以及读取尚未完成。只有成功取得空数据,才有依据展示正常空清单。把这些情况都转换成空数组虽然方便,却会抹掉界面作出正确判断所需的信息。
显式等待首先解决顺序问题。它不会让磁盘自动变快,也不能单凭这一处修改宣称应用快启已经取得收益。
3. 实现
先把版本搞清楚。AbilityStage接口的关系如下:
| 能力或回调 | 起始版本 | 本文用途 |
|---|---|---|
| 应用快启基础机制 | API24 | 理解初始化复用的背景 |
| onAboutToCreateAbility | API24 | 同步发起异步工作的对照入口 |
| onLaunchFromHyperSnap | API24 | 快启路径的观察依据之一 |
| onAboutToCreateAbilityAsync | 26.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版本表、时序关系和实验记录表,分别回答等待什么、使用哪个入口、如何交接、怎样核对。它们会继续沿用于后面的文章。
不过,成功路径只回答了准备顺利完成时怎么办。文件读不到、记录损坏,或者任务迟迟没有返回时,用户仍需要一个能够继续操作的入口。下一篇就沿着同一个准备函数,讨论怎样结束等待,同时保留足够的信息,让页面诚实地表达失败。
更多推荐





所有评论(0)