查看: 66|回复: 3

鸿蒙AppStartup实战:任务拆分与依赖配置避坑指南

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在鸿蒙应用开发中,冷启动慢是不少开发者的心头病。尤其是功能复杂的App,各种SDK、数据库、配置中心、推送服务的初始化代码全堆在UIAbility的onCreate里,串行执行导致主线程阻塞,用户盯着白屏好几秒。更头疼的是,这些初始化任务之间存在依赖关系——比如推送SDK需要先拿到用户Token,Token又依赖登录模块,登录模块得等网络库就绪。结果代码写成一大坨回调嵌套,维护起来像俄罗斯套娃。

鸿蒙提供了AppStartup启动框架,专门解决这个问题。它允许你把每个初始化功能抽成独立的StartupTask,在配置文件中声明依赖关系,无依赖的任务可以并行执行(跑在taskPool线程池里),有依赖的按顺序来。这样就从“一个个排队”变成了“能并行的并行,有依赖的按序执行”。下面结合我的实际改造成本,聊聊具体用法和踩过的坑。

== 开启AppStartup ==

首先要在module.json5里声明配置文件的路径。在resources/base/profile目录下新建startup_config.json(文件名随便取),结构如下:
  1. { "startupTasks": [], "appPreloadHintStartupTasks": [], "configEntry": "./ets/startup/StartupConfig.ets" }
复制代码

然后在module.json5的module字段里加上appStartup引用:
  1. { "module": { "name": "entry", "type": "entry", "appStartup": "$profile:startup_config", "srcEntry": "./ets/entryability/EntryAbility.ets", ... } }
复制代码

注意appStartup字段全小写,文档早期版本有写作appStartUp(大写U)的,但实际编译要求全小写,否则报错。

== 编写启动任务 ==

每个启动任务必须实现StartupTask接口,并且实现类上必须加@Sendable注解,因为接口用了@Sendable协议。关键方法有两个:
  1. @Sendable export default class StartupTask_Logger extends StartupTask {
  2.   constructor() { super(); }
  3.   async init(context: common.AbilityStageContext): Promise<string> {
  4.     // 初始化逻辑
  5.     return 'StartupTask_Logger done';
  6.   }
  7.   onDependencyCompleted(dependence: string, result: Object): void {
  8.     // 依赖完成回调
  9.   }
  10. }
复制代码

init方法是任务的核心,等所有前置依赖跑完后才会调用。它的返回值(字符串)可以通过onDependencyCompleted的result参数传给下游任务。注意result是Object类型,下游使用时需要解析。

== 配置任务依赖关系 ==

在startup_config.json的startupTasks数组里逐个配置任务。以五个典型任务为例:Logger(无依赖)、Database(无依赖)、Network(依赖Logger)、ConfigCenter(依赖Database和Network)、PushService(依赖ConfigCenter)。配置如下:
  1. { "startupTasks": [
  2.   { "name": "Logger", "srcEntry": "./ets/startup/StartupTask_Logger.ets", "runOnThread": "taskPool", "waitOnMainThread": false },
  3.   { "name": "Database", "srcEntry": "./ets/startup/StartupTask_Database.ets", "runOnThread": "taskPool", "waitOnMainThread": false },
  4.   { "name": "Network", "srcEntry": "./ets/startup/StartupTask_Network.ets", "dependencies": ["Logger"], "runOnThread": "taskPool", "waitOnMainThread": false },
  5.   { "name": "ConfigCenter", "srcEntry": "./ets/startup/StartupTask_ConfigCenter.ets", "dependencies": ["Database", "Network"], "runOnThread": "taskPool", "waitOnMainThread": false },
  6.   { "name": "PushService", "srcEntry": "./ets/startup/StartupTask_PushService.ets", "dependencies": ["ConfigCenter"], "runOnThread": "mainThread", "waitOnMainThread": true, "excludeFromAutoStart": true }
  7. ], "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中编写:
  1. export default class MyStartupConfigEntry extends StartupConfigEntry {
  2.   onConfig(): StartupConfig {
  3.     let onCompletedCallback = (error) => {
  4.       if (error) { /* 超时或出错处理 */ }
  5.       else { /* 全部任务完成 */ }
  6.     };
  7.     return { 'timeoutMs': 15000, 'startupListener': { 'onCompleted': onCompletedCallback } };
  8.   }
  9. }
复制代码

timeoutMs初始建议设大一点(15~30秒),观察一段时间后再调优。startupListener用于在全部任务执行完毕后做后续操作,比如上报启动耗时。

== 手动模式启动 ==

对于excludeFromAutoStart为true的任务,需要在合适的时机手动调用:
  1. import { startupManager } from '@kit.AbilityKit';
  2. async function runManualTasks() {
  3.   try {
  4.     await startupManager.run();
  5.     console.info('Manual startup tasks completed');
  6.   } catch (err) { /* 处理错误 */ }
  7. }
复制代码

我通常在登录成功后调用,因为推送服务和用户会话在用户未登录前初始化了也没用。

== 预加载so任务 ==

如果应用用到native库(C++写的so文件),可以从API 18开始使用appPreloadHintStartupTasks预加载。配置示例:
  1. { "appPreloadHintStartupTasks": [
  2.   { "name": "libcrypto", "srcEntry": "libcrypto.so", "dependencies": ["libssl"], "runOnThread": "taskPool" },
  3.   { "name": "libssl", "srcEntry": "libssl.so", "runOnThread": "taskPool" }
  4. ] }
复制代码

注意:so预加载只能在taskPool线程跑,不支持系统级so,且不要在加载回调里跑业务逻辑。

== 进阶用法:条件匹配与调度阶段 ==

从API 20开始,可以给任务加matchRules,根据启动场景决定是否自动启动。例如从桌面卡片进入时只初始化卡片相关数据,不跑全部任务:
  1. { "name": "PushService", "matchRules": { "scenarios": ["default"] } }
复制代码

API 21引入了schedulerPhase,可控制任务在AbilityStage加载前还是加载后执行。preAbilityStageLoad能进一步挤出启动时间,适合不依赖AbilityStage的初始化:
  1. { "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做全局初始化,体验更佳。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙AppStartup实战:任务拆分与依赖配置避坑指南

感谢楼主分享这么详细的鸿蒙AppStartup实战经验!最近正好在优化我们App的冷启动,每次看到白屏都好头疼。你提到的任务拆分和依赖配置真是说到点子上了,特别是那个`waitOnMainThread`和`runOnThread`的组合,我之前一直没搞明白什么场景该选`mainThread`——像你例子里PushService配置成主线程但又用了`excludeFromAutoStart`,是不是意味着它会在所有依赖完成后才在主线程执行,但不会阻塞首屏显示?还有`@Sendable`注解是必须的吗?我猜是为了支持并行线程安全,但写起来会不会限制某些单例类的处理?再请教下,如果某个任务初始化失败,框架会如何处理依赖链上的下游任务?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙AppStartup实战:任务拆分与依赖配置避坑指南

感谢楼主的详细分享!最近正好在重构一个冷启动巨慢的项目,看到这篇简直雪中送炭。之前自己硬撸回调嵌套确实痛苦,AppStartup这种声明式依赖配置的思路清晰多了。 有几个细节想请教下:`runOnThread` 设为 `taskPool` 时,如果某个任务本身依赖了主线程的UI操作(比如读取本地缓存时顺带更新了某个全局状态),会不会有线程安全问题?另外 `excludeFromAutoStart` 设为 `true` 的 `PushService` 你是在具体哪个时机手动触发的?是等主页完全加载后再调吗?期待后续也能聊聊那部分的设计思路。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙AppStartup实战:任务拆分与依赖配置避坑指南

非常感谢楼主的详细分享!之前一直被鸿蒙冷启动的白屏问题困扰,手动串行初始化确实让维护变得很痛苦。看到你提到的 `taskPool` 并行执行和 `onDependencyCompleted` 传值这两块,感觉思路一下就清晰了。 想请教一下:`waitOnMainThread` 设为 `true` 且 `excludeFromAutoStart` 设为 `true` 的 PushService 任务,是不是意味着它不会自动触发,需要手动调用启动接口?另外,如果某个任务在 `init` 里抛了异常,框架是会中断整个启动流程还是只跳过该任务?我在实验时发现日志偶尔会丢失,不知道和异常处理有没有关系。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-23 16:48 , Processed in 0.032112 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部