应用冷启动概述
应用启动时延是影响用户体验的关键要素,当用户点击桌面应用图标、通知或其他入口启动应用,到用户的数据显示在屏幕上,如果这段时间花费的时间比较长,这样肯定会影响用户的体验。
应用启动可以分为冷启动和热启动,当应用启动时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用, 这种启动方式就叫做冷启动;而热启动是当应用程序已经在后台运行,用户再次打开应用程序时,应用程序仍然在内存中,可以直接从内存中加载并继续之前的状态,而不需要重新初始化和加载资源。
当应用冷启动时延大于1100ms时,可以认为是应用启动缓慢,体验标准可以参考应用流畅体验设计。
本文将介绍以下内容,来帮助开发者提升应用的冷启动速度,避免卡顿感:
应用冷启动流程
在优化应用冷启动体验前,需要先了解应用冷启动的流程和几个重要的生命周期。应用冷启动的过程大致可分成以下四个阶段:应用进程创建&初始化、Application&Ability初始化、Ability/AbilityStage生命周期、加载绘制首页,如下图所示。
- 应用进程创建&初始化阶段:该阶段主要是系统完成应用进程的创建以及初始化的过程,包含了启动页图标(startWindowIcon)的解码。
- Application&Ability初始化:该阶段主要是资源加载、虚拟机创建、Application&Ability相关对象的创建与初始化、依赖模块的加载等。
- Ability/AbilityStage生命周期:该阶段主要是AbilityStage/Ability的启动生命周期,执行相应的生命周期回调。
- 加载绘制首页:该阶段主要是加载首页内容、测量布局、刷新组件并绘制。
- 网络数据二次刷新:该阶段主要是应用根据业务需要对网络数据进行请求、处理、二次刷新。
可见如果想要提升应用冷启动速度,需要缩短以上几个阶段的耗时。
识别启动缓慢问题
如果开发者需要分析启动过程的耗时瓶颈,优化应用或服务的冷启动速度,可使用Profiler提供的Launch场景分析能力,录制启动过程中的关键数据进行分析,从而识别出导致启动缓慢的原因所在。Profiler Launch可以拆解应用冷启动过程,抓取不同阶段的耗时数据,帮助开发者快速分析冷启动过程的耗时瓶颈,Launch的具体使用可以参考冷启动分析:Launch分析。
下面录制了一段Launch任务,具体操作步骤请参见性能问题定位:深度录制。
从上图可以看到Launch将应用的冷启动过程拆解为以下几个阶段:
- Create Process:应用进程创建阶段,对应的trace打点为AbilityManagerService::StartAbility,MissionListManager::StartAbilityLocked##{bundleName}
- Application Launching:应用启动阶段,对应的trace打点为AppMgrServiceInner::AttachApplication##{bundleName}
- UI Ability Launching:UIAbility启动阶段,对应的trace打点为MainThread::HandleLaunchAbility##{bundleName}
- UI Ability OnForeground:应用进入前台阶段,对应的trace打点为AbilityThread::HandleAbilityTransaction
- First Frame - App Phase:App首帧渲染提交阶段,对应的trace打点为H:ReceiveVsync,H:MarshRSTransactionData
- First Frame - Render Phase:RS首帧渲染提交阶段,对应的trace打点为H:ReceiveVsync,H:RSMainThread::ProcessCommandUn
- EntryAbility:应用启动之后的阶段,渲染完成,首页显示。
从Create Process阶段开始到First Frame - Render Phase阶段,这段时间可以看作是应用的冷启动时间,在Launch泳道到鼠标左键拖动这两个阶段的区间,可以看到应用冷启动时间为215.2ms,这个启动速度是比较快的。
冷启动缓慢示例分析
运行如下示例代码,我们可以明显的感知应用启动比较缓慢。接下来我们通过这个示例,结合Launch来分析应用冷启动缓慢问题。
const LARGE_NUMBER = 200000000;
const DELAYED_TIME = 1000;
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
aboutToAppear(): void {
console.log('aboutToAppear');
this.computeTask();
}
computeTask(): void {
let count = 0;
while (count < LARGE_NUMBER) {
count++;
}
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
首先是创建Launch模板录制,可以看到整个的启动时间比较长,其中UI Ability OnForeground这个阶段占据应用冷启动过程的大部分时间,耗时达到了4.1s,所以我们需要重点分析这个阶段的耗时。
针对应用冷启动问题的性能分析,有以下两种方式可以选择,一种是分析主线程的Trace数据,另一种则是分析采样得到的函数热点。
分析主线程的Trace数据
- 单击“Launch”泳道上的UI Ability OnForeground阶段,在下方的“Details”详情面板中,可查看到所选阶段的耗时统计情况;
- 展开UI Ability OnForeground统计信息折叠表,可以看到各函数的具体耗时信息;
- 根据Duration找到耗时最长的函数aboutToAppear;
- 单击图标按钮,可直接跳转至主线程的打点任务中,查看相关Trace数据,如下图所示。
可以看到在UI Ability OnForeground阶段的耗时基本是由aboutToAppear造成的,我们再看aboutToAppear中的代码逻辑,可以推断是由于计算任务computeTask耗时造成的。
分析采样得到的函数热点
我们也可以分析采样得到的函数热点直观的显示应用冷启动过程中具体函数的耗时,如下图:
- 单击“Launch”泳道上的UI Ability OnForeground阶段;
- 选择“ArkTS Callstack”泳道,其会基于时间轴展示CPU使用率和状态的变化,以及当前调用栈名称和调用类型;
- 下方“Details”详情面板中查看到这段时间内的函数热点,其会以Top-Down形式的树状列表进行展示。很明显aboutToAppear函数中的computeTask函数耗时最多,占整个阶段的96.7%,双击该函数可以跳转到源码。
- 此外,点击底部Flame Chart按钮打开火焰图可以更直观的看出热点函数的耗时情况,如下图所示。
冷启动速度优化
通过前面的分析,冷启动缓慢是由于在aboutToAppear执行了耗时计算任务,我们可以将该computeTask以异步延时的方式处理,优化后的代码如下:
const LARGE_NUMBER = 100000000;
const DELAYED_TIME = 1000;
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
aboutToAppear(): void {
console.log('aboutToAppear');
this.computeTaskAsync();
}
...
computeTask(): void {
let count = 0;
while (count < LARGE_NUMBER) {
count++;
}
}
// 运算任务异步处理
private computeTaskAsync(): void {
setTimeout(() => { // 这里使用setTimeout来实现异步延迟运行
this.computeTask();
}, DELAYED_TIME);
}
}
然后重新编译运行程序以及录制Launch,可以看到优化后UI Ability OnForeground阶段耗时大幅度缩短,如下图所示:
查看首帧卡顿
为了识别首帧是否卡顿,可以先在Launch的Frame泳道进行查看。应用的首帧渲染提交在First Frame - App Phase阶段,APP侧下面的这一帧表示应用渲染的首帧,如下图所示,此处首帧为36号帧:
如上所示36号帧被标记为了红色,表示首帧出现了卡顿。鼠标左键36号帧,可以看到它的期望提交渲染时间为左边白色竖线区域所示,这里出现了比较严重的延时。发现问题后,开发者可以参考前面讲到的示例进行问题定位和优化。
提升应用冷启动速度
本文将通过公共类优化的方法,包括非冷启动必须的服务或模块延迟加载、减少主线程非UI耗时操作和网络请求提前发送,以及结合应用启动的几个阶段分别介绍提升应用冷启动速度的相关方法。
非冷启动必须的服务或模块延迟加载
应用在启动前加载过多不必要启动项,同时这些启动项在主线程串行执行,该阶段耗时接近450ms。
应用冷启动过程中,加载自身不必要的串行启动项,会导致冷启动耗时增加。建议延后加载或者并行处理。
减少主线程非UI耗时操作
在应用启动流程中,主要聚焦在执行UI相关操作中,为了更快的显示首页内容,不建议在主线程中执行非UI相关的耗时操作,建议通过异步任务进行延迟处理或放到其他子线程中执行,线程并发方案详细请参见TaskPool和Worker的对比实践。
在冷启动过程中如果存在图片下载、网络请求前置数据、数据反序列化等非UI操作开发者可以根据实际情况移至子线程中进行,详细请参见应用并发设计。
网络请求提前发送
当前大多数应用的首页内容需从网络获取,发送网络请求的时机显得尤为重要。应用发送网络请求后等待网络数据的返回,网络请求的这段时间应用可以继续执行启动流程,直到网络数据返回后进行解析,反序列化之后就可以加载首页数据,因此网络请求的发起时机越早,整个冷启动的完成时延阶段越短。
可将网络请求及网络请求前的初始化流程放置在AbilityStage/UIAbility的onCreate()生命周期中,在AbilityStage/UIAbility中仅执行网络相关预处理,等待网络请求发送后可继续执行首页数据准备、UI相关操作。在服务端处理流程相同的情况下,应用可以更早的拿到网络数据并行展示。
图1 应用首页框架加载时进行网络数据请求
将网络请求提前至AbilityStage/UIAbility生命的onCreate()生命周期回调函数中,可以将首刷或二刷的时间提前,减少用户等待时间。此处为了体现性能收益,将网络请求放到了更早的AbilityStage的onCreate()生命周期回调中。
图2 网络请求提前至AbilityStage的onCreate()周期回调中
【优化前】:在首页根组件的onAppear()周期回调中发起网络请求。
// entry/src/main/ets/pages/Index.ets
import { httpRequest } from '../utils/NetRequest';
import { number } from '../utils/Calculator';
AppStorage.link('netData');
PersistentStorage.persistProp('netData', undefined);
@Entry
@Component
struct Index {
@State message: string = 'Hello World' + number; // 为了体现性能收益,引用耗时函数的执行结果number
@StorageLink('netData') netData: PixelMap | undefined = undefined;
build(){
Row(){
Image(this.netData)
.objectFit(ImageFit.Contain)
.width('50%')
.height('50%')
}
.onAppear(() => {
// 发送网络请求
httpRequest();
})
}
}
// entry/src/main/ets/utils/NetRequest.ets
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';
// 通过http的request方法从网络下载图片资源
export function httpRequest() {
hiTraceMeter.startTrace('Http Request', 1);
http.createHttp()
// 实际开发需要将"https://www.example1.com/POST?e=f&g=h"替换成为真实要访问的网站地址
.request('https://www.example1.com/POST?e=f&g=h',
(error: BusinessError, data: http.HttpResponse) => {
if (error) {
// 下载失败时不执行后续逻辑
return;
}
// 处理网络请求返回的数据
transcodePixelMap(data);
}
)
}
// 使用createPixelMap将ArrayBuffer类型的图片装换为PixelMap类型
function transcodePixelMap(data: http.HttpResponse) {
if (http.ResponseCode.OK === data.responseCode) {
const imageData: ArrayBuffer = data.result as ArrayBuffer;
// 通过ArrayBuffer创建图片源实例
const imageSource: image.ImageSource = image.createImageSource(imageData);
const options: image.InitializationOptions = {
'alphaType': 0, // 透明度
'editable': false, // 是否可编辑
'pixelFormat': 3, // 像素格式
'scaleMode': 1, // 缩略值
'size': { height: 100, width: 100 }
}; // 创建图片大小
// 通过属性创建PixelMap
imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
AppStorage.set('netData', pixelMap);
hiTraceMeter.finishTrace('Http Request', 1);
});
}
}
// entry/src/main/ets/utils/Calculator.ets
const LARGE_NUMBER = 100000000;
function computeTask(): number {
let count = 0;
while (count < LARGE_NUMBER) {
count++;
}
return count;
}
export let number = computeTask();
【优化后】
- 在NetRequest.ets中进行Http请求以及数据处理。
// NetRequest.ets
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';
// 通过http的request方法从网络下载图片资源
export function httpRequest() {
hiTraceMeter.startTrace('Http Request', 1);
http.createHttp()
// 实际开发需要将"https://www.example1.com/POST?e=f&g=h"替换成为真实要访问的网站地址
.request('https://www.example1.com/POST?e=f&g=h',
(error: BusinessError, data: http.HttpResponse) => {
if (error) {
// 下载失败时不执行后续逻辑
return;
}
// 处理网络请求返回的数据
transcodePixelMap(data);
}
)
}
// 使用createPixelMap将ArrayBuffer类型的图片装换为PixelMap类型
function transcodePixelMap(data: http.HttpResponse) {
if (http.ResponseCode.OK === data.responseCode) {
const imageData: ArrayBuffer = data.result as ArrayBuffer;
// 通过ArrayBuffer创建图片源实例
const imageSource: image.ImageSource = image.createImageSource(imageData);
const options: image.InitializationOptions = {
'alphaType': 0, // 透明度
'editable': false, // 是否可编辑
'pixelFormat': 3, // 像素格式
'scaleMode': 1, // 缩略值
'size': { height: 100, width: 100 }
}; // 创建图片大小
// 通过属性创建PixelMap
imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
AppStorage.set('netData', pixelMap);
hiTraceMeter.finishTrace('Http Request', 1);
});
}
}
2.在AbilityStage的onCreate()生命周期回调中发起网络请求。
// MyAbilityStage.ets
import { AbilityStage, Want } from '@kit.AbilityKit';
import { httpRequest } from '../utils/NetRequest';
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
// 发送网络请求
httpRequest();
}
onAcceptWant(want: Want): string {
// 仅specified模式下触发
return 'MyAbilityStage';
}
}
3.在首页Index.ets中展示请求获取的图片。
// Index.ets
import { number } from '../utils/Calculator';
AppStorage.link('netData');
PersistentStorage.persistProp('netData', undefined);
@Entry
@Component
struct Index {
@State message: string = 'Hello World' + number; // 为了体现性能收益,引用耗时函数的执行结果number
@StorageLink('netData') netData: PixelMap | undefined = undefined;
build() {
Row() {
Image(this.netData)
.objectFit(ImageFit.Contain)
.width('50%')
.height('50%')
}
.onDisAppear(() => {
AppStorage.set('netData', undefined);
})
.height('100%')
.width('100%')
}
}
使用Launch模板,对优化前后启动性能进行对比分析。分析阶段的起点为启动Ability(即H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility的开始点),阶段终点为应用接收到网络数据返回后的首帧刷新(即H:ReceiveVsync dataCount:24Bytes now:timestamp expectedEnd:timestamp vsyncId:int的开始点)
图3 优化网络请求时机前
图4 优化网络请求时机后
对比数据如下:
方案 | 阶段时长(毫秒) |
---|---|
优化网络请求时机前 | 1700 |
优化网络请求时机后 | 885.3 |
因此,可以通过提前网络请求的方式减少应用冷启动耗时。