启动性能与首帧优化
一、引言
启动是用户对应用的第一印象:冷启动超过 3 秒,用户就可能放弃等待;首帧长时间白屏,再好的内容也无人问津。多设备短视频项目横跨直板机、PC、TV、手表四个 HAP,不同设备的硬件差异决定了启动策略必须"一机一策"——手表不能照搬 PC 的启动逻辑,TV 的焦点系统也需要在首帧前就绪。
从第 46 篇的总览视角看,启动性能属于"卡顿"维度的特例:它不是单帧问题,而是从进程创建、Ability 生命周期、首帧渲染到数据就绪的整条链路问题。本章围绕项目四个 HAP 的入口MultiShortVideo*Ability.ets,拆解启动链路、冷启动优化、首帧路径精简、懒初始化与按需加载,并对比四个 HAP 的启动差异。
二、启动链路分析:Ability 生命周期与 loadContent
冷启动的完整链路如下:
graph LR A[点击图标] --> B[进程创建] B --> C[onCreate] C --> D[onWindowStageCreate] D --> E[loadContent 首帧] E --> F[onForeground] F --> G[数据加载/播放器初始化]onWindowStageCreate是首帧前最关键的一环。loadContent之前的所有同步代码都会推迟首帧,因此这里只做"必须做的事"。以直板机入口为例(products/default/src/main/ets/defaultability/MultiShortVideoDefaultAbility.ets):
onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.loadContent('view/Index', (err) => { if (err.code) { ...; return; } let windowUtil = WindowUtil.getInstance(); windowUtil.setWindowStage(windowStage); windowUtil.setUIContext(); windowUtil.updateWindowInfo(); // 设置状态栏文字颜色等窗口属性 }); }注意两点:窗口初始化放在 loadContent 成功回调之后,避免阻塞首帧;WindowUtil是单例懒创建,首次调用才实例化。如果反过来先做窗口配置再 loadContent,首帧时间会被无谓拉长。
启动按进程与页面状态可分为三类,优化目标不同:
| 类型 | 定义 | 优化重点 |
| 冷启动 | 进程不存在,全量创建 | 缩短能力创建与首帧路径 |
| 温启动 | 进程在但页面销毁 | 复用进程与单例,快速重建页面 |
| 热启动 | 进程与页面均在后台 | 恢复前台,几乎无开销 |
onCreate只执行一次,冷启动的主要耗时集中在onWindowStageCreate与loadContent;温启动则依赖WindowUtil等单例跨生命周期存活,避免重复初始化。项目四个 HAP 的onWindowStageDestroy都调用WindowUtil.getInstance().release()注销监听,但单例对象本身保留,保证下一次启动能复用窗口信息。
三、Ability 冷启动优化:按需窗口配置
不同设备的窗口配置差异明显,也决定了启动路径的长短:
| HAP | 入口 Ability | 启动窗口额外配置 |
| 直板机 | MultiShortVideoDefaultAbility | 状态栏文字颜色 |
| PC | MultiShortVideoPcAbility | 装饰栏隐藏、高度、按钮样式 |
| TV | MultiShortVideoTvAbility | 无(依赖焦点系统) |
| 手表 | MultiShortVideoWearableAbility | 无(精简) |
PC 端的配置最多,但都发生在loadContent回调中,且setWindowDecorVisible、setWindowDecorHeight、setDecorButtonStyle相互独立、互不等待,全部走异步接口,不阻塞首帧:
// products/pc/src/main/ets/pcability/MultiShortVideoPcAbility.ets(节选) windowStage?.getMainWindowSync().setWindowDecorVisible(false); windowStage?.getMainWindowSync().setWindowDecorHeight(56); windowStage?.getMainWindowSync().setDecorButtonStyle({ colorMode: ... });冷启动优化的三条原则:
- onCreate 保持极简:只做参数解析与必要单例,不初始化任何 UI 相关资源;
- 窗口配置后置:跟随
loadContent回调,与首帧渲染并行; - 不阻塞回调链:多个窗口接口串行
await会拖慢可用时间,改为并发触发。
四、首帧渲染路径精简
首帧时间 = 加载页面 + 构建首屏组件树 + 首帧上屏。缩短路径从页面结构入手,项目Index.ets的首页结构为:
// products/default/src/main/ets/view/Index.ets(节选) build() { Navigation(this.pathStack) { MSVTabs({ data: this.data, isDark: this.isDark, onIndexChange: (index: number) => { ... } }) } .navBarWidth(new WidthBreakpointType<number>(410, 410, 700, 700).getValue(this.windowInfo.widthBp)) .hideBackButton(true) .hideTitleBar(true) .mode(this.showSideComment || this.showSideIndividual ? NavigationMode.Split : NavigationMode.Stack) }精简手法:
- 数据量最小化:
getMainTabsData()只构造 5 个页签模型,页签内容通过@Builder(Builders.ets中的home()等)延迟到 TabContent 构建时才执行; - 惰性页签内容:非首页签(朋友、消息、我的)对应
MSVEmptyComponent空态,几乎零成本;视频流recommend()在用户切到推荐页签时才构建AdaptiveVideoForDefault,播放器初始化被推迟到真正需要时; - 先框架后细节:
NavPathStack、NavBarWidth等骨架属性先行,侧栏、模式切换等状态按需响应。
五、懒初始化与按需加载
首帧之后仍有大量"重活"可以后置,避免挤占首帧预算:
| 资源 | 初始化时机 | 触发点 |
| WindowUtil 单例 | 首次调用 | loadContent 回调 |
| 主 Tabs 数据 | aboutToAppear | Index 构建前 |
| 视频数据源 | aboutToAppear | AdaptiveVideo 构建 |
| 播放器实例 | 成为当前页 | onLoad + currentIndex |
| 评论数据 | 打开评论时 | Comment aboutToAppear |
| 作品数据 | 进入个人页时 | Works aboutToAppear |
数据模型层均为"构造即就绪"的轻量对象:MainTabsViewModel.getMainTabsData()在内存中构建页签数组,AvDataSourceViewModel.getAvDataSource()返回 5 条本地视频路径,均无网络与磁盘等待。播放器的initAVPlayer受isInitializing标志与onLoad双重保护,只初始化一次且只在可见时执行,这是首帧后最大的启动成本控制点(详见第 47、49 篇)。
六、四个 HAP 的启动差异对比
多 HAP 架构下,各设备的启动策略对照如下:
| 维度 | 直板机 | PC | TV | 手表 |
| 首页复杂度 | 高(Tabs+视频流) | 高(分栏+Tabs) | 高(Tabs+焦点) | 低(精简 Tabs) |
| 首帧负担 | 视频流懒构建 | 分栏布局 | 焦点系统初始化 | 组件极少 |
| 主要优化点 | 播放器后置 | 窗口配置并发 | 焦点即时响应 | 资源最小化 |
| 数据准备 | 内存模型 | 内存模型 | 内存模型 | 内存模型 |
手表端首页只加载精简的SubTabsComponent与空态页签,不引入视频流;TV 端在首帧前不额外做窗口配置,把资源留给焦点系统;PC 端并发执行多项窗口配置但全部异步。四者共享同一套WindowUtil单例与数据模型,启动代码的差异化被收敛到 Ability 层——这正是"公共代码下沉 common、平台差异隔离在 products"架构的收益。
七、总结与最佳实践
- 启动链路按"onCreate 极简 → loadContent 先行 → 窗口配置后置 → 数据懒加载"的顺序编排;
- 冷启动优化优先保证首帧时间:任何非必要同步工作都不要放在
onWindowStageCreate的同步段; - 首帧路径精简靠惰性构建:页签内容
@Builder延迟执行、空态页签零成本、播放器不可见不初始化; - 懒初始化用单例与标志位保证"只做一次":
WindowUtil.getInstance()、isInitializing防重入; - 多设备启动策略差异化:手表减组件、TV 保焦点、PC 并发窗口配置,但共享公共代码;
- 启动性能同样要量化:用 Profiler 的启动分析(Launch)面板测量冷启动与首帧时间,建立回归基线。