文章目录

  • Launch
  • pre-main vs. post-main
  • pre-main
  • System Interface
  • Runtime Init
  • post-main
  • 优化方案
  • Profile APP Launch
  • Cold/Warm/Resume


本文将探讨一下对于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将启动阶段划分为如下图示:

postgress 启动命令_初始化


紫色的部分为在进入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的初始化也受影响,所以我们需要尽可能减少代码:
  1. 通过逆向二进制或者生成linkmap,解析所有方法(TEXT.text)和引用到的方法(DATA objc_selrefs),找出无用方法删除
  2. 解析所有类(DATA.objc_classlist)和引用到的类(DATA.objc_classrefs),找出无用的类删除
  3. 使用第三方工具或者clang扫描重复代码,精简去重
  4. 使用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。

postgress 启动命令_launcher_02


点击启动后,APP Launch会经过若干时间的分析,得到如下图所示的统计结果,可以看到各个阶段的可以看到方法调用堆栈等信息:

postgress 启动命令_launcher_03


三击某个阶段,即可聚焦于那个阶段,

postgress 启动命令_launcher_04


APP Launch的使用方式可以查阅相关帮助文档,这里就不再赘述。

Cold/Warm/Resume

APP的launch过程,本质上是将APP从存储介质加载到内存中并展示出UI,响应用户操作的过程。这里涉及到一个内存调度的策略。我们知道,在内存紧张的情况下,系统会对内存中长期不被使用的部分转回到存储介质中暂存,同时将其他活跃部分置换到内存中来。

按照这个现实,我们的APP在launc时,可以分为三种情况:Cold launch、Warm launch、Resume。

postgress 启动命令_postgress 启动命令_05


如图所示,Cold->Resume的启动速度依次递增。关于Cold\Warm\Resume的启动时机如下图所示:

postgress 启动命令_postgress 启动命令_06

参考资料

WWDC 2019