先检查启动后的内容是否正确
阅读应用启动更快之后,如果字号仍然停留在上一次的值,用户感受到的就是设置失效。适配准备 Demo 把字号保存到本机,强制停止进程,再次启动时重新读取;同时接入快启开关与回调标记,分别观察接口调用和实际启动路径。
字号从 20sp 改为 28sp 后,重新启动读回 28sp;快启开关的开启与关闭调用均返回 16000150,未观察到快启回调。字号持久化与普通重启回读已经通过,快启恢复和加速效果没有可用结果。
用独立工程隔离快启准备
本实验只保留一个阅读页、一个字号仓库和一个 AbilityStage,不加载三维模型、音频播放器或网络模块。模块导入阶段没有读写磁盘,仓库构造时也没有读取配置。需要访问外部状态的部分放在页面显示后的流程中,让每次进入都取得当前值。
HyperSnap 属于 AbilityKit,基础接口从 API 24 起提供。实验运行在 API 26 工程中,用它为系列应用做启动适配准备;这项基础接口不属于 API 26 首次新增,也不属于 FAST 算法工具。参考官方应用快启指南。
记录回调,不从耗时猜快照命中
AbilityStage 的普通创建回调先写入未观察标记,只有 onLaunchFromHyperSnap 才写入已观察标记。页面通过 StorageLink 显示该值。当前标记是进程内观察,不是持久化计数,也没有推断系统是否已经生成可用快照。
import{AbilityStage}from'@kit.AbilityKit';exportdefaultclassQuickStageextendsAbilityStage{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';exportclassLaunchPreferences{privatestore:preferences.Preferences|undefined=undefined;asyncopen(context:common.UIAbilityContext):Promise<void>{this.store=awaitpreferences.getPreferences(context,{name:'quick_start_lab'});}asyncreadFont():Promise<number>{if(!this.store){thrownewError('存储未初始化');}constvalue=awaitthis.store.get('fontSize',20);returnvalue===28?28:20;}asyncsaveFont(value:number):Promise<void>{if(!this.store){thrownewError('存储未初始化');}if(value!==20&&value!==28){thrownewError('不支持的字号');}awaitthis.store.put('fontSize',value);awaitthis.store.flush();}}仓库本身不持有字号缓存,因此新实例会再次从存储读取。不过 Preferences 对象可能有进程内缓存,不能把同一进程里连续 get 等同于物理磁盘重新加载。本次额外执行了强制停止进程和重新启动,验证的是新进程回读路径。
如果 flush 失败,调用方收到错误,不显示保存成功。put 已经改变的底层缓存是否回滚,不由这个仓库保证;遇到保存错误应提示用户,而不能通过静默读回缓存值把失败改写成成功。替身测试检查了错误向上传递,未模拟文件系统断电或存储损坏。
页面把读取、保存与开关结果分别显示
页面依赖前面的仓库与 AbilityKit,以下是完整组件体,省略工程内部相对导入路径。onPageShow 触发回读,ready 在读取完成后才允许修改。revision 拦截晚到的读取结果,避免离页后的旧读取覆盖当前状态;busy 用于防止同页连续保存。
@Entry@Componentstruct Index{@StorageLink('hyperSnapObserved')observed:boolean=false;@StatesavedFont:number=20;@Stateready:boolean=false;@Statebusy:boolean=false;@Statefeedback:string='正在读取本机字号';@StateswitchResult:string='尚未请求快启开关';privaterepo:LaunchPreferences=newLaunchPreferences();privateactive:boolean=true;privaterevision:number=0;aboutToAppear():void{this.active=true;}onPageShow():void{this.reload();}aboutToDisappear():void{this.active=false;this.revision++;}privateasyncreload():Promise<void>{constrevision=++this.revision;this.ready=false;try{awaitthis.repo.open(this.getUIContext().getHostContext()ascommon.UIAbilityContext);constvalue=awaitthis.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=`读取失败:${(errorasError).message}`;}}}privateasyncsave():Promise<void>{if(!this.ready||this.busy){return;}this.busy=true;constvalue=this.savedFont===20?28:20;try{awaitthis.repo.saveFont(value);if(this.active){this.savedFont=value;this.feedback=`已持久化:${value}sp`;}}catch(error){if(this.active){this.feedback=`保存失败:${(errorasError).message}`;}}finally{if(this.active){this.busy=false;}}}privateconfigure(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 签名条件的包上确认开关调用与真实回调,再采集启动请求、首帧、可操作时刻和内容就绪时刻。用同一个版本、同一数据集、多次交错运行做对照,保留普通启动、实际快启和路径未知三组样本。
目前没有这些分组数据,无法计算毫秒级收益或百分比。系统生成和使用快照由自身策略决定,不能通过等待固定秒数或重启一次来保证命中。后续即使首帧提前,也仍需核对刚修改的字号、失效条目和新的外部入口,确保优化后的内容符合当前用户意图。