OC底层探索(十三): 类的加载(一)

源代码 2024-9-20 10:16:42 93 0 来自 中国
所用版本:

  • 处置惩罚器: 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} 1.png [ environ_init() ] 情况变量初始化

2.png 再次运行可发现, 新增打印信息

可看到打印了许多干系情况变量, OBJC_PRINT_IMAGES,  OBJC_PRINT_CLASS_SETUP, OBJC_DISABLE_NONPOINTER_ISA等等。详细见: Xcode情况变量阐明
[ tls_init ] 线程处置惩罚

针对当地线程处置惩罚做处置惩罚, 假如满意SUPPORT_DIRECT_THREAD_KEYS析构, 不满意初始化
4.png 此中
// 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实行。
举个例子, 我们写一个全局构造函数, 运行可发现
8.png 如图可看出, 在_dyld_objc_notify_register之前假如有静态C++构造函数, 那么通过static_init方法直接运行。
[ runtime_init ] 运行时初始化。

10.png 可看出重要分对两部分, 分类初始化、类的表初始化举行
[ 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
15.png 实行_objc_terminate

16.png
末了crash

其实当 crash发生时,会走_objc_terminate方法,接着走到uncaught_handler, 扔出非常并传入一个参数(e), 而e的回调往下看
18.png e = fn

19.png
可看出将objc_uncaught_exception_handler fn(设置的非常) 赋值给uncaught_handler, 即 uncaught_handler 即是 fn, 由此可看出uncaught_handler, 本质是一个回调函数。
如图,体系其实会针对crash举行拦截处置惩罚,app会抛出一个非常句柄NSSetUncaughtExceptionHandler,传入一个函数给体系,当非常发生后,调用函数(函数中可以线程保活、网络并上传瓦解日志),然后回到原有的app层中,其本质是一个回调函数。
[cache_t::init()] 缓存初始化

[ _imp_implementationWithBlock_init ] 启动回调机制

22.png [ _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同步发生变革, 重要原因这个函数很紧张,  苹果不渴望它会由于一些重复加载发生繁芜。同时这个映射操纵也比力耗时, 假如不是一起的话, 也会增长耗时性。看下其内部

23.png
接下来看下map_images_nolock内部
24.png 代码有点长, 直接看重点代码: 读取镜像_read_images
read_images这个方法很紧张, 先说下_read_images都做了什么
_read_images


  • 条件控制举行一次加载
  • 修复预编译阶段的@selector紊乱标题
  • 错误紊乱的类处置惩罚
  • 修复重映射一些没有被镜像文件加载进来的类
  • 修复消息
  • 假如类内里有协议读取
  • 分类处置惩罚
  • 类的加载处置惩罚
  • 优化类
接下来看下_read_images底层实现, 并依次看下上面内容

27.png
① 第一次加载

略过一些代码看重点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); 38.png 可看出能把体系类和自界说类都读取到, 没有用到的自界说类也会读取, 自界说类后添加的先读取。接下来我们跟一下自界说类的流程
    const char *SRTest = "SRTest";      // 是否匹配    if (strcmp(mangledName, SRTest) == 0) {        printf("%s - 当前类 - %s \n", __func__, mangledName);    }运行发现SRTest已进入


先走修正方法

假如类要求稳固, 那么会修正下不稳固的类

43.png 接下来跟流程可发现会走addNamedClass


[addNamedClass]

轻微看下addNamedClass内部实现


addNamedClass将当前类添加到之前创建好的gdb_objc_realized_classes总表中
(之前有写, 往上翻第一次加载会创建一个表(key-value 哈希表): gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);)
继续跟流程可发现走addClassTableEntry
[addClassTableEntry]

47.png 将类和元类插入allocatedClasses表中。这张表是在runtime_init中创建的。(之前也有写, 往上翻runtime_init )
void runtime_init(void){    objc::unattachedCategories.init(32);    objc::allocatedClasses.init();}之后就会走readClass中的return cls;方法返回
综上,可看出readClass的重要将Mach-O中的类, 添加进内存 (插入到表中, 总表, alloc的分表都插一份)
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 20:23, Processed in 0.157411 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表