_objc_init:初始化流程
- _objc_init 源码
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
我们先不要着急分析流程,看到最后一行代码:_dyld_objc_notify_register
。这个很明显是 _dyld
里面的一个方法,上一篇文章中分类了 _dyld
的加载流程。
所以这里从 _dyld
中将这个方法的源码拿了过来,下面是 _dyld_objc_notify_register
的源码
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
……
}
源码很简单,仅仅是记录了这三个方法的地址,在需要的时候进行一个调用。在我们平时开发中,需要用到地址传递的时候,一般是需要修改这个变量的值,那么可以猜测这个方法的作用是将 _dyld
中的 mappedImage
回传给 objc
,这里的对内容放在后面分析,下面对这些初始化方法进行一个简单的介绍。
environ_init();
读取影响运行时的环境变量。如果需要,可以打印环境变量帮助。
1. 在终端中输入 export OBJC_HELP=1
可以输出所有环境变量
如果没有输出,尝试再执行一个 open
命令
2. 在Xcode 源码中输出环境变量
在这里手动修改:PrintHelp=true;
3. OBJC_DISABLE_NONPOINTER_ISA 环境变量
-
设置
OBJC_DISABLE_NONPOINTER_ISA
为YES
设置之后的打印出来的isa结果
设置之前的isa结果
因为系统做了优化,对isa联合体做了平移操作,所以会不一样
OBJC_PRINT_LOAD_METHODS
4. 环境变量 OBJC_PRINT_LOAD_METHODS
-
设置 OBJC_PRINT_LOAD_METHODS 为 YES
-
打印所有的 +load 方法
作用:可以对这些方法做方法优化,避免全局搜索找到的load方法(可能没有用到)
tls_init();
关于线程的绑定,比如每个线程数的析构函数
static_init();
- 运行C++ 的静态构造函数。
- 在 _dyld 调用我们自己的静态构造函数之前调用
-
libc
会调用_objc_init()
,所以我们必须自己做。
runtime_init();
是空实现!就是说 objc 的锁是完成采用C++那一套的, oc中不需要做任何处理。
exception_init();
初始化 libobjc 的异常处理系统。比如监控下一行注册异常的回调的代码。
cache_init();
_imp_implementationWithBlock_init()
_dyld_objc_notify_register()
仅供 objc 运行时使用
注册处理程序,以便在映射、取消映射和初始化 objc-image 时调用
Dyld将使用包含 objc-image-info 的景象文件的数组,回调 mapped 函数
- 研究对象
- map_images(重点)
- load_images(重点)
- unmap_image:主要做卸载相关的操作,不做重点研究
_objc_init:map_images 流程
read_images 注释
/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
先进入 map_images
,这个函数的注释说“处理给定的镜像文件(在dyld中映射进去的镜像)”,这句注释正好验证了我们上面的猜测——registerObjCNotifiers
方法的作用是将 _dyld
中的 mappedImage
回传给 objc
。
下面来分析 map_images_nolock
方法的流程
read_images 要读懂的问题
- 我们的dyld主题的思维是加载库-镜像文件,但是镜像文件怎么读取的?
- 我们的macho里面的数据怎么到我们的内存?
- 有没有不在macho里面的数据同样也可以在内存找到?
- sel方法编号的加载?
- 什么是懒加载类和非懒加载类?
- 类是如何加载实现的 -ro-rw 的关系?
- 协议&分类里面的数据是如何加载的?
read_images 初体验
在dyld的源码中去查找_dyld_objc_notify_register方法实现
1. 如何开展dyld研究
- 看注释 “// record functions to call”——记录被调用的函数
- 可以看出来,如果要研究这三个参数,和明显要研究这三个函数被调用的地方
- 以第一个参数为例,找到第一个函数被调用的地方
2. 在objc源码中分析,定位到_read_images方法
-
_objc_init->map_images
-
map_images->map_images_nolock
-
map_images_nolock->_read_images
3. 怎么分析 _read_images方法
这个方法有400多行,逐行读肯定不行
我们先将所有有注释的代码块折叠起来
-
方法的准备条件
-
方法的流程
我们可以看到每个代码块都有很标准的注释和日志输出,整个流程一目了然:
- 加载所有类到类的
gdb_objc_realized_classes
表中 - 对所有类做重映射
- 将所有SEL都注册到
namedSelecotors
表中 - 修复函数指针遗留
- 将所有
Protocol
都添加到protocol_map
表中 - 对所有
Protocol
做重映射 - 初始化所有非懒加载的类,进行
rw、ro
等操作 - 遍历已标记的懒加载的类,并做初始化操作
- 处理所有
Category
,包括Class
和Meta Class
- 初始化所有未初始的类。
- 加载所有类到类的
4. 表的介绍
-
gdb_objc_realized_classes
:无论是否实现,只要不在dyld
共享缓存中的已命名类的表 -
allocatedClasses
:通过objc_allocateClassPair
已分配的所有类(和元类)的表 -
namedSelecotors
:方法编号的表 -
protocol_map
:协议的表
题外话
- 为什么类的开头是 NS
NextStep的简称
乔帮主曾经被苹果踢出去了,然后又回来了 - NX开头的又代表什么含义
NX表示一种CPU的计数,“禁止执行的意思”
read_images 流程分析
第一流程:查找类。修复未解决的future类。标记 bundle类。
从编译后的类列表中取出所有类, 获取到的是一个
classref_t
类型指针
classref_t *classlist = _getObjc2ClassList(hi, &count);
-
遍历数组中会去除
OS_dispatch_queue_concurrent
OS_xpc_object
NSRunloop
等系统类, 例如CFFoundation
libdispatch
中的类, 以及自己创建的类.通过readClass函数获取处理后的新类, 内部主要操作 ro 和 rw 结构体
-
初始化所有懒加载的类需要的内存空间,现在数据没有加载
第二流程:修复类的重映射(一般不会走进来)
注释
- 类列表和非懒加载类类表保持未重映射
- 重映射类和super类,用于消息分发
- 将未映射class和super class重映射,被remap的类都是非懒加载类
第三流程:修复SEL引用
将所有 SEL 都注册到 namedSelecotors
表中
第四流程:修复旧的objc_msgSend_fixup调用站点,有条件才进来,不做分析
第五流程:查找协议protocols。修复协议protocols引用
从 _getObjc2ProtocolList
读协议存到 protocol_map
中
第六流程:修复协议的引用
预先优化的图像可能已经在正确的位置了,但并不能确定。所以需要重新映射协议的引用
第七流程:实现非懒加载类(重点流程)
- 实现了load方法的类
- 静态实例变量的类(常见的是单例)
第八流程:实现未来类。(条件依赖第一个流程)
第九流程:查找分类。(默认找的是懒加载分类-实现了load)
- 注册分类并关联目标类
- 重新构建类的方法列表(attachLists流程)
因为分类的方法也是放到了类的方法列表里面,而且是通过 attachLists
流程,插入了方法列表的前面,所以会造成分类方法“覆盖”主类方法的现象。
第十流程:实现所有未实现的类