OC底层探索(十三): 类的加载(一)
2024-04-09 16:15:47  阅读数 8060

所用版本:

  • 处理器: 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_registermain

例子

执行_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_imagesload_imagesunmap_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

  1. 先看下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