一些概念
Mach-O是运行时产生的可执行文件的文件类型
-
Image:
- Executable:程序的主二进制文件
- Dylib:动态库
- Bundle:是一种特殊的Dylib,是不能进行链接的,只能在运行时用dlopen()函数打开。
-
Framework:Dylib+储存该Dylib需要的资源、头文件的目录结构
Mach-O Image File被分割成几个段
引用自WWDC2016-
_TEXT
:头文件,代码,只读常量 -
_DATA
:所有可读写内容(全局变量、静态变量等等) -
_LINKEDIT
:储存关于如何加载程序的“元数据”
每个段都是page size的倍数,图中
_TEXT
占有三个page,arm64一个page size是16KB,其它的是4KB
虚拟内存把每一个进程地址映射到物理内存RAM中:
- page错误
- 多个进程中出现的相同的RAM page
- 文件回溯page:mmap()、延迟读取(lazy reading)
- Copy-On-Write(COW)
- 脏page和干净page
- Permissions:rwx
安全:
ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。
ASLR:
- 地址空间随机布局
- 镜像(Images)加载在随机的地址
代码签名
- 每一个page拥有的内容
- page内的哈希验证
exec()到main(),内核让你的App在随机的地址开始运行
什么是Dyld?
- Dyld是动态加载器,内核加载的辅助程序
- 程序从Dyld开始执行
- Dyld运行在进程中
- Dyld负责加载动态依赖库
-
Dyld拥有与App相同的权限
Main()函数之前的五个阶段
Dyld主导的五个阶段:
引用自WWDC2016一、Load dylibs:
动态映射所有的动态依赖库。
- 解析动态依赖库(dylib)的列表
- 找到所有需要的mach-o(dylib)文件
- 打开并读取每一个找到的文件
- 验证这些文件是不是mach-o文件
- 找到它的编码签名,在内核里对它进行注册
- 给每一个分段调用映射
现在,所有App指向的动态依赖库都被递归加载,App通常需要加载100-400个动态库,大多数是OS(操作系统)的动态库
二、Rebase
遍历所有内部数据指针为他们添加一个滑动值。这些指针的位置都被编码在
__LINKEDIT
段里。
- 调整所有镜像内的指针,添加一个slide偏差值。
Slide = actual_address - preferred_address
- 出于安全考虑,引入了 ASLR,全称
Address Space Layout Randomization
。
大概意思就是镜像(dylib)会加载在随机的地址上,和actual_address会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。
- Rebase+Bind+ObjC大多数时间在做修复:
- 代码签名意味着命令不能被修改
- 代码不能被加载到任何地址上,而且永远不能被修改
- 所有的修复都发生在
__DATA
数据段
三、Bind
遍历查询符号表,设置指向镜像外部的指针。
- 所有在其它动态库引用的东西都会符号化
- Dyld需要找到所有符号名
- 会比Rebasing进行更多的计算
四、ObjC
通知 runtime 准备镜像、OC类的注册、偏移
ivar
的地址、加载Category
- runtime要维护一张表,包含所有类名(
Class Name
)及其映射的类(Class
) - 完成所有OC类的定义注册
- 运行时改变所有
ivar
的偏移量 - 接下来在ObjC阶段可以定义分类(
Category
) - 最后让
Selector
都是唯一的
五、Initializers
-
dyld开始调用C++静态构造函数,初始化器用来初始化那些抽象DATA
-
调用所有类的
+load
方法,顺序:父类->子类->Category -
每个Initializers按照从下向上的顺序执行。
为什么从下向上?
因为当Initializers运行时,可能会调用一些dylib,我们需要确保那些dylib已经准备好被调用,所以从下开始运行Initializers,一直往上到应用类,可以很安全的调用依赖的内容。
-
最后Dyld调用main()函数
小结:Dyld是一个辅助程序
- 加载所有的依赖库
- 修复所有DATA page中的指针
- 运行所有的初始化器(Initializers)
- 跳转到主函数
优化启动时间的实践部分
- 启动时间如果在400ms(0.4s)以内会让用户觉得启动快
- 启动时间千万不要超过20s,否则OS将会认为你的APP进入死循环,杀死APP
App启动都做了什么?
在main函数之前的五个阶段之后,还要调用:
-
main()
-
UIApplicationMain()
: 加载framework的初始化器,加载nibs -
applicationWillFinishLaunching
以上8个步骤都算在这400ms内。
热启动和冷启动
- 热启动:App及其数据已经在内存(磁盘)中
- 冷启动:App不在内核的缓存中
优化方案:
一、Load Dylib阶段
-
合并已有的动态库(包括framework),限制动态库(包括framework)个数效果非常明显
-
使用静态库代替
-
可以使用延迟加载,也就是使用dlopen()函数,但是dlopen()会带来细微的性能和正确性的问题。
优化前的链接库个数
优化前的启动时间
优化后的链接库个数原有26个framework合并成2个后,由240ms变为21ms。
优化后的启动时间
二、Rebase和Bind阶段
-
减少OC元数据
-
减少OC类的数量(不鼓励使用很多很小的类,只有一两个方法的那种)
-
减少
selector
的数量 -
减少Category的数量
-
-
减少C++虚拟函数,虚拟函数创建被称作 V表格,和OC元数据相同
-
避免让机器生成过多的代码,机器生成的指针非常耗内存
- 使用偏移量代替指针
-
标记为只读
三、ObjC阶段
这个阶段的优化工作已经在Rebase和Bind阶段做完了
四、Initializers阶段
有两种Initializers:显式和隐式
显式:
+load
方案:使用 +initiailize
代替
- C/C++的
__attribute__((constructor))
方案:使用site initializers
- 使用dispatch_once()
- 使用pthread_once()
- 使用std::once()
隐式:大部分是C++的全局变量带来的非默认初始化器
- 使用site initializers
- 只设置简单的值
- 不要在初始化器中调用dlopen()
-
不要在初始化器中创建线程
网友评论