热点新闻
OC底层探索(十三): 类的加载(一)
2023-07-12 01:51  浏览:458  搜索引擎搜索“广企汇”
温馨提示:为防找不到此信息,请务必收藏信息以备急用! 联系我时,请说明是在广企汇看到的信息,谢谢。
展会发布 展会网站大全 报名观展合作 软文发布

所用版本:

  • 处理器: Intel Core i9
  • MacOS 12.3.1
  • Xcode 13.3.1
  • objc4-838



熟悉类加载前, 先看下类的初始化方法_objc_init( 留意看下下面的注释 ):

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

发布人:8acd****    IP:120.244.94.***     举报/删稿
展会推荐
让朕来说2句
评论
收藏
点赞
转发