OC底层探索(十三): 类的加载(一)
所用版本:
- 处理器: Intel Core i9
- MacOS 12.3.1
- Xcode 13.3.1
- objc4-838
熟悉类加载前, 先看下类的初始化方法_objc_init
( 留意看下下面的注释 ):
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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();
// 运行C++静态构造函数。
static_init();
// runtime运行时初始化
runtime_init();
// objc异常处理系统初始化
exception_init();
#if __OBJC2__
// 缓存初始化
cache_t::init();
#endif
// 启动回调机制
_imp_implementationWithBlock_init();
// dyld通知注册
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
[ environ_init() ] 环境变量初始化
再次运行可发现, 新增打印信息
可看到打印了很多相关环境变量, OBJC_PRINT_IMAGES
, OBJC_PRINT_CLASS_SETUP
, OBJC_DISABLE_NONPOINTER_ISA
等等。详细见: Xcode环境变量说明
[ tls_init ] 线程处理
针对本地线程处理做处理, 如果满足SUPPORT_DIRECT_THREAD_KEYS
析构, 不满足初始化
其中
// Thread keys 由libc保留供我们使用。
# define SUPPORT_DIRECT_THREAD_KEYS 1 - 满足 0 - 不满足
- 判断满足:
pthread_key_init_np
- 判断不满足:
tls_create
重新初始化个线程key
[ static_init ] 运行C++静态构造函数。
如果有C++静态构造函数
, libc会在dyld 调用_dyld_objc_notify_register
之前, 先调用static_init
执行。
举个例子, 我们写一个全局构造函数, 运行可发现
如图可看出, 在_dyld_objc_notify_register
之前如果有静态C++构造函数
, 那么通过static_init
方法直接运行。
[ runtime_init ] 运行时初始化。
可看出主要分对两部分, 分类初始化
、类的表初始化
进行
[ exception_init ] objc异常处理系统初始化
初始化libobjc的异常处理系统。其实是注册异常处理的回调,从而监控异常的处理
举个例子:
数组越界例子肯定会发生crash, 接着我们运行一下
先走了_objc_init
中的exception_init
执行old_terminate = std::set_terminate(&_objc_terminate);
, 留意下此时还没有执行_dyld_objc_notify_register
。
执行_dyld_objc_notify_register
→ main
执行_objc_terminate
最后crash
其实当 crash发生时,会走_objc_terminate
方法,接着走到uncaught_handler
, 扔出异常并传入一个参数(e)
, 而e
的回调往下看
e
= fn
可看出将objc_uncaught_exception_handler fn
(设置的异常) 赋值给uncaught_handler
, 即 uncaught_handler
等于 fn
, 由此可看出uncaught_handler
, 本质是一个回调函数。
如图,系统其实会针对crash进行拦截处理,app会抛出一个异常句柄NSSetUncaughtExceptionHandler
,传入一个函数给系统,当异常发生后,调用函数(函数中可以线程保活、收集并上传崩溃日志),然后回到原有的app层中,其本质是一个回调函数。
[cache_t::init()] 缓存初始化
[ _imp_implementationWithBlock_init ] 启动回调机制
[ _dyld_objc_notify_register ] dyld通知注册
首先可以看到_dyld_objc_notify_register(&map_images, load_images, unmap_image);
3个参数&map_images
、load_images
、unmap_image
-
&map_images
: 映射整个镜像文件, 管理文件中, 动态库所有符号 (class, Protocol, selector, category)
先留意下&
, 说明是指针传递
, 传递是一个函数。这里用指针传递的好处是为了让map_images
同步发生变化, 主要原因这个函数很重要, 苹果不希望它会因为一些重复加载发生错乱。同时这个映射操作也比较耗时, 如果不是一起的话, 也会增加耗时性。看下其内部
接下来看下map_images_nolock
内部
代码有点长, 直接看重点代码: 读取镜像_read_images
read_images
这个方法很重要, 先说下_read_images
都做了什么
_read_images
- 条件控制进行一次加载
- 修复预编译阶段的
@selector
混乱问题 - 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复消息
- 如果类里面有协议读取
- 分类处理
- 类的加载处理
- 优化类
接下来看下_read_images
底层实现, 并依次看下上面内容
① 第一次加载
略过一些代码看重点NXCreateMapTable
可看出第一次加载会创建一个表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
- NXStrValueMapPrototype: 开辟类型
- namedClassesSize: 开辟总容积
创建一张类的总表,这个表包含所有的类。4/3 因子
这个我稍微讲一下 , 先了解哈希表负载因子
一个概念
哈希表负载因子
-
负载因子
=总键值对数
/数组的个数
。 -
负载因子
是哈希表
的一个重要属性,用来衡量哈希表
的空/满程度,一定程度也可以提现查询的效率。负载因子
越大,意味着哈希表
越满,越容易导致冲突,性能也就越低。所以当负载因子大于某个常数(一般是0.75 即 3 / 4)时,哈希表
将自动扩容
。 -
哈希表
扩容时,一般会创建两倍于原来的数组长度,因此即使key
的哈希值
没有变化,对数组个数取余的结果会随着数组个数的扩容发生变化,因此键值对的位置都有可能发生变化,这个过程也成为重哈希
(rehash)。
那么回来再看下, 表的大小也遵循负载因子,这里 namedClassesSize = totalClasses * 4 / 3
相当于是负载因子``3/4
的逆过程。namedClassesSize相当于总容量,totalClasses相当于要占用的空间。
例如我们想创建一张表 , 总容积: totalClass = x * 4 / 3
开辟表大小 x = totalClass * (3 / 4) = x * (4 / 3) * (3 / 4) = x = namedClassesSize
- 先看下
gdb_objc_realized_classes
:
其实gdb_objc_realized_classes
是一张总表含所以类的表, 而runtime_init
中的allocatedClasses
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
可看出allocatedClasses
只是一个alloc的分表. gdb_objc_realized_classes
包含它。
② 修复预编译阶段的@selector
- sel是在dyld和llvm的时候加载的。
- sels[i]是从mach-o获取的 mach-o会有相对内存地址和偏移地址。
sel 会有 名字 + 地址, 有些时候名字可能相同但是地址不相同, 这个时候需要修复一下
其中_getObjc2SelectorRefs
是获取Mach-O
中的静态段__objc_selrefs
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
再看下sel_registerNameNoLock
方法
调成一致, 将SEL覆盖到中namedSelectors
集合Set对应位置上, 这里用Set原因: 虽然都是集合但是相比array, set处理hash方面效率确实是更高一些
举个例子: 比如你要存储元素A, 一个hash算法直接就能直接找到A应该存储的位置; 同样, 当你要访问A时, 一个hash过程就能找到A存储的位置. 而对于array,若想知道A到底在不在数组中, 则需要便利整个数组, 显然效率较低了;
综上: UnfixedSelectors修复sel
就是把相不同的@selector统一化, 同时要以dyld的sel
为准.
③ 错误/混乱类处理
主要是从Mach-O
中取出所有类,在遍历进行读取, 核心方法readClass
我们看下它的底层
[readClass]
先加一个打印, 看看都能读到什么类
printf("%s - Test - %s /n", __func__, mangledName);
可看出能把系统类和自定义类都读取到, 没有用到的自定义类也会读取, 自定义类后添加的先读取。接下来我们跟一下自定义类的流程
const char *SRTest = "SRTest";
// 是否匹配
if (strcmp(mangledName, SRTest) == 0) {
printf("%s - 当前类 - %s /n", __func__, mangledName);
}
运行发现SRTest
已进入
先走修正方法
如果类要求稳定, 那么会修正下不稳定的类
接下来跟流程可发现会走addNamedClass
[addNamedClass]
稍微看下addNamedClass
内部实现
addNamedClass
将当前类添加到之前创建好的gdb_objc_realized_classes
总表中
(之前有写, 往上翻第一次加载会创建一个表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
)
继续跟流程可发现走addClassTableEntry
[addClassTableEntry]
将类和元类插入allocatedClasses
表中。这张表是在runtime_init
中创建的。(之前也有写, 往上翻runtime_init
)
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
之后就会走readClass
中的return cls;
方法返回
综上,可看出readClass
的主要将Mach-O
中的类, 添加进内存 (插入到表中, 总表, alloc的分表都插一份)
共有 0 条评论