iOS底层 - 启动优化(上)

697 阅读7分钟

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. iOS底层 - @synchronized 流程分析
  31. iOS底层 - 锁的原理探索
  32. iOS底层 - 带你实现一个读写锁
  33. iOS底层 - 谈Objective-C block的实现(上)
  34. iOS底层 - 谈Objective-C block的实现(下)
  35. iOS底层 - Block, 全面解析!

以上内容的总结专栏


细枝末节整理


前言

开发完成一个APP之后,也上架了。那么用户在实际使用中对于你开发的APP的感受是如何的呢? 首相,APP响应很迅速、启动很快、几乎没有需要用户等待的操作 这基本可以说是一个比较完美的APP。那么,对于APP响应以及操作是否需要用户等待,这对于我们开发者来说是比较好控制的,然而,对于APP的启动,如何去优化,让我们的APP启动更快一点,今天我们开始逐步探讨一下。

启动概念

启动有两种定义:

  • 广义: 点击图标到首页数据加载完毕
  • 狭义: 点击图标到 Launch Image 完全消失第一帧

今天我们要重点关注的是以 mian 函数 为分界, 其之前我们称为 pre-main 阶段;之后,我们称为 main 函数之后。为什么我们这么划分呢? 因为 Xcode 给我们提供了一个很好的监测工具,它可以监测到 main 函数之前到耗时时间。 iOS 的项目 最早执行我们代码的地方是 load 方法, 在 load 方法之前 系统加载和链接绑定等操作 会有很多影响我们的启动时间,但是我们很难去监测。

那么, 这个工具就可以将 dyld 所耗时间,都反馈给我们。如何使用这个 监测工具呢?见下图(配置一个环境变量):

image.png

配置之后,连接真机,运行一下项目,这里以我的一个项目为例,会有如下日志内容:

image.png

  • dylib loading time:动态库载入耗时;
  • rebase/binding time:重定位、重绑定;
  • ObjC setup time:OC类注册(OC是一个动态语言,读取二进制的data内容,找到OC的相关的信息,注册OC类;OC的runtime需要维护一张映射表(sel 和 imp 的映射, 类与类名的全局表, 加载mach-O的时候,所有的类都需要加载到全局表中));
  • initializer time:执行load、构造函数耗时;

rebase/binding time

  • 编译时 链接(macho-O 告诉 dyld 我需要用到外面的一个库(可能需要Fundation库的api 就留好位置 等运行的时候绑定))
  • 运行时 绑定

虚拟内存

物理地址时代

早期的mach-O都是物理内存,也就是应用加载到内存之后的真实内存地址。

  • 这样会存在问题:
  • 1、内存很容易就不够用了;
  • 2、安全问题。

懒加载方式

系统工程师在优化的时候因为应用加载的时候,并不是一开始就需要全部的功能,所以将应用按需加载到内存中去,用户需要哪里再加载哪里?这样可以解决内存不够用的问题。但是又会造成 应用在 内存中地址的不连续的问题(会造成运行过程中的计算,因为应用的内存不是连续的所以需要计算,对于程序开发很是不方便)。

再一次,系统工程师们,就通过一个映射表来解决上面提到的问题(硬件上增加了 MMU 内存管理单元 - 翻译地址 )。 我们的应用只去找映射表的虚拟内存表(地址都是连续的),通过 MMU 翻译为物理内存地址,来优化系统。

为了提升翻译的效率,将内存 通过 分页(PAGE) 来 管理 (在iOS系统中 是 16K , Mac 中是 4K) 。

物理内存的地址通过操作系统来管理,应用只能操作虚拟内存地址;这样,内存不够用的问题就解决了, 安全问题也得到了解决。因为,应用只能访问到 它 的虚拟内存表, 物理内存被很好的隔离开来。而且,进程和进程之间也做到了安全隔离。

这个时候虚拟内存其实也是不安全的,因为每次加载引用程序虚拟内存都是从0开始的,我们计算好某个文件的偏移地址,每次都可以通过内存地址来访问。后来,操作系统通过 ASLR 来使每次生成的虚拟内存其实位置从一个随机值开始。( ASLR + offset(文件偏移) = rebase )

虚拟内存 - 物理内存.001.jpeg

当用户在使用中,使用到了进程1 的P2功能的时候,因为 P2 并没有加载在内存中, 操作系统会报 缺页异常(缺页中断), 将用户操作中断,接着,系统会将 P2 加载到 内存中去。 加载到哪一个内存中去,这是系统通过算法(页面置换),来覆盖掉不那么活跃的部分。

二进制重排

启动具有 局部特征性,即只有少部分函数在启动的时候用到,这些函数在二进制中的分布是零散的,所以 页面置换 读入的数据利用率并不高。如果我们可以把启动用到的函数排列到二进制的连续区间,那么就可以减少 页面置换 的次数,从而 优化启动时间。

以下图为例,方法 1 和方法 3 是启动的时候用到的,为了执行对应的代码,就需要两次 Page In。假如我们把方法 1 和 3 排列到一起,那么只需要一次 Page In,从而提升启动速度。

链接器 ld 有个参数-order_file 支持按照符号的方式排列二进制。

下一篇,我们就讲一下具体的 二进制重拍如何去做。