iOS启动速度优化,看这一篇就够了

参考:
[mach-o]: Mac OS X ABI Mach-O File Format Reference
[mach-o]: https://juejin.im/post/5ab47ca1518825611a406a39
[启动优化] https://blog.csdn.net/hello_hwc/article/details/78317863
[启动优化] https://cloud.tencent.com/developer/article/1449880
[dyld3]: https://easeapi.com/blog/blog/83-ios13-dyld3.html
[dyld2]: https://github.com/opensource-apple/dyld
[dyld]: https://juejin.im/post/5e12ce8a5188253a6647c900?utm_source=gold_browser_extension
[attribute(constructor)] https://www.jianshu.com/p/dd425b9dc9db
[c++虚函数] https://blog.csdn.net/lyztyycode/article/details/81326699
[c++虚函数] http://c.biancheng.net/view/2294.html

Mach-o文件包含以下三类:
1.executable可执行文件
2.dylib动态库
3.bundle无法被链接的动态库,只能在运行时通过dlopen()加载

Image指的是Executeable、 dylib、 bundle中的一种
苹果的操作系统可执行文件格式几乎都是mach-o,ios也是。

mach-o大致分为以下三部分:
1.header:包含可执行的cpu框架,比如x86, arm64
2.load commands:加载命令,包含文件的组织架构和虚拟内存中的布局方式
3.data:包含第二部分中需要的各个segment
*data部分主要包含三个segment:
1._text:代码段-(只读)包括函数、只读字符串
2._data:数据段-(读写)可读写的全局变量等
3._linkedit:包含方法&变量的元数据(位置、偏移)、代码签名等信息

DYLD(dyld: the dynamic link editor):
动态库加载器,加载一个进程所需要的image

虚拟内存Virtual Memory
这里说的虚拟内存是在物理内存上建立的一个逻辑地址空间;
向上提供一个连续的地址空间,向下隐藏了物理内存的细节;
虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应一个物理地址;
虚拟内存被划分为一个个的大小相同Page(64位为16K),32位(4K?),提高了管理和读写效率;

  • 虚拟内存建立在物理内存与进程之间,在ios上,当内存不足时,释放那些只读的Page,因为只读
    Page在下次访问时可以再次从磁盘读取;
    *如果没有可用内存,通知后台APP,(后台app会受到memory warning)如果之后还是没有可用内存
    则杀死后台APP

Page fault[二进制重拍技术是这里的优化]
在应用执行过程中,他被分配的逻辑内存地址空间都是可以被访问的。当app访问一个逻辑Page,而在对应的
物理内存不存在的情况下,就会发生一次Page fault
当Page fault发生时,会中断当前的程序,在物理内存中找一个可用的Page,然后从磁盘中读取数据到物理内存
然后继续执行当前程序

Drity Page & Clean Page
如果一个page可以从磁盘上重新生成,这个Page就是 clean page
如果一个page包含了进程相关信息,那么这个Page就是dirty page
*_text代码段 这中segment就是 clean page
*_data数据段这种可读写的Page当发生了写操作那么他就是dirty page

启动过程:
dyld -> load dylibs -> rebase -> bind -> objc -> initializers

1.加载动态连接器(dyld)到app进程
2.使用dyld递归加载其他的dylibs
dyld首先读取mach-o文件中load commands 和 header
然后就知道这个可执行文件所依赖的动态库,然后就可以递归加载
通常一个app所依赖的动态库大约在400左右,大多数为系统的动态库,他们这些动态库会被缓存到dyld shared cache
这类库效率较高
3.rebase & bind
why:
首先保证app安全有两种技术:
1.ASLR和code sign
ASLR :(address space layout randomization)
地址空间布局随机化,app被启动的时候,程序会被映射到逻辑的地址空间,这个逻辑的地址空间有一个随机的地址,而ASLR就是
让这个开始地址随机化的技术,如果是固定的那么心怀不轨的人就可以通过起始地址 + 偏移地址的方式找到函数的地址

Code sign,在进行code sign的时候,加密哈希不是针对整个文件,而是针对每一个Page,这样子就保证了dyld进行加载的时候,可以
对每一个Page进行验证

2.mach-o中有很多符号,有指向当前mach-o的,有指向其他dylib的,比如printf,在运行时,如果准确找到printf的地址
mach-o中采用了PIC技术(position independ code),当你的程序要调用printf的时候,会先在_data中建立一个指针指向printf
在通过这个指针实现间接调用。 dyld需要做一些fix-up工作,帮助app找到这个符号的实际地址
主要分为两部分:
1.rebase-修正内部(指向当前mach-o文件)的指针指向
2.binding(修正外部指针指向)

之所以需要rebase;
ASLR技术使得开始地址随机化,同时code sign导致不能直接修改image,rebase只需要增加对应的偏移量即可
rebase是解决内部符号引用问题。外部符号问题使用bind解决,他是根据字符串匹配来进行符号表查找,所以bind更慢。

4.objc runtime
oc是一个动态化语言,在执行main函数之前,需要吧类的信息注册到一个全局的table中;同时
oc支持category在初始化的时候需要将category中方法注册到对应类中,同时会唯一Selector
(category实现了类中同名方法,类中的方法会被覆盖)

5.Initializers
必要的初始化部分
a. +load()方法
b. c/c++静态初始化对象和标记为_attribute_(constructor)的方法 [此关键字意思为在main函数之前做一些操作]

  • +load()已被废弃,swift中没有,官方建议使用initialize,load是在类装载的时候执行,
    initialize则是在类第一次收到message前调用

=以上讲解的是dyld2 最新的dyld3如下图所示===

iOS启动速度优化,看这一篇就够了
dyld2是纯粹的in-process,所有的操作在app呗启动时候执行
dyld3部分out-of-process,部分in-process,上图中虚线以上的都是 out-of-process;在app
下载和版本更新的时候会执行。
out-of-process会做如下事情:
1.分析mach-o headers
2.分析依赖的动态库
3.查找需要rebase & bind 之类的符号
4.将上述结果写入缓存

  • 这个dyld3 是在ios13中全面引入替换了dyld2;所以ios13相同条件APP启动速度更快
  • dyld3这个库在ios11就已经引入到系统中,只不过对第三方app不起作用
  • dyld2开源了,dyld3并为开源

启动时间
冷启动 & 热启动
1.刚启动过app,这时候app所需要的数据依旧在缓存中,再次启动时候被称为热启动,否则就是冷启动
云+测试: 2020年1月7日,iphone6 plus 冷启动大约需要1.2S, 热启动大约需要600MS左右
观测属性设置:Xcode-(ios9以上)
DYLD_PRINT_STATISTICS
DYLD_PRINT_STATISTICS_DETAILS

//优化顺序建议先优化业务面(main函数开始与之后)

pre-main阶段优化处理:
1、内核加载可执行文件 [无需处理]

2、load dylibs image (加载程序所需的动态库镜像文件)
优化点:
1.pod、carthage管理的所有库使用静态库处理
2.排查项目中使用到的framework(xcode-setting中排查不适用的framework删除之)

3、Rebase image / Bind image (由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内 存中的加载地址每次启动都不固定,所以需要修复镜像中的资源指针)

4、Objc setup (注册Objc类、将Category中的方法插入方法列表)
优化点:
1.减少category使用,是否可合并同类下的不同category
2.脚本扫描没有用到的objc类(自研或者github)
3.减少-data数据段中的数据量
4.多用swift struct
5.减少__atribute__((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化

5、initializers (调用Objc类的+load()方法、调用C++类的构造函数)
优化点:
1.减少hook函数在 +load 中的处理(1.减少莫名第三方库的引入,很多第三方库会hook一些vc的生命周期函数,例如bugly);自研得一些hook类型操作尽量将hook操作方法其他函数中如 +intilization方法中
2.尽量不要用C++虚函数(创建虚函数表有开销),不要在C++构造函数中做大量耗时操作
* c++中虚函数(表)作用就是实现多态的技术手段;父类有一个子类,如果类中成员函数不使用virtual(虚函数)表示,那么多态后调用的子类函数依旧为父类函数

main()函数开始与首页vc-viewdidappear之前的优化处理:

1.将不需要finishlaunch处理的服务放到首页展示之后再处理;
优化:
1.除bugly和激光服务需要放到didfinishlaunch方法内,其他服务按需放到首屏展示后
* 2.bugly也可以延后处理

2.业务、代码、数据方面的优化
优化:
1.使用Xcode的Instruments的Time-Profiler工具,分析启动过程中比较耗时的方法和操作,然后,进行具体的优化。
2.重点关注TabBarController和首页的性能,保证尽快的能展示出来。这两个控制器及里边的view尽量用代码进行布局,不使用storyboard和xib,如果在布局上想更进一步的优化,那就连autolayout(Massonry)都不要使用,直接使用frame进行布局。
3.本地缓存。首页的数据离线化,优先展示本地缓存数据,等待网络数据返回之后更新缓存并展示。
4.uitabbarvc 中只需要初始化第一个需要展示的vc即可,其他的用一个空vc做占位就行,真正点击或者第一页面didappear之后再加载即可