Administrator
发布于 2026-09-03 / 2,015 阅读
0
0

鸿蒙大型应用启动框架优化:AppStartup 编排与冷启动提速

系列文章《鸿蒙开发从入门到专家》第 51 篇 · 高级

第 8 篇讲性能优化时提到过启动速度,但那篇侧重的是"量化和定位"。这篇往深走一层:当你知道启动慢,具体怎么改?

HarmonyOS NEXT 从 API 12 开始提供了 AppStartup 启动框架,允许你用声明式的方式编排初始化任务,自动处理依赖关系,支持异步并行执行。用好了,冷启动能从 2 秒压到 800ms 以内。用不好,就是另一层"配置地狱"。

一、冷启动慢在哪

先搞清楚问题再动手。鸿蒙应用的冷启动链路:

用户点击图标
  → AbilityStage onCreate(进程创建)
    → UIAbility onCreate(Ability 初始化)
      → 页面首帧渲染(ArkUI build)
        → 页面可交互

大部分应用的初始化代码堆在 UIAbility.onCreate 里,串行执行:初始化日志 → 初始化网络 → 初始化数据库 → 初始化推送 → 初始化埋点 → 拉取用户信息 → 加载首屏数据。每一步 50-200ms,叠起来就是 1-2 秒。

问题在于:

  • 很多初始化任务之间没有依赖关系,却在串行等待。

  • 有些任务(如推送、埋点)不阻塞首屏渲染,却被放在了首屏渲染之前。

  • 有些任务有依赖关系(如网络初始化依赖日志),但代码顺序写反了导致出错。

AppStartup 就是来解决这个编排问题的。

二、AppStartup 基本用法

2.1 开启启动框架

在 module.json5 中配置:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "appStartup": "$profile:startup_config",
    "abilities": [...]
  }
}

然后在 resources/base/profile/ 下创建 startup_config.json:

{
  "startupTasks": [
    {
      "name": "StartupTask_Logger",
      "srcEntry": "./ets/startup/LoggerTask.ets",
      "runOnThread": "mainThread"
    },
    {
      "name": "StartupTask_Network",
      "srcEntry": "./ets/startup/NetworkTask.ets",
      "runOnThread": "taskPool",
      "dependencies": ["StartupTask_Logger"]
    },
    {
      "name": "StartupTask_Database",
      "srcEntry": "./ets/startup/DatabaseTask.ets",
      "runOnThread": "taskPool",
      "dependencies": ["StartupTask_Logger"]
    },
    {
      "name": "StartupTask_UserInfo",
      "srcEntry": "./ets/startup/UserInfoTask.ets",
      "runOnThread": "taskPool",
      "dependencies": ["StartupTask_Network", "StartupTask_Database"]
    },
    {
      "name": "StartupTask_Analytics",
      "srcEntry": "./ets/startup/AnalyticsTask.ets",
      "runOnThread": "taskPool"
    }
  ]
}

这个配置定义了 5 个启动任务,它们的依赖关系:

Logger(主线程)
  ├── Network(TaskPool,依赖 Logger)
  └── Database(TaskPool,依赖 Logger)
        └── UserInfo(TaskPool,依赖 Network + Database)

Analytics(TaskPool,无依赖)

框架会自动做拓扑排序,Logger 先执行,Network 和 Database 并行执行,UserInfo 等两者都完成后执行。Analytics 没有依赖,和 Logger 并行启动。

2.2 实现启动任务

每个任务继承 StartupTask,用 @Sendable 装饰器标注:

// startup/LoggerTask.ets
import { StartupTask, common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Sendable
export default class LoggerTask extends StartupTask {
  constructor() {
    super();
  }

  async init(context: common.AbilityStageContext): Promise<string> {
    // 初始化日志模块
    hilog.info(0x0000, 'AppStartup', 'LoggerTask init');
    // 模拟初始化耗时
    await new Promise<void>(resolve => setTimeout(resolve, 30));
    return 'LoggerTask done';
  }

  onDependencyCompleted(dependency: string, result: Object): void {
    hilog.info(0x0000, 'AppStartup',
      `LoggerTask dependency: %{public}s, result: %{public}s`,
      dependency, JSON.stringify(result));
  }
}
// startup/NetworkTask.ets
import { StartupTask, common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Sendable
export default class NetworkTask extends StartupTask {
  constructor() {
    super();
  }

  async init(context: common.AbilityStageContext): Promise<string> {
    hilog.info(0x0000, 'AppStartup', 'NetworkTask init');
    // 初始化 HTTP 客户端、配置拦截器等
    await new Promise<void>(resolve => setTimeout(resolve, 150));
    return 'NetworkTask done';
  }

  onDependencyCompleted(dependency: string, result: Object): void {
    hilog.info(0x0000, 'AppStartup',
      `NetworkTask sees dependency %{public}s completed`, dependency);
  }
}

2.3 手动触发启动

配置好之后,有两种触发方式。

方式一:框架自动触发。只要在 module.json5 配了 appStartup,AbilityStage 创建时框架会自动读取配置文件并执行。

方式二:手动触发。在 UIAbility.onCreate 中手动调用:

import { AbilityConstant, UIAbility, Want, startupManager } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'AppStartup', 'Ability onCreate');

    // 只执行阻塞首屏的任务
    let criticalTasks = ['StartupTask_Logger', 'StartupTask_Network'];
    startupManager.run(criticalTasks).then(() => {
      hilog.info(0x0000, 'AppStartup', 'Critical tasks done');
    }).catch((error: BusinessError) => {
      hilog.error(0x0000, 'AppStartup',
        'run failed, code: %{public}d, msg: %{public}s',
        error.code, error.message);
    });
  }
}

手动触发的好处是你可以选择只运行关键路径上的任务,把非关键任务延迟到首屏渲染后。

三、任务分级策略

不是所有初始化任务都要在启动时跑完。根据对首屏的影响,分三级:

级别

含义

执行时机

典型任务

P0 阻塞

首屏渲染前必须完成

AbilityStage 同步

日志、网络、数据库

P1 延迟

首屏渲染后、用户交互前

onCreate 异步

用户信息、配置拉取

P2 空闲

用户空闲时

页面可见后延迟

推送注册、埋点上报

代码实现:

// AbilityStage - P0 任务由框架自动执行
import { AbilityStage } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { startupManager } from '@kit.AbilityKit';

export default class MyApp extends AbilityStage {
  onCreate(): void {
    hilog.info(0x0000, 'AppStartup', 'AbilityStage onCreate');
    // P0 任务由 AppStartup 框架自动执行
    // 在 startup_config.json 中配置的会在此时自动跑
  }
}
// UIAbility - P1/P2 任务延迟执行
export default class EntryAbility extends UIAbility {
  // P1 任务:首屏渲染后
  runP1Tasks(): void {
    startupManager.run(['StartupTask_UserInfo']).then(() => {
      hilog.info(0x0000, 'AppStartup', 'P1 tasks done');
    });
  }

  // P2 任务:页面可见后延迟 2 秒
  runP2Tasks(): void {
    setTimeout(() => {
      startupManager.run(['StartupTask_Analytics']).then(() => {
        hilog.info(0x0000, 'AppStartup', 'P2 tasks done');
      });
    }, 2000);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', () => {
      // 首屏加载完成后触发 P1
      this.runP1Tasks();
      // P2 在 P1 之后
      this.runP2Tasks();
    });
  }
}

四、配置超时与监听

AppStartup 支持设置超时时间和监听回调。在 startup_config.json 的同级目录下创建参数文件,或者在 startup_config.json 中直接配置:

{
  "configEntry": "$profile:startup_params"
}

startup_params.json:

{
  "timeout": 10000,
  "configEntry": "$profile:startup_config"
}

代码层面监听任务执行:

import { StartupListener, StartupConfig } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    let onCompletedCallback = (error: BusinessError) => {
      if (error) {
        hilog.error(0x0000, 'AppStartup',
          'Startup failed: %{public}s', JSON.stringify(error));
      } else {
        hilog.info(0x0000, 'AppStartup', 'All startup tasks completed');
      }
    };

    let config: StartupConfig = {
      timeout: 10000,
      listener: {
        onCompleted: onCompletedCallback
      } as StartupListener
    };

    startupManager.run(['StartupTask_Logger', 'StartupTask_Network'], config)
      .then(() => {
        hilog.info(0x0000, 'AppStartup', 'Run completed');
      });
  }
}

超时机制很重要:某个第三方 SDK 初始化卡住时,不至于把整个应用启动链路堵死。

五、性能对比

优化前(串行初始化)和优化后(AppStartup 并行)的对比:

指标

优化前

优化后

Logger 初始化

30ms

30ms

Network 初始化

150ms(等 Logger)

150ms(与 Database 并行)

Database 初始化

80ms(等 Network)

80ms(与 Network 并行)

UserInfo 拉取

200ms(等 Database)

200ms(等 Network+Database)

Analytics

100ms(等 UserInfo)

0ms(空闲时执行)

总耗时

~560ms

~380ms

并行省了约 180ms。如果任务更多(大项目可能有 15-20 个初始化任务),收益更大。

六、踩坑记录

坑 1:@Sendable 限制。 StartupTask 类用 @Sendable 装饰,意味着它的字段不能引用非 Sendable 对象。如果初始化逻辑需要访问全局单例,要通过 init 的 context 参数获取,不能直接在类字段里持有。

坑 2:循环依赖。 启动框架会检测循环依赖并报错(错误码 28800002)。如果 A 依赖 B、B 又依赖 A,框架直接拒绝启动。配置时要画出依赖图确认无环。

坑 3:TaskPool 限制。 runOnThread 设为 taskPool 时,任务在 TaskPool 线程执行。注意 TaskPool 的线程数量和并发上限,如果同时并行 10 个任务,实际并发度受 TaskPool 配置限制。

坑 4:只在 entry 模块可用。 启动框架只支持在 entry 类型的 HAP 中使用。feature 模块的初始化逻辑需要通过依赖注入或事件总线触发,不能走 AppStartup。

小结

AppStartup 解决的不是"初始化代码怎么写"的问题,而是"初始化任务怎么编排"的问题。核心收益是并行执行和依赖管理。配合任务分级策略(P0 阻塞、P1 延迟、P2 空闲),可以把冷启动时间从"所有任务串行"降到"关键路径串行 + 非关键并行延迟"。

下篇讲列表渲染。LazyForEach 是鸿蒙长列表性能的关键,但很多人用了还是卡。我们来看为什么。

参考阅读:

  • 第 8 篇:鸿蒙性能优化实战(启动速度部分)

  • 第 31 篇:鸿蒙性能剖析:Profiler 工具链与卡顿定位

  • 官方文档:@ohos.app.appstartup.StartupTask


评论