iOS启动优化之实践篇

1,323 阅读4分钟

扫一扫关注公众号,获得更多iOS相关内容


这篇文章会给大家介绍一下关于启动优化,我们可以从哪些方面入手,而不会讲具体的实现,具体的实现会在后面其它专题的文章中陆续给大家引出来。

这篇文章所提的启动都是冷启动。

为什么要进行启动优化

在进行启动优化之前,我们首先要知道为什么要进行启动优化,相信看了下面这组数据,你就会意识到启动优化的重要性:

  • iOS应用启动时间一旦超过20s,系统会认为发生了死循环并杀掉App进程。
  • 互联网领域八秒定律是指用户访问一个网站时,如果等待网页打开的时间超过8秒,会有超过70%的用户放弃等待。
  • 如果应用启动时长超过3秒,会有70%的用户直接卸载应用。

启动时间的计算方式

iOS冷启动过程为:从用户点击App图标开始到appDelegate didFinishLaunching方法执行完成为止。这个过程主要分为两个阶段:

  • main()函数之前:即操作系统加载App可执行文件到内存,然后执行一系列的加载、链接等工作,最后执行至App的main()函数。
  • main()函数之后:即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕。
启动时间(T)= main()函数执行之前的时间(T1)+ main()函数执行之后的时间(T2).

如何测量启动时间

测量main()函数之前的时间

dyld里有内置的测量系统,可以通过设置环境变量访问:

测量main()函数之后的时间

这段时间我们可以通过插入代码来测量。

  • 先在main()函数里用变量StartTime记录当前时间:
CFAbsoluteTime StartTime;

int main(int argc, char * argv[]) {
    StartTime = CFAbsoluteTimeGetCurrent();
    ...
}
  • 在AppDelegate.m文件中用extern声明全局变量StartTime
extern CFAbsoluteTime StartTime;
  • 在didFinishLaunchingWithOptions里,获取一下当前时间,与StartTime的差值即是main()函数之后的运行时间。
double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);

开始启动优化

启动速度要多快

苹果的经验法则告诉我们:400ms是一个不错的启动时间。 **注意:**在测量启动时间时,需要在最慢的支持设备上进行测试。

main()函数之前

先回顾下Dyld的加载步骤:

接下来我们从这几个步骤来分析下有哪些可以进行启动优化的点

Load dylibs

平均每个应用会包含100到400个dylib,但是系统的dylibs非常快。 加载内嵌的dylib非常昂贵。

优化方案:依赖的dylib越少越好。

  • 尽量不使用内嵌的dylib。
  • 合并已有的dylib。
  • 使用dlopen()函数懒加载dylib,不建议用。因为,dlopen()会带来细微的性能和正确性的问题,实际在之后会带来更多的工作量。
Rebase & Bind

Rebase由于有许多I/O操作,会慢一些,而Bind在计算上会昂贵一些。其时间都消耗在修复__DATA段里的指针上。

优化方案:减少修复指针的数量

  • 减少ObjC类(class)、方法(selector)、分类(category)的数量。
  • 减少C++虚函数的的数量。
  • 使用Swift structs,Swift语言内部做了优化,符号量更少且更内联。
ObjC

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

优化方案:同Rebase & Bind

initializers

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

优化方案

  • 少使用+load方法
    • 尽量把这些事情推迟到+initiailize
    • 将+load方法的加载与执行分离,加载放到main之前执行大概耗时5毫秒,执行放到main之后的一个合适的时机。后面,我会专门写一篇文章来介绍这个方案。
  • 减少构造器函数个数,在构造器函数里少做些事情。
  • 减少C++静态全局变量的个数。

main()函数之后

这一阶段要根据具体的业务进行优化,这里不做过多阐述,后面的文章中也会有相关内容,记住指导原则:在满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好。