HarmonyOS 7 新特性实战(23):应用快启适配准备与字号持久化回读
先检查启动后的内容是否正确
阅读应用启动更快之后,如果字号仍然停留在上一次的值,用户感受到的就是设置失效。适配准备 Demo 把字号保存到本机,强制停止进程,再次启动时重新读取;同时接入快启开关与回调标记,分别观察接口调用和实际启动路径。
字号从 20sp 改为 28sp 后,重新启动读回 28sp;快启开关的开启与关闭调用均返回 16000150,未观察到快启回调。字号持久化与普通重启回读已经通过,快启恢复和加速效果没有可用结果。

用独立工程隔离快启准备
本实验只保留一个阅读页、一个字号仓库和一个 AbilityStage,不加载三维模型、音频播放器或网络模块。模块导入阶段没有读写磁盘,仓库构造时也没有读取配置。需要访问外部状态的部分放在页面显示后的流程中,让每次进入都取得当前值。
HyperSnap 属于 AbilityKit,基础接口从 API 24 起提供。实验运行在 API 26 工程中,用它为系列应用做启动适配准备;这项基础接口不属于 API 26 首次新增,也不属于 FAST 算法工具。参考官方应用快启指南。
记录回调,不从耗时猜快照命中
AbilityStage 的普通创建回调先写入未观察标记,只有 onLaunchFromHyperSnap 才写入已观察标记。页面通过 StorageLink 显示该值。当前标记是进程内观察,不是持久化计数,也没有推断系统是否已经生成可用快照。
import { AbilityStage } from '@kit.AbilityKit';
export default class QuickStage extends AbilityStage {
onCreate(): void {
AppStorage.setOrCreate<boolean>('hyperSnapObserved', false);
}
onLaunchFromHyperSnap(): void {
AppStorage.setOrCreate<boolean>('hyperSnapObserved', true);
}
}
把“未观察到”显示出来比自动标成“冷启动”更准确:同一进程中的页面往返、前后台切换和实际的新进程启动属于不同路径。这个标记只回答当前进程是否走过指定回调,不测首帧时间,也不覆盖系统的其他启动路径。
工程在模块配置中注册这个 AbilityStage;没有注册时,页面代码即使显示一个布尔值,也不能证明系统会调用对应方法。现在的标记接入已编译,真实快启回调仍须在满足条件的环境中触发后检查,不能在验收脚本里直接把布尔值设为 true。
字号只从存储读取,不在模块顶层缓存
仓库允许 20 和 28 两个字号。存储没有值或含有不支持的值时,回到 20;写入不接受范围外的值。保存先 put,再 await flush,调用方等待成功后才更新页面的保存反馈。这里没有自动删除收藏、账号或其他业务数据,实验使用自己的存储名称。
import { common } from '@kit.AbilityKit';
import { preferences } from '@kit.ArkData';
export class LaunchPreferences {
private store: preferences.Preferences | undefined = undefined;
async open(context: common.UIAbilityContext): Promise<void> {
this.store = await preferences.getPreferences(context, { name: 'quick_start_lab' });
}
async readFont(): Promise<number> {
if (!this.store) { throw new Error('存储未初始化'); }
const value = await this.store.get('fontSize', 20);
return value === 28 ? 28 : 20;
}
async saveFont(value: number): Promise<void> {
if (!this.store) { throw new Error('存储未初始化'); }
if (value !== 20 && value !== 28) { throw new Error('不支持的字号'); }
await this.store.put('fontSize', value);
await this.store.flush();
}
}
仓库本身不持有字号缓存,因此新实例会再次从存储读取。不过 Preferences 对象可能有进程内缓存,不能把同一进程里连续 get 等同于物理磁盘重新加载。本次额外执行了强制停止进程和重新启动,验证的是新进程回读路径。
如果 flush 失败,调用方收到错误,不显示保存成功。put 已经改变的底层缓存是否回滚,不由这个仓库保证;遇到保存错误应提示用户,而不能通过静默读回缓存值把失败改写成成功。替身测试检查了错误向上传递,未模拟文件系统断电或存储损坏。
页面把读取、保存与开关结果分别显示
页面依赖前面的仓库与 AbilityKit,以下是完整组件体,省略工程内部相对导入路径。onPageShow 触发回读,ready 在读取完成后才允许修改。revision 拦截晚到的读取结果,避免离页后的旧读取覆盖当前状态;busy 用于防止同页连续保存。
@Entry
@Component
struct Index {
@StorageLink('hyperSnapObserved') observed: boolean = false;
@State savedFont: number = 20;
@State ready: boolean = false;
@State busy: boolean = false;
@State feedback: string = '正在读取本机字号';
@State switchResult: string = '尚未请求快启开关';
private repo: LaunchPreferences = new LaunchPreferences();
private active: boolean = true;
private revision: number = 0;
aboutToAppear(): void { this.active = true; }
onPageShow(): void { this.reload(); }
aboutToDisappear(): void { this.active = false; this.revision++; }
private async reload(): Promise<void> {
const revision = ++this.revision;
this.ready = false;
try {
await this.repo.open(this.getUIContext().getHostContext() as common.UIAbilityContext);
const value = await this.repo.readFont();
if (!this.active || revision !== this.revision) { return; }
this.savedFont = value; this.ready = true; this.feedback = `本次读取完成:${value}sp`;
} catch (error) { if (this.active && revision === this.revision) { this.feedback = `读取失败:${(error as Error).message}`; } }
}
private async save(): Promise<void> {
if (!this.ready || this.busy) { return; }
this.busy = true;
const value = this.savedFont === 20 ? 28 : 20;
try {
await this.repo.saveFont(value);
if (this.active) { this.savedFont = value; this.feedback = `已持久化:${value}sp`; }
} catch (error) { if (this.active) { this.feedback = `保存失败:${(error as Error).message}`; } }
finally { if (this.active) { this.busy = false; } }
}
private configure(enabled: boolean): void {
try {
hyperSnapManager.setHyperSnapEnabled(enabled);
this.switchResult = `setHyperSnapEnabled(${enabled}) 已返回`;
} catch (error) { this.switchResult = `开关失败:${JSON.stringify(error)}`; }
}
build() {
Column({ space: 18 }) {
Text('18 · 快启与字号回读').fontSize(24).fontWeight(FontWeight.Bold)
Text(this.observed ? '本进程观察到快启回调' : '本进程未观察到快启回调').fontSize(17).id('launchObserved')
Text('开关调用返回,不代表系统生成或使用快照。').fontSize(16)
Button('请求开启快启').id('enableHyper').onClick(() => { this.configure(true); })
Button('请求关闭快启').id('disableHyper').onClick(() => { this.configure(false); })
Text(this.switchResult).fontSize(16).id('hyperResult')
Divider()
Text('旋龟:其状如龟而鸟首虺尾').fontSize(this.savedFont).width('100%')
Text(`当前字号:${this.savedFont}sp`).fontSize(17).id('fontValue')
Button('切换字号并保存').id('saveFont').enabled(this.ready && !this.busy).onClick(() => { this.save(); })
Button('重新读取存储').enabled(!this.busy).onClick(() => { this.reload(); })
Text(this.feedback).fontSize(16).id('launchFeedback')
Text('重启应用后核对字号;快启命中与耗时需要独立证据。').fontSize(16)
}.padding(24).width('100%').height('100%').backgroundColor('#F3F0E8')
}
}
快启开关的返回信息单独显示,字号保存信息也单独显示。即使快启调用失败,普通阅读设置仍可保存与回读。当前组件没有自动重建快照,也没有在每次显示页面时重复调用开关,避免让系统请求与普通业务读取相互干扰。

按实际执行记录验收
| 操作 | 观察结果 | 验证范围 |
|---|---|---|
| 首次进入 | 当前字号 20sp,未观察到快启回调 | 默认状态与读取完成反馈 |
| 点击切换并保存 | 字号 28sp,显示已持久化 | 页面更新和存储写入路径 |
| 强制停止后重新启动 | 读回 28sp | 新进程持久化回读 |
| 请求开启快启 | 16000150 | 错误显示路径,未开启成功 |
| 请求关闭快启 | 16000150 | 错误显示路径,未确认系统开关状态 |
当前使用未签名 HAP 安装到 API 26 模拟器。官方指南对开发调试说明了 debug 签名条件,发布环境还有独立接入要求,所以当前错误不足以证明“模拟器缺少快启”。现有记录没有符合本应用的 debug 签名验证或系统侧详细原因日志;复验时需要同时确认签名条件和服务状态。
本地 SDK 将 16000150 描述为向系统服务发送请求失败。它不是“没有快照”的查询结果,也不是系统能力缺失的专用错误。因此页面保留原始错误,不把它翻译成“设备不支持”。请求失败后,同样不能宣称已经成功关闭或恢复了某个系统设置。
三类数据采用不同的恢复策略
| 数据类型 | 示例 | 恢复时的处理 |
|---|---|---|
| 固定内容 | 分类常量、静态文案 | 在无外部依赖的前提下复用 |
| 可变业务数据 | 字号、收藏、下载状态 | 从当前持久化状态重新读取 |
| 外部资源及授权 | 网络连接、文件句柄、登录权限 | 按当前环境重新确认有效性 |
这份 Demo 只验证第二类中的字号,不包括账号过期、数据库迁移或 GPU 句柄恢复。真正接入应用时,要沿启动入口检查所有模块顶层代码、静态初始化和 Native 构造函数,避免看似只创建一个对象,实际却在构造期间读取了外部状态。
当前回调标记与业务读取路径是两条独立证据:回调证明启动路径,数据回读证明业务状态。只有两条都取得实际证据,才可以进一步比较开启和关闭条件下的表现。字体回读通过不能推断快启回调已发生,调用开关成功也不能推断下次必定使用快照。
准备性能实验之前还缺什么
先在符合 debug 签名条件的包上确认开关调用与真实回调,再采集启动请求、首帧、可操作时刻和内容就绪时刻。用同一个版本、同一数据集、多次交错运行做对照,保留普通启动、实际快启和路径未知三组样本。
目前没有这些分组数据,无法计算毫秒级收益或百分比。系统生成和使用快照由自身策略决定,不能通过等待固定秒数或重启一次来保证命中。后续即使首帧提前,也仍需核对刚修改的字号、失效条目和新的外部入口,确保优化后的内容符合当前用户意图。
更多推荐



所有评论(0)