所用版本:
- 处置惩罚器: 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是从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的分表都插一份)
|