1. 背景
冷启动时长是App性能的重要指标,冷启动的快慢直接影响着用户对App的第一印象.
随着版本的不断迭代,产品形态不断完善,业务功能日趋复杂.App里面的很多配置数据及接入的第三方SDK需要在冷启动期间完成,这给App的冷启动性能带来挑战.为此我们需要建立一套完整质量指标和体系来监测和持续优化冷启动性能.
2. 冷启动定义
大家一般把iOS冷启动过程定义为:从用户点击App图标开始到appDelegate didFinishLaunching
方法执行完成为止。然而,当didFinishLaunchingWithOptions
执行完成时,用户还没有看到App的主界面,也不能对App进行操作。App还需要做一些初始化工作,首页渲染等过程后,用户才能真正看到数据内容并开始使用,我们认为这个时候冷启动才算完成。所以我们把App把冷启动过程定义为:从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2+T3:
App冷启动的3个阶段:
-
T1
: 加载阶段,main()
函数之前,即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()
函数。 -
T2
: 初始化阶段,main()
函数之后,即从main()
开始,到appDelegate的didFinishLaunchingWithOptions
方法执行完毕。 -
T3
: 渲染阶段,didFinishLaunchingWithOptions
方法执行完毕到第一个ViewDidAppear
执行完毕.此时用户可以看到App的主界面,可以使用App.
image.png
其中T1
又分以下几个阶段
3. 问题及现状
3.1 性能存量问题
问题 | 影响阶段 | 影响程度 |
---|---|---|
业务层大量的启动任务 | T2 | 高 |
第三方库的启动任务 | T2+T3 | 高 |
一些比较耗时的主线程异步任务 | T2+T3 | 高 |
同步I/O操作:数据库及NSUserDefaults
|
T2+T3 | 高 |
请求数据,渲染(过程串行,时间较长 | T3 | 高 |
大量的分类 | T1 | 中 |
执行大量的+load() 方法 |
T1 | 中 |
执行大量的+ (void)initialize 方法 |
T1 + T2 | 中 |
加载无用的类,方法 | T1 | 中 |
注:启动项的定义,在App启动过程中需要被完成的某项工作,我们称之为一个启动项。例如配置数据的加载,某个SDK的初始化、某个功能的预加载等。
3.2 性能增量问题:
一般情况下,在App早期阶段,冷启动不会有明显的性能问题。冷启动性能问题也不是在某个版本突然出现的,而是随着版本迭代,App功能越来越复杂,启动任务越来越多,冷启动时间也一点点延长。最后当我们注意到,并想要优化它的时候,这个问题已经变得很棘手了。我们App的性能问题增量主要来自启动项的增加,随着版本迭代,启动项任务简单粗暴地堆积在启动流程中。几个版本后,冷启动时长就会明显增加很多。
4. 治理思路
image.png4.1 数据上报及监控,分析各阶段耗时
- 增加数据上报,监控各阶段耗时,及时发现新版本引入的性能问题.
- 客户端卡顿检测,对现网用户检测启动卡顿情况,及时发现问题.
- 数据上报打点:
-
main
函数之前耗时 -
didFinishLaunchingWithOptions
耗时 - 第一个
viewDidLoad
耗时 - 第一个View
渲染耗时
: 第一个ViewWillAppear
开始到第一个ViewDidAppear
结束耗时 - App内启动耗时: 从
main
函数开始到第一个ViewDidAppear
结束总耗时 - App启动总耗时: 从点击App到第一个
ViewDidAppear
结束总耗时
-
细化
didFinishLaunchingWithOptions
每一步耗时:
对didFinishLaunchingWithOptions
每一步耗时单独上报
4.2 规范启动流程
之前为了实现方便,在 didFinishLaunchingWithOptions
, fireOnWnsLoginSucc
,onLogInSucess
中添加了大量的初始化任务,某些任务不是必须在启动过程中执行,只需要在首页渲染成功后再执行即可.
4.2.1 定义启动任务阶段
以前为了方便某些初始化工作,不停地在 didFinishLaunchingWithOptions
, fireOnWnsLoginSucc
,onLogInSucess
里面添加任务,维护起来不方便,而且某些任务不是必须在启动过程中执行,只需要在首页渲染成功后再执行即可. 分析现有的各阶段调用:
-
didFinishLaunchingWithOptions
必定会在首页显示之前执行且只执行一次 -
fireOnWnsLoginSucc
,onLogInSucess
有可能在首页显示前执行,也有可能在首页显示后执行.
我们需要重新定义各种启动任务的执行阶段,确定自己的任务需要在哪个阶段执行:注意:为了提高启动速度,任务尽可能地靠后执行
我们可以将任务执行阶段分为以下几个阶段:
-
kMain
: 在main
函数开始执行执行 -
kStartup
: 在application:didFinishLaunchingWithOptions:
执行 -
kAppeared
: 在首页渲染完毕后执行 -
kLogin
: 在登录完成后执行 -
kLoginEx
: 在登录完成后且首页渲染完毕后执行
为此我们需要实现这样一种机制:
- 如果首页已经显示出来,任务立刻执行
- 如果首页尚未显示出来,则延后到首页显示出来后执行
详情请参考runTaskAfterLaunchFinish
4.2.2 启动任务框架:不需要修改现有代码流程实现启动任务的方法
目前要在 didFinishLaunchingWithOptions
, fireOnWnsLoginSucc
,onLogInSucess
添加任务,都需要在相应的函数里面添加代码,可扩展性和维护性不高(fireOnWnsLoginSucc
,onLogInSucess
可以通过注册相应通知来实现非嵌入添加任务,但是通知的方式性能比较差),我们可以利用Clang提供的编译器特性,在编译期将相关函数注册到二进制文件,在运行时读取相关配置来执行相关函数
通过该框架,我们可以很方便地添加一个在特定阶段执行的代码
//在application:didFinishLaunchingWithOptions阶段执行的代码
RUN_FUNCTION_ON_STAY(kStartup)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在main开始阶段执行的代码
RUN_FUNCTION_ON_STAY(kMain)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在首页渲染完毕后执行的代码
RUN_FUNCTION_ON_STAY(kAppeared)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在登录完成后执行的代码
RUN_FUNCTION_ON_STAY(kLogin)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在登录完成后且首页渲染完毕后执行
RUN_FUNCTION_ON_STAY(kLoginEx)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
4.3 代码规范
-
避免频繁访问IO,减少IO操作,尽可能使用异步IO
- 避免频繁从
NSUserDefaults
或者数据库读取数据.内存缓存需要频繁访问的配置数据(小数据) - 尽可能将多个相关的配置放进一个结构里面统一保存,避免零散读写配置
- 内存缓存需要频繁访问的图片,如头像,作品,伴奏等的默认占位图.
- 异步IO:在异步线程执行IO操作
- 避免频繁从
-
尽可能地将耗时任务放在非主线程异步处理
-
删除无用代码,减少分类,减少自定义
+load()
和+ (void)initialize
以下代码会增加main函数前的加载耗时- 自定义
+load()
和+ (void)initialize
- 分类
- 无用代码
- 自定义
-
避免在
ViewController
的init
里面加载数据,尽量在viewDidLoad加载数据
目前首页里面有三个tab,其中动态页有三个子页面,点歌台有四个子页面,动态页有一个子页面,每个子页面都有一个ViewController
承载,总共7个子VC,如果都在VC的init
里面加载数据,会大幅减低首页加载速度.
5. 优化目标:
- preMain前耗时: 700ms以内(目前800ms以上)
- 首次启动耗时: 少于1.5秒
- 非首次启动耗时:少于 2秒(目前平均 3.3秒)
6. 优化计划:
6.1 阶段1: 分析各阶段耗时分布,打点并上报,建立启动耗时各项指标(已完成)
-
main
函数之前耗时 -
didFinishLaunchingWithOptions
耗时 - 第一个
viewDidLoad
耗时 - 第一个View
渲染耗时
: 第一个ViewWillAppear
开始到第一个ViewDidAppear
结束耗时 - App内启动耗时: 从
main
函数开始到第一个ViewDidAppear
结束总耗时 - App启动总耗时: 从点击App到第一个
ViewDidAppear
结束总耗时
-
细化
didFinishLaunchingWithOptions
每一步耗时:
对didFinishLaunchingWithOptions
每一步耗时单独上报
6.2 阶段2: 优化耗时任务,非必要任务延后执行(已完成)
- 使用
Time Profiler
和System Trace
分析耗时函数 - 梳理启动流程任务,非必要任务移到首页显示完毕后再开始执行
- 推动第三方库优化
- 建立新的启动任务执行框架,方便添加和统一管理各阶段启动任务
- 使用更高效的框架来替换使用广播通知来通知登录成功事件
- 定义各阶段启动任务并用更易于扩展和管理的方式添加启动任务
//在application:didFinishLaunchingWithOptions阶段执行的代码
RUN_FUNCTION_ON_STAY(kStartup)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在main开始阶段执行的代码
RUN_FUNCTION_ON_STAY(kMain)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在首页渲染完毕后执行的代码
RUN_FUNCTION_ON_STAY(kAppeared)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在登录完成后执行的代码
RUN_FUNCTION_ON_STAY(kLogin)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
//在登录完成后且首页渲染完毕后执行
RUN_FUNCTION_ON_STAY(kLoginEx)
{
NSLog(@"File: %s,Line: %d",__FILE__,__LINE__);
}
6.3 阶段3: 优化IO操作(部分完成)
- 需要频繁访问的保存在磁盘的数据使用内存缓存:如
NSUserDefaults
或者数据库的数据做内存缓存 - 梳理高频访问的图片:如头像,作品,伴奏等的默认占位图,使用内存缓存
- 将配置从
NSUserDefaults
迁移到更高效的数据库 - 提供配置类统一管理配置,将高频访问的配置合并保存
6.4 阶段4: 移除多余代码,减少PreMain
耗时(部分完成)
6.5 代码重排,提高启动速度及整体运行速度
7. 耗时分布分析
启动过程主要耗时分布
从数据分析,耗时主要分布如下
-
premain
耗时 -
didFinishLaunch
耗时 -
viewDidAppear
耗时: 首页渲染耗时,从viewWillAppear
到viewDidAppear
之间的耗时
登录类型 | preMain耗时 | didFinishLaunch耗时 | didBecomeActive耗时 | viewDidLoad耗时 | viewDidAppear耗时 | 启动App耗时 | 启动总耗时 | count |
---|---|---|---|---|---|---|---|---|
有闪屏 | 781.75 | 1,294.32 | 22.75 | 0.32 | 287.15 | 21,397.12 | 22,178.87 | 5,340.00 |
非首次启动无粘贴板 | 870.70 | 1,325.54 | 17.32 | 0.20 | 851.25 | 2,436.70 | 3,307.40 | 1,323.00 |
有粘贴板有闪屏 | 914.31 | 1,396.95 | 22.54 | 0.28 | 369.90 | 6,891.93 | 7,806.24 | 569.00 |
非首次启动有粘贴板 | 1,221.50 | 1,616.51 | 18.78 | 0.30 | 730.98 | 2,664.43 | 3,885.94 | 113.00 |
首次启动无粘贴板 | 738.88 | 825.47 | 11.53 | 0.06 | 228.29 | 1,270.41 | 2,009.29 | 17.00 |
首次启动有粘贴板 | 1,684.71 | 1,267.43 | 7.14 | 0.43 | 262.43 | 1,943.57 | 3,628.29 | 7.00 |
didFinishLaunchingWithOptions 主要耗时分布
从数据分析,耗时主要分布如下
-
enterMainUIWithLaunchOptions
: 重点优化 -
initWNSModule
: 重点优化 -
updateUserAgentWeSing
:主要耗时在WKWebView
触发的webkit模块加载逻辑,可优化成无需创建WKWebView
-
WSAuthSDKBase
: 无优化空间,是否可以延后操作? -
pasteBoardStrings
: 可延后处理 initRDMCrashReportModule
函数 | 耗时 |
---|---|
enterMainUIWithLaunchOptions | 557.65 |
initWNSModule | 446.73 |
updateUserAgentWeSing | 66.91 |
WSAuthSDKBase | 68.31 |
pasteBoardStrings | 18.14 |
initRDMCrashReportModule | 29.95 |
count | 25.10 |
enterMainUIWithLaunchOptions | 20.56 |
initWNSModule | 15.67 |
updateUserAgentWeSing | 19.66 |
WSAuthSDKBase | 10.03 |
pasteBoardStrings | 9.53 |
启动后优化
优化列表
-
image.png-[KSImageDownloadManager _clearCacheForKey:]
获取本地图片缓存失败要清理相应缓存,而实际上没有做缓存,该操作为多余
image.png -
image.png-[KSLoginManager(Auth) fireOnWnsLoginSucc]
部分任务延后执行
-
-[KSAppDelegate LogInSucess]
部分任务延后执行
网友评论