iOS应用的启动流程和优化详解

一、应用启动流程

1、整体过程

(1)解析Info.plist

  • 加载相关信息,例如如闪屏
  • 沙箱建立、权限检查

(2)Mach-O(可执行文件)加载

  • 如果是胖二进制文件(为了保持向下兼容,且支持旧有设备及旧有指令集),寻找合适当前CPU类别的部分
  • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
  • 定位内部、外部指针引用,例如字符串、函数等
  • 加载类扩展(Category)中的方法
  • C++静态对象加载、调用ObjC的 +load 函数
  • 执行声明为__attribute__((constructor))的C函数

(3)程序执行

  • 调用main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching

 

2、主要阶段:

分为两个阶段,pre-main阶段和main()阶段。程序启动到main函数执行前是pre-main阶段;在执行main函数后,调用AppDelegate中的-application:didFinishLaunchingWithOptions:方法完成初始化,并展示首页,这是main()阶段,或者叫做main()之后阶段。

(1)pre-main阶段:
  • 加载应用的可执行文件
  • 加载动态链接库加载器dyld(dynamic loader)。
  • dyld递归加载应用所有依赖的dylib(dynamic library 动态链接库)。
  • 进行rebase指针调整和bind符号绑定。
  • ObjCruntime初始化(ObjC setup):ObjC相关Class的注册、category注册、selector唯一性检查等。
  • 初始化(Initializers):执行+load()方法、用attribute((constructor))修饰的函数的调用、创建C++静态全局变量等。
(2)main()阶段:
  •  dyld调用main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching
  • 调用didFinishLaunchingWithOptions
 
 

二、获取启动流程的时间消耗

1、pre-main阶段

对于pre-main阶段,Apple提供了一种测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量DYLD_PRINT_STATISTICS 设为1 。之后控制台会输出类似内容,我们可以清晰的看到每个耗时:

 

 从上面可以看出时间区域主要分为下面几个部分:

  • dylib loading time
    • 动态库载入过程,会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。
    • dyld (the dynamic link editor)动态链接器,是一个专门用来加载动态链接库的库,它是开源的。在 xnu 内核为程序启动做好准备后,执行由内核态切换到用户态,由dyld完成后面的加载工作,dyld的主要是初始化运行环境,开启缓存策略,加载程序依赖的动态库(其中也包含我们的可执行文件),并对这些库进行链接(主要是rebaseing和binding),最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。
  • rebase/binding time

ASLR(Address Space Layout Randomization),地址空间布局随机化。在ASLR技术出现之前,程序都是在固定的地址加载的,这样hacker可以知道程序里面某个函数的具体地址,植入某些恶意代码,修改函数的地址等,带来了很多的危险性。ASLR就是为了解决这个的,程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。rebasing这一步主要就是调整镜像内部指针的指向。

Binding:将指针指向镜像外部的内容。

  • ObjC setup time
    • dyld调用的objc_init方法,这个是runtime的初始化方法,在这个方法里面主要的操作就是加载类(对需要的class和category进行注册);
    • objc_init方法通过内部的_dyld_objc_notify_register向dyld注册了一个通知事件,当有新的image(程序中对应实例可简称为image,如程序可执行文件macho,Framework,bundle等)加载到内存的时候,就会触发load_images方法,这个方法里面就是加载对应image里面的类,并调用load方法(在下一阶段initializer)。
    • 如果有继承的类,那么会先调用父类的load方法,然后调用子类的,但是在load里面不能调用[super load]。最后才是调用category的load方法。总之,所有的load都会被调用到(注意:子类的initialize方法会覆盖父类,不同于load方法)。
  • initializer time

承接上一过程进行初始化(load)。如果我们代码里面使用了clang的__attribute__((constructor))构造方法,这里会调用到。

 

2、main()阶段

测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的时间,简单的方法:直接插入代码。(也可以使用其他工具)

  • main函数里

  

  • 到主UI框架的.m文件用extern声明全局变量StartTime

  

  • 在viewDidAppear函数里,再获取一下当前时间,与StartTime的差值即是main()阶段运行耗时。

  

 

 

 

三、改善APP的启动

建议应用的启动时间控制在400ms之下,并且在20s内启动,否则系统会kill app。优化APP的启动时间,需要就是分别优化pre-main和main的时间。

1、改善启动时pre-main阶段

(1)加载 Dylib

载入动态库,这个过程中,会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。对于Apple提供的的系统动态库,做了高度的优化。而对于开发者定义导入的动态库,则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们

(2)Rebase/Binding
减少App的Objective-C类,分类和Selector的个数。这样做主要是为了加快程序的整个动态链接, 在进行动态库的重定位和绑定(Rebase/binding)过程中减少指针修正的使用,加快程序机器码的生成;
 
(3)Objc setup

大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。

在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。

(4)Initializers

到了这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。

在这一步,我们可以做的优化有:

    • 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize
    • 减少构造器函数个数,在构造器函数里少做些事情
    • 减少C++静态全局变量的个数
 
 2、main()阶段的优化

(1)核心点:didFinishLaunchingWithOptions方法

  这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里我们经常会进行:

  • 创建应用的window,指定其rootViewController,调用window的makeKeyAndVisible方法让其可见;
  • 由于业务需要,我们会初始化各个三方库;
  • 设置系统UI风格;
  • 检查是否需要显示引导页、是否需要登录、是否有新版本等;

由于历史原因,这里的代码容易变得比较庞大,启动耗时难以控制。

(2)优化点:

  满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好。在这一步,我们可以做的优化有:

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

四、+load与+initialize

1、+load

(1)+load方法是一定会在runtime中被调用的。只要类被添加到runtime中了,就会调用+load方法,即只要是在Compile Sources中出现的文件总是会被装载,与这个类是否被用到无关,因此+load方法总是在main函数之前调用

(2)+load方法不会覆盖。也就是说,如果子类实现了+load方法,那么会先调用父类的+load方法(无需手动调用super),然后又去执行子类的+load方法。

(3)+load方法只会调用一次。

(4)+load方法执行顺序是:类 -> 子类 ->分类。而不同分类之间的执行顺序不一定,依据在Compile Sources中出现的顺序(先编译,则先调用,列表中在下方的为“先”)

(5)+load方法是函数指针调用,即遍历类中的方法列表,直接根据函数地址调用。如果子类没有实现+load方法,子类也不会自动调用父类的+load方法。


2、+initialize

(1)+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。因此+initialize方法总是在main函数之后调用

(2)+initialize方法只会调用一次。

(3)+initialize方法实际上是一种惰性调用,如果一个类一直没被用到,那它的+initialize方法也不会被调用,这一点有利于节约资源。

(4)+initialize方法会覆盖。如果子类实现了+initialize方法,就不会执行父类的了,直接执行子类本身的。如果分类实现了+initialize方法,也不会再执行主类的。

(5)+initialize方法的执行覆盖顺序是:分类 -> 子类 ->类。且只会有一个+initialize方法被执行

(6)+initialize方法是发送消息(objc_msgSend()),如果子类没有实现+initialize方法,也会自动调用其父类的+initialize方法。

 

3、两者的异同

(1)相同点

  1. load和initialize会被自动调用,不能手动调用它们。
  2. 子类实现了load和initialize的话,会隐式调用父类的load和initialize方法。
  3. load和initialize方法内部使用了锁,因此它们是线程安全的。

(2)不同点

  1. 调用顺序不同,以main函数为分界,+load方法在main函数之前执行,+initialize在main函数之后执行。
  2. 子类中没有实现+load方法的话,子类不会调用父类的+load方法;而子类如果没有实现+initialize方法的话,也会自动调用父类的+initialize方法。
  3. +load方法是在类被装在进来的时候就会调用,+initialize在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不回调用到+initialize方法。

 

4、使用场景

(1)+load一般是用来交换方法Method Swizzle,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在+load方法中注册。
(2)+initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作。



 

 

参考链接:
https://www.jianshu.com/p/f41bf869809f
https://www.jianshu.com/p/c9406eff7b89
https://www.jianshu.com/p/7a5610d5802f 

 

posted @ 2020-04-28 19:33  执着的怪味豆  阅读(1337)  评论(0编辑  收藏  举报