性能优化-应用启动时间

性能优化-应用启动时间

设置环境变量

这里的应用启动时间指,应用启动到显示第一个页面展示时的时间。

应用启动有冷启动和热启动,热启动是指应用在后台活着,然后再启动应用。这里只谈冷启动。

启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

Xcode11,通过添加环境变量可以打印出APP的启动时间分析(Product- scheme-Edit scheme -> Arguments-如下),DYLD_PRINT_STATISTICS设置为1,如果查看更详细的信息可以DYLD_PRINT_STATISTICS_DETAILS设置为1。

截屏2020-03-24上午11.48.34.png
截屏2020-03-24上午11.48.34.png

跑下工程:

Total pre-main time: 6.3 seconds (100.0%)    
 dylib loading time: 2.1 seconds (34.7%)     
rebase/binding time: 3.7 seconds (59.5%)        
 ObjC setup time:  70.06 milliseconds (1.1%)    
  initializer time: 287.55 milliseconds (4.5%)     
slowest intializers :
...........

可以看到,在执行main函数前,应用准备了执行了4个流程:dylib loadingrebase/bindingObjC setupinitializer,下面我们将好好分析这几个流程。

  • load dylibs:加载动态库,包括系统的、自己添加的(第三方的),递归一层一层加载所依赖的库。
加载dylib
分析每个dylib(大部分是iOS系统的),找到其Mach-O文件,
打开并读取验证有效性,找到代码签名注册到内核,
最后对dylib的每个segment调用mmap()。
  • Rebase&Bind:修复指针,mach-o内部的存储逻辑是,信息的存储地址是虚拟内存,不是直接对应物理内存;每一次应用启动的时候,内存的开始地址又是随机的,因此需要对接虚拟内存和物理内存地址。为了安全,防止黑客攻击。
rebase/bind
dylib加载完成之后,它们处于相互独立的状态,需要绑定起来。
在dylib的加载过程中,系统为了安全考虑,引入了ASLR(Address Space Layout Randomization)技术和代码签名。
由于ASLR的存在,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。
Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。
Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。
  • Objc:注册类信息到全局Table中
OC setup
OC的runtime需要维护一张类名与类的方法列表的全局表。
dyld做了如下操作:
对所有声明过的OC类,将其注册到这个全局表中(class registration)
将category的方法插入到类的方法列表中(category registration)
检查每个selector的唯一性(selector uniquing)
  • Initializers:初始化部分,+load方法初始化,C/C++静态初始化对象和标记__attribute__(constructor)的方法
如果在各个 OC 类别的 ‘load’方法里做了不少事情(如在里面使用 Method swizzle),那么这是pre-main阶段最耗时的部分。dyld运行APP的初始化函数,调用每个OC类的+load方法,调用C++的构造器函数(attribute((constructor))修饰),创建非基本类型的C++静态全局变量,然后执行main函数。
  • Main():执行main函数,执行APPDelegate的方法
  • 加载Window+加载RootViewController+初始化操作:主要在didFinishLaunchingWithOptions执行操作,比如初始化第三方库,初始化基础信息,加载RootViewController等

优化整体思路

1. 移除不需要用到的动态库
2. 移除不需要用到的类
3. 合并功能类似的类和扩展
4. 尽量避免在+load方法里执行的操作,可以推迟到+initialize方法中。

在了解了应用启动流程后,那对应用启动优化的工作就细分到了对每个流程的优化上。

1. load dylibs加载动态库

启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。合并动态库。

2. Rebase & Bind & Objective C Runtime


Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:

1)减少__DATA段中的指针数量。

2)合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个
删除无用的方法和类。

3)多用Swift Structs,因为Swfit Structs是静态分发的。

3. Initializers


通常,我们会在+load方法中进行method-swizzling,但这会影响应用启动的时间。

1)用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。

2)减少atribute((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。

3)不要创建线程

4)重写代码。

4. main函数之后

优化的核心思想:能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。

  • 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions,applicationDidBecomeActive
  • 初始化第三方skd
  • 初始化Window,初始化基础的ViewController
  • 获取数据(Local DB/Network),展示给用户。

在这个过程中我们可以借助工具来进行检测

  • 知道这个过程后,可以借助Time Profiler工具查找具体的耗时模块,几点要注意:

    • 分析启动时间,一般只关心主线程
    • 选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码
    • 右侧可以看到详细的调用堆栈信息
  • 另外,也可以借用C语言函数查看模块运行时间:

CFTimeInterval startTime = CACurrentMediaTime();
//执行某个方法
CFTimeInterval endTime = CACurrentMediaTime();
当检测出耗时的模块时,就可以按照优化的核心思想来进行处理了;

能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

main以后的优化思路

梳理各个三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的viewDidAppear方法里。
梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
避免复杂/多余的计算。
避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。
采用性能更好的API。
首页控制器用纯代码方式来构建。

你可能感兴趣的:(性能优化-应用启动时间)