本文将探讨一下对于APP Launch的相关概念以及影响Launch的因素及优化方法。
Launch
APP Launch的概念是 用户点击APP icon开始,system将APP加载入内存,直到展现APP的第一帧画面位置的过程。这个过程的时间长短,对于用户体验的影响,还是比较重要的。
pre-main vs. post-main
APP Launch过程从代码角度来划分,可以分为pre-main, post-main两个阶段。即,在进入APP main函数前,以及进入APP main函数后,调用Application delegate相关方法两个阶段。
对于iOS APP,WWDC将启动阶段划分为如下图示:
紫色的部分为在进入main函数前的部分,我们称之为pre-main。 绿色为进入main函数,直到APP第一帧渲染出来的时间, 蓝色是为APP首页数据加载完毕并完全显示的时间。 绿色和蓝色的部分我们称之为post-main。
pre-main
pre-main,主要包括两部分: System Interface, Runtime Init。
System Interface
System Interface主要工作是将APP加载入内存,设置APP可以运行的内存环境。包括
-
将APP加载入内存 -
Load dylibs dyld会递归依次加载动态库入内存中,这其中包括系统通用库,以及我们自己定义或第三方动态库。Apple系统会在操作系统启动时会计算和缓存系统动态库。因此影响这部分时间的,主要是我们自定义或第三方动态库。 -
符号地址修正 Apple为了解决安全问题,引入ASLR和Code Sign,如果不作符号修正,程序将没法正常运行,所以会有Rebase和Bind过程。 -
libSystem init 调用系统的的一些初始化方法,这部分一般时间比较固定,可以不用太关注。
Runtime Init
这个阶段是初始化我们编程语言环境,包括OC及Swift。这个阶段是通过注册dyld的_dyld_objc_notify_register回调,在image加载完时做的。
-
初始化有默认值的静态变量和全局变量的 -
C++ static constructors. -
加载category -
Objective-C +load methods defined in classes or categories. 按照继承层级依次调用:父类+load→子类+load→category +load,注意category的+load不会覆盖原类。 -
Functions marked with the clang attribute attribute((constructor)). -
Any function linked into the __DATA,__mod_init_func section of an app or framework binary.
post-main
经过pre-main阶段后,代码就进入了main()方法体内,这里主要包含三个阶段:
- UIKit Init
- 实例化 UIApplication 和 UIApplicationDelegate
- 开始事件处理和系统集成
- Application Init
这部分是我们熟悉的UIApplicationDelegate的几个生命周期调用:
- application:willFinishLaunchingWithOptions:
- application:didFinishLaunchingWithOptions:
- applicationDidBecomeActive:
- scene:willConnectToSession:options:
- sceneWillEnterForeground:
- sceneDidBecomeActive:
- Initial Frame Render
这里是App渲染第一帧,主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。这里面布局计算,图片解码,图层树的递归commit到Render Server等都是可能影响耗时的点,所以要特别注意。
除了上面三个步骤之外,Apple添加了一个蓝色的Extended阶段,这个阶段是指第一帧的UI框架已经加载完毕,我们程序从网络或DB获取数据来初始化UI的过程。这里主要和我们代码如何获取数据的逻辑相关。不同的APP会有不同的实现逻辑,但是对于用户启动APP的感受来说,也是很重要的。
优化方案
我们针对Launch的每个阶段,可以做一些针对性的优化。 在pre-main阶段,可以:
- 尽量使用静态库.a代替动态库.dylib。因为动态库是在运行时动态链接到APP 内存中的,这里就涉及到一些IO操作以及地址定位计算,这些都会消耗Launch 时间。因此可以使用静态库在编译阶段将代码并入最终的APP产物中。
- 尽量少使用static变量及static 初始化。
- 使用initialize方法代替+load方法。因为所有的+load方法会在启动时被执行,因此可以使用运行时的initialize方法来优化启动时间。
- 删除无用代码
如果符号越多,很显然Rebase和Bind的处理时间就会越长,Objc的初始化也受影响,所以我们需要尽可能减少代码:
- 通过逆向二进制或者生成linkmap,解析所有方法(TEXT.text)和引用到的方法(DATA objc_selrefs),找出无用方法删除
- 解析所有类(DATA.objc_classlist)和引用到的类(DATA.objc_classrefs),找出无用的类删除
- 使用第三方工具或者clang扫描重复代码,精简去重
- 使用LLVM_LTO和GCC_OPTIMIZATION_LEVEL等其他编译选项优化二进制大小
在post-main阶段,可以:
- 轻量化Application Delegate中的操作
主要是针对application(:willFinishLaunchingWithOptions:) 和 application(:didFinishLaunchingWithOptions:) 方法。我们不要在这些方法的main thread中执行一些阻塞的繁重操作,取而代之可以延迟执行或是在子线程中执行。这里可以针对具体情况,做一些优化。 - 轻量化首屏UI及数据
首屏尽量不要使用复杂的UI展示,同时对于展示数据,我们可以通过缓存及分段加载的方式,来优化数据响应时间
除了上述方法外,也可以通过二进制重排的手段,来减少启动阶段的内存抖动,进而达到优化启动时间的效果。关于二进制重排,我们可以查找其他资料了解一下。
Profile APP Launch
在之前,我们可以通过设置APP的启动环境变量
DYLD_PRINT_STATISTICS = 1
来在控制台打印在main方法前dyld的耗时统计,但是现在由于Apple升级了dyld,这个变量已经不起作用了。 但是我们可以通过Instrument 中的App Launch工具来调试Launch time。 点击启动后,APP Launch会经过若干时间的分析,得到如下图所示的统计结果,可以看到各个阶段的可以看到方法调用堆栈等信息: 三击某个阶段,即可聚焦于那个阶段,
APP Launch的使用方式可以查阅相关帮助文档,这里就不再赘述。
Cold/Warm/Resume
APP的launch过程,本质上是将APP从存储介质加载到内存中并展示出UI,响应用户操作的过程。这里涉及到一个内存调度的策略。我们知道,在内存紧张的情况下,系统会对内存中长期不被使用的部分转回到存储介质中暂存,同时将其他活跃部分置换到内存中来。
按照这个现实,我们的APP在launc时,可以分为三种情况:Cold launch、Warm launch、Resume。 如图所示,Cold->Resume的启动速度依次递增。
关于Cold\Warm\Resume的启动时机如下图所示:
参考资料
WWDC 2019
|