iOS 启动优化(一)

“冷启动”与“热启动”

主要区别:

名称 区别
冷启动 启动时,App的进程不在系统里,需要开启新进程。
热启动 启动时,App的进程还在系统里,不需要开启新进程

APP启动时间的优化

那我们通常所说的启动时间优化都是在说的冷启动的时间优化

冷启动过程做了什么

主要分为三个阶段:
1. pre-main:main()函数之前
2. main:main()函数之后(从main函数执行,到设置self.window.rootViewController执行完成)
3.首屏渲染完成后:(从self.window.rootViewController执行完成到didFinishLaunchWithOptions方法作用域结束)

查看Pre-Main()阶段花费的总时间

想查看Pre-Main阶段的时间比较简单。

直接打开Xcode,找到Product->Scheme>Edit Scheme->Run->Arguments->Environment Variables->DYLD_PRINT_STATISTICS设置为 YES

image.png

Total pre-main time: 353.01 milliseconds (100.0%)
         dylib loading time: 210.68 milliseconds (59.6%)  //加载动态库
        rebase/binding time: 126687488.8 seconds (71548418.3%)
            ObjC setup time: 134.92 milliseconds (38.2%)
           initializer time: 137.90 milliseconds (39.0%)
           slowest intializers :
             libSystem.B.dylib :   3.83 milliseconds (1.0%)
   libBacktraceRecording.dylib :   7.19 milliseconds (2.0%)
    libMainThreadChecker.dylib :  97.57 milliseconds (27.6%)
        libLLVMContainer.dylib :  20.92 milliseconds (5.9%)

1 . load dylibs:这一阶段dyld会分析应用依赖的dylib,找到其mach-o文件,打开和读取这些文件并验证其有效性,接着会找到代码签名注册到内核,最后对dylib的每一个segment调用mmap()。
2 . rebase/bind:进行rebase指针调整和bind符号绑定。
3 . ObjC setup:runtime运行时初始化。包括ObjC相关Class的注册、category注册、selector唯一性检查等。
4 . Initializers:调用每个ObjC类与分类的+load方法,调用attribute((constructor))修饰的函数、创建C++静态全局变量。

main函数之前的启动过程

image.png
1. 加载dyld到App进程

从Mach-o文件中读取dylb的路径并加载到App进程

2. 加载动态库(包括所依赖的所有动态库)

dyld会首先加载Mach-O中的 header 和 load command
接着就知道了这个app所依赖的动态库。添加依赖的动态库会按照先填加第一个依赖的动态库A,然后A所依赖的所有动态库,这样递归,直到所有的动态库添加完毕。通常一个App所依赖的动态库有100-400个。大多都是系统的动态库。它们会被缓存到dyld shared cache中,这样大大提高了读取效率。

查看mach-o文件所依赖的动态库,可以通过MachOView的图形化界面(展开Load Command就能看到),也可以通过命令行otool。

otool -L xxxx 

xxxx:
    @rpath/PullToRefreshKit.framework/PullToRefreshKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1444.12.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    @rpath/libswiftCore.dylib (compatibility version 1.0.0, current version 900.0.65)
    @rpath/libswiftCoreAudio.dylib (compatibility version 1.0.0, current version 900.0.65)
    //...
3. Rebase && Bind

有两种主要的技术来保证应用的安全:ASLRCode Sign

ASLR 的全称是 Address space layout randomization,翻译过来就是 “地址空间布局随机化”App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。
由于 ASLR 的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。

mach-o中有很多符号,有指向当前mach-o的,也有指向其他dylib的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?

mach-o中采用了 PIC 技术,全称是Position Independ code。当你的程序要调用printf的时候,会先在__DATA段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分:

  • Rebase修复的是指向当前镜像内部的资源指针

Rebase 修正内部(指向当前mach-o文件)的指针指向。是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中

  • Bind指向的是镜像外部的资源指针。

Bind 修正外部指针指向。外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的

image.png

优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:

减少Objc类数量, 减少selector数量
减少C++虚函数数量
转而使用swift struct(其实本质上就是为了减少符号的数量)

4. ObjC setup
  • 读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
  • 注册 Objc 类,ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中;
  • 读取 protocol 以及 category 的信息,把category的定义插入方法列表 (category registration),
  • 确保 selector 的唯一性
5. Initializers

接下来就是必要的初始化部分了,主要包括几部分:

  1. Objc的+load()函数
  2. C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
  3. 非基本类型的C++静态全局变量的创建。(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度

这里要提一点的就是,+load方法已经被弃用了,如果你用Swift开发,你会发现根本无法去写这样一个方法,官方的建议是实用initialize。区别就是,load是在类装载的时候执行,而initialize是在类第一次收到message前调用。

所以,main()函数之前耗时的影响因素

  • 动态库加载越多,启动越慢。
  • ObjC类越多,启动越慢
  • C的constructor函数越多,启动越慢
  • C++静态对象越多,启动越慢
  • ObjC的+load越多,启动越慢

pre-main的优化

  1. 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
  2. 检查下 framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
  3. 合并或者删减一些OC类和函数;关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高);有一个叫做FUI
    的开源项目能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。
  4. 移除不需要用到的, 减少selectorcategory的数量,如合并功能类似的类和Category
  5. 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
  6. dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;

减少调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数);

main()阶段

主要工作就是初始化必要的服务,显示首页内容等。而我们的优化也是围绕如何能够快速展现首页来开展。简要来说,只需要关注这个didFinishLaunchingWithOptions方法即可。
其实在这方法里面,我们主要是初始化第三方sdk项目配置设置根视图控制器等。

查看Main()函数后的花费时间

  1. 我们可以借助打点计时器BLStopwatch来度量didFinishLaunchingWithOptions每行代码的初始时间。
例如:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
   NSLog(@"didFinishLaunchingWithOptions 开始执行");
   self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
   self.window.backgroundColor = [UIColor whiteColor];
   UITabBarController *tabVC = [[UITabBarController alloc] init];
   self.window.rootViewController =  tabVC;
   [self.window makeKeyAndVisible];
    
   BLStopwatch *timer = [BLStopwatch sharedStopwatch];
   [timer start];
   [self initShareSDK];
   [timer splitWithDescription:@"初始化分享SDK"];
   [timer stop];
   NSLog(@"%@",timer.prettyPrintedSplits);
   // #1 初始化SDK: 0.523  这是个假设时间
   return YES;
}
  1. 定时抓取主线程方法调用堆栈,计算一段时间里的方法耗时。(Xcode中的Time Profiler就是使用的这种的方法)
  2. objc_msgSend方法进行hook,来得到所有方法的耗时。

注:hook是指在原有方法开始执行时,换成你指定的方法(用RuntimeMethod Swizzle / Facebook开源的fishhook框架)。或在原有方法的执行前后,添加执行你指定的方法。从而达到改变指定方法的目的。
(PS:关于fishhook,推荐阅读一篇博客:fishhook原理)

总结来说,就是必须一进app就必须初始化和可以延迟初始化的。就上例而言,初始化一个SDK需要耗时0.5s,如果在该SDK 不是非必须初始化,可以放HomeVC的viewdidLoad或者viewDidAppear去做又或者需要用到才初始化。

* 日志、统计等必须在 APP 一启动就最先配置的事件
* 项目配置、环境配置、用户信息的初始化 、推送、IM等事件
* 其他 SDK 和配置事件

main()优化

1、展示的首页尽量用纯代码创建,结合缓存更加。
2、结合BLStopwatch对启动服务进行分级分时
3、对一些非必要的初始化操作,可以放到viewDidAppear,因为到viewDidAppear开始执行的时候,用户已经看到了APP的首屏,即宣告启动结束
4、一般仅针对测试版本进行log打印
5、对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载
6、优化主线程耗时操作,子线程执行,防止屏幕卡顿。
7、对于启动页的网络请求接口可以做合并,减少请求

总之:性价比最高的优化阶段就是main函数之后的一些逻辑整理,尽量将不需要的耗时操作延迟到首屏展示之后执行。

深入理解iOS App的启动过程

你可能感兴趣的:(iOS 启动优化(一))