这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
- iOS底层 - @synchronized 流程分析
- iOS底层 - 锁的原理探索
- iOS底层 - 带你实现一个读写锁
- iOS底层 - 谈Objective-C block的实现(上)
- iOS底层 - 谈Objective-C block的实现(下)
- iOS底层 - Block, 全面解析!
以上内容的总结专栏
细枝末节整理
前言
开发完成一个APP之后,也上架了。那么用户在实际使用中对于你开发的APP的感受是如何的呢? 首相,APP响应很迅速、启动很快、几乎没有需要用户等待的操作 这基本可以说是一个比较完美的APP。那么,对于APP响应以及操作是否需要用户等待,这对于我们开发者来说是比较好控制的,然而,对于APP的启动,如何去优化,让我们的APP启动更快一点,今天我们开始逐步探讨一下。
启动概念
启动有两种定义:
- 广义: 点击图标到首页数据加载完毕
- 狭义: 点击图标到 Launch Image 完全消失第一帧
今天我们要重点关注的是以 mian 函数 为分界, 其之前我们称为 pre-main 阶段;之后,我们称为 main 函数之后。为什么我们这么划分呢? 因为 Xcode 给我们提供了一个很好的监测工具,它可以监测到 main 函数之前到耗时时间。 iOS 的项目 最早执行我们代码的地方是 load 方法, 在 load 方法之前 系统加载和链接绑定等操作 会有很多影响我们的启动时间,但是我们很难去监测。
那么, 这个工具就可以将 dyld 所耗时间,都反馈给我们。如何使用这个 监测工具呢?见下图(配置一个环境变量):
配置之后,连接真机,运行一下项目,这里以我的一个项目为例,会有如下日志内容:
- 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 )
当用户在使用中,使用到了进程1 的P2功能的时候,因为 P2 并没有加载在内存中, 操作系统会报 缺页异常(缺页中断), 将用户操作中断,接着,系统会将 P2 加载到 内存中去。 加载到哪一个内存中去,这是系统通过算法(页面置换),来覆盖掉不那么活跃的部分。
二进制重排
启动具有 局部特征性,即只有少部分函数在启动的时候用到,这些函数在二进制中的分布是零散的,所以 页面置换 读入的数据利用率并不高。如果我们可以把启动用到的函数排列到二进制的连续区间,那么就可以减少 页面置换 的次数,从而 优化启动时间。
以下图为例,方法 1 和方法 3 是启动的时候用到的,为了执行对应的代码,就需要两次 Page In。假如我们把方法 1 和 3 排列到一起,那么只需要一次 Page In,从而提升启动速度。
链接器 ld 有个参数-order_file 支持按照符号的方式排列二进制。
下一篇,我们就讲一下具体的 二进制重拍如何去做。