在鸿蒙应用开发中,冷启动慢是不少开发者的心头病。尤其是功能复杂的App,各种SDK、数据库、配置中心、推送服务的初始化代码全堆在UIAbility的onCreate里,串行执行导致主线程阻塞,用户盯着白屏好几秒。更头疼的是,这些初始化任务之间存在依赖关系——比如推送SDK需要先拿到用户Token,Token又依赖登录模块,登录模块得等网络库就绪。结果代码写成一大坨回调嵌套,维护起来像俄罗斯套娃。
鸿蒙提供了AppStartup启动框架,专门解决这个问题。它允许你把每个初始化功能抽成独立的StartupTask,在配置文件中声明依赖关系,无依赖的任务可以并行执行(跑在taskPool线程池里),有依赖的按顺序来。这样就从“一个个排队”变成了“能并行的并行,有依赖的按序执行”。下面结合我的实际改造成本,聊聊具体用法和踩过的坑。
== 开启AppStartup ==
首先要在module.json5里声明配置文件的路径。在resources/base/profile目录下新建startup_config.json(文件名随便取),结构如下:
- { "startupTasks": [], "appPreloadHintStartupTasks": [], "configEntry": "./ets/startup/StartupConfig.ets" }
复制代码
然后在module.json5的module字段里加上appStartup引用:
- { "module": { "name": "entry", "type": "entry", "appStartup": "$profile:startup_config", "srcEntry": "./ets/entryability/EntryAbility.ets", ... } }
复制代码
注意appStartup字段全小写,文档早期版本有写作appStartUp(大写U)的,但实际编译要求全小写,否则报错。
== 编写启动任务 ==
每个启动任务必须实现StartupTask接口,并且实现类上必须加@Sendable注解,因为接口用了@Sendable协议。关键方法有两个:
- @Sendable export default class StartupTask_Logger extends StartupTask {
- constructor() { super(); }
- async init(context: common.AbilityStageContext): Promise<string> {
- // 初始化逻辑
- return 'StartupTask_Logger done';
- }
- onDependencyCompleted(dependence: string, result: Object): void {
- // 依赖完成回调
- }
- }
复制代码
init方法是任务的核心,等所有前置依赖跑完后才会调用。它的返回值(字符串)可以通过onDependencyCompleted的result参数传给下游任务。注意result是Object类型,下游使用时需要解析。
== 配置任务依赖关系 ==
在startup_config.json的startupTasks数组里逐个配置任务。以五个典型任务为例:Logger(无依赖)、Database(无依赖)、Network(依赖Logger)、ConfigCenter(依赖Database和Network)、PushService(依赖ConfigCenter)。配置如下:
- { "startupTasks": [
- { "name": "Logger", "srcEntry": "./ets/startup/StartupTask_Logger.ets", "runOnThread": "taskPool", "waitOnMainThread": false },
- { "name": "Database", "srcEntry": "./ets/startup/StartupTask_Database.ets", "runOnThread": "taskPool", "waitOnMainThread": false },
- { "name": "Network", "srcEntry": "./ets/startup/StartupTask_Network.ets", "dependencies": ["Logger"], "runOnThread": "taskPool", "waitOnMainThread": false },
- { "name": "ConfigCenter", "srcEntry": "./ets/startup/StartupTask_ConfigCenter.ets", "dependencies": ["Database", "Network"], "runOnThread": "taskPool", "waitOnMainThread": false },
- { "name": "PushService", "srcEntry": "./ets/startup/StartupTask_PushService.ets", "dependencies": ["ConfigCenter"], "runOnThread": "mainThread", "waitOnMainThread": true, "excludeFromAutoStart": true }
- ], "configEntry": "./ets/startup/StartupConfig.ets" }
复制代码
关键字段说明:
- runOnThread: mainThread或taskPool。taskPool的任务可并行执行,mainThread的任务在主线程跑,适合必须在主线程做的操作。
- waitOnMainThread: 当runOnThread为taskPool时有效。true表示主线程要等这个任务跑完才渲染首页;false表示主线程不等,页面直接渲染。如果首页强依赖某个能力,必须设为true,否则可能因数据未就绪导致crash。
- excludeFromAutoStart: true表示该任务不走自动模式,需要手动调用startupManager.run()来执行。适用于非一级启动就要初始化的功能。
== 设置启动参数与监听 ==
通过StartupConfigEntry配置超时和监听器。在ets/startup/StartupConfig.ets中编写:
- export default class MyStartupConfigEntry extends StartupConfigEntry {
- onConfig(): StartupConfig {
- let onCompletedCallback = (error) => {
- if (error) { /* 超时或出错处理 */ }
- else { /* 全部任务完成 */ }
- };
- return { 'timeoutMs': 15000, 'startupListener': { 'onCompleted': onCompletedCallback } };
- }
- }
复制代码
timeoutMs初始建议设大一点(15~30秒),观察一段时间后再调优。startupListener用于在全部任务执行完毕后做后续操作,比如上报启动耗时。
== 手动模式启动 ==
对于excludeFromAutoStart为true的任务,需要在合适的时机手动调用:
- import { startupManager } from '@kit.AbilityKit';
- async function runManualTasks() {
- try {
- await startupManager.run();
- console.info('Manual startup tasks completed');
- } catch (err) { /* 处理错误 */ }
- }
复制代码
我通常在登录成功后调用,因为推送服务和用户会话在用户未登录前初始化了也没用。
== 预加载so任务 ==
如果应用用到native库(C++写的so文件),可以从API 18开始使用appPreloadHintStartupTasks预加载。配置示例:
- { "appPreloadHintStartupTasks": [
- { "name": "libcrypto", "srcEntry": "libcrypto.so", "dependencies": ["libssl"], "runOnThread": "taskPool" },
- { "name": "libssl", "srcEntry": "libssl.so", "runOnThread": "taskPool" }
- ] }
复制代码
注意:so预加载只能在taskPool线程跑,不支持系统级so,且不要在加载回调里跑业务逻辑。
== 进阶用法:条件匹配与调度阶段 ==
从API 20开始,可以给任务加matchRules,根据启动场景决定是否自动启动。例如从桌面卡片进入时只初始化卡片相关数据,不跑全部任务:
- { "name": "PushService", "matchRules": { "scenarios": ["default"] } }
复制代码
API 21引入了schedulerPhase,可控制任务在AbilityStage加载前还是加载后执行。preAbilityStageLoad能进一步挤出启动时间,适合不依赖AbilityStage的初始化:
- { "name": "Logger", "schedulerPhase": "preAbilityStageLoad" }
复制代码
== HSP/HAR中使用 ==
大型应用模块拆成HSP或HAR后,每个模块可以有自己的启动任务,但必须通过HAP里的自动任务拉起,不支持自动模式。需在HSP的startup_config.json中设置excludeFromAutoStart为true。
== 常见踩坑记录 ==
坑1:waitOnMainThread设置错误导致crash。刚用AppStartup时我把所有任务都设为waitOnMainThread:false,结果首页渲染后调用了网络库的数据,但网络库尚未初始化完,直接抛NetworkManager is not initialized。修复方法:把首页强依赖的任务设为waitOnMainThread:true,或单独拆出一个HomePageData任务并设为等待。
坑2:循环依赖。配置了A→B→C→A的依赖链,编译不报错,但启动时一个任务都不执行,日志全无。排查后去掉循环即正常。配置依赖前最好画个DAG图。
坑3:忘记加@Sendable注解。继承StartupTask的子类必须加@Sendable,否则编译报错。
坑4:异步线程里操作UI对象。在taskPool线程中直接操作全局UI对象(如globalCache.update)运行时报异常。正确的做法是把UI操作放到waitOnMainThread:true的任务里,或用消息机制抛到主线程。
坑5:超时时间设太短。初始设为3秒,模拟初始化时setTimeout设了5秒,结果频繁触发超时回调。建议先设15~30秒稳定后再调优。
== 总结建议 ==
AppStartup适合启动任务多、依赖关系复杂的场景。如果你的冷启动时间超过3秒,或代码中onCreate堆满初始化,值得一试。它让代码结构清晰,每个初始化能力独立成文件,依赖关系一目了然。但别忘了在module.json5里配置appStartup字段,我第一次就漏了这步,折腾半天没生效。改完后,别忘了在AbilityStage的onCreate里监听启动完成状态,配合AppStartup做全局初始化,体验更佳。 |