所用版本:
- 处理器: 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
}
_objc_init
[ environ_init() ] 环境变量初始化
打印准备
再次运行可发现, 新增打印信息
打印信息
可看到打印了很多相关环境变量, OBJC_PRINT_IMAGES, OBJC_PRINT_CLASS_SETUP, OBJC_DISABLE_NONPOINTER_ISA等等。详细见: Xcode环境变量说明
[ tls_init ] 线程处理
针对本地线程处理做处理, 如果满足SUPPORT_DIRECT_THREAD_KEYS析构, 不满足初始化
tls_init
其中
// Thread keys 由libc保留供我们使用。
# define SUPPORT_DIRECT_THREAD_KEYS 1 - 满足 0 - 不满足
- 判断满足:
pthread_key_init_np
pthread_key_init_np
- 判断不满足:
tls_create
tls_create
重新初始化个线程key
[ static_init ] 运行C++静态构造函数。
如果有C++静态构造函数, libc会在dyld 调用_dyld_objc_notify_register之前, 先调用static_init执行。
static_init
举个例子, 我们写一个全局构造函数, 运行可发现
全局构造函数
打印结果
如图可看出, 在_dyld_objc_notify_register之前如果有静态C++构造函数, 那么通过static_init方法直接运行。
[ runtime_init ] 运行时初始化。
runtime_init
可看出主要分对两部分, 分类初始化、类的表初始化进行
[ exception_init ] objc异常处理系统初始化
初始化libobjc的异常处理系统。其实是注册异常处理的回调,从而监控异常的处理
exception_init
举个例子:
例子
数组越界例子肯定会发生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的回调往下看
`uncaught_handler `
e = fn
uncaught_handler
可看出将objc_uncaught_exception_handler fn(设置的异常) 赋值给uncaught_handler, 即 uncaught_handler 等于 fn, 由此可看出uncaught_handler, 本质是一个回调函数。
应用级crash
如图,系统其实会针对crash进行拦截处理,app会抛出一个异常句柄NSSetUncaughtExceptionHandler,传入一个函数给系统,当异常发生后,调用函数(函数中可以线程保活、收集并上传崩溃日志),然后回到原有的app层中,其本质是一个回调函数。
[cache_t::init()] 缓存初始化
缓存初始化
[ _imp_implementationWithBlock_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
接下来看下map_images_nolock内部
map_images_nolock
代码有点长, 直接看重点代码: 读取镜像_read_images
_read_images
read_images这个方法很重要, 先说下_read_images都做了什么
_read_images
- 条件控制进行一次加载
- 修复预编译阶段的
@selector混乱问题 - 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复消息
- 如果类里面有协议读取
- 分类处理
- 类的加载处理
- 优化类
_read_images
接下来看下_read_images底层实现, 并依次看下上面内容
_read_images
① 第一次加载
doneOnce
略过一些代码看重点NXCreateMapTable
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
其实gdb_objc_realized_classes是一张总表含所以类的表, 而runtime_init中的allocatedClasses
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
allocatedClasses
可看出
allocatedClasses只是一个alloc的分表. gdb_objc_realized_classes包含它。
② 修复预编译阶段的@selector
修复预编译阶段的@selector
- sel是在dyld和llvm的时候加载的。
- sels[i]是从mach-o获取的 mach-o会有相对内存地址和偏移地址。
sel 会有 名字 + 地址, 有些时候名字可能相同但是地址不相同, 这个时候需要修复一下
地址不相同
其中_getObjc2SelectorRefs是获取Mach-O中的静态段__objc_selrefs
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
再看下sel_registerNameNoLock方法
sel_registerNameNoLock
__sel_registerName
调成一致, 将SEL覆盖到中namedSelectors集合Set对应位置上, 这里用Set原因: 虽然都是集合但是相比array, set处理hash方面效率确实是更高一些
举个例子: 比如你要存储元素A, 一个hash算法直接就能直接找到A应该存储的位置; 同样, 当你要访问A时, 一个hash过程就能找到A存储的位置. 而对于array,若想知道A到底在不在数组中, 则需要便利整个数组, 显然效率较低了;
综上: UnfixedSelectors修复sel就是把相不同的@selector统一化, 同时要以dyld的sel为准.
③ 错误/混乱类处理
混乱类处理
主要是从Mach-O中取出所有类,在遍历进行读取, 核心方法readClass
我们看下它的底层
[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已进入
运行
先走修正方法
fixupBackwardDeployingStableSwift
如果类要求稳定, 那么会修正下不稳定的类
fixupBackwardDeployingStableSwift
接下来跟流程可发现会走addNamedClass
走addNamedClass
[addNamedClass]
稍微看下addNamedClass内部实现
addNamedClass
addNamedClass
addNamedClass将当前类添加到之前创建好的gdb_objc_realized_classes总表中
(之前有写, 往上翻第一次加载会创建一个表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);)
继续跟流程可发现走addClassTableEntry
[addClassTableEntry]
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的分表都插一份)
readClass











网友评论