OC底层原理之-类的加载过程-上( _objc_init实现原理)

1,512 阅读11分钟

前言

我们在上篇文章OC底层原理之-App启动过程(dyld加载流程)讲到dyld加载中会调用_objc_init。这篇文章我们就来仔细研究一下_objc_init方法都做了哪些工作这边文章和dyld加载过程有关联,可以先看看我上篇文章

探究_objc_init

我们来看下_objc_init方法 上面的方法做了标注,下面我们看看这些方法,其中主要会对_dyld_objc_notify_register(&map_images, load_images, unmap_image)方法进行解读(这个方法会牵扯到类的加载过程)

探究environ_init

我们先看下environ_init 这个方法就是进行环境变量操作的,下面我们将426-430行移出来,并将无用判断删掉,如下图所示 运行项目 上图就是打印的环境(截取部分),这些环境我们有的用到过,下面我们看下** OBJC_DISABLE_NONPOINTER_ISA**,我们作如下处理 我们运行代码,对Person打印 下面将OBJC_DISABLE_NONPOINTER_ISA删除掉,再运行打印Person

通过上面的两个打印我们发现当设置OBJC_DISABLE_NONPOINTER_ISA时,打印的isa指针是纯isa指针,没有优化,而不设置则是优化过的isa指针

我们再举个例子,环境变量中有个OBJC_PRINT_LOAD_METHODS,我们设置下这个变量,然后运行

我们发现这个是打印项目中所有的load方法,Person也有load调用是因为我们在Person类写了load方法。

通过上面的例子可以看出来,环境变量设置,会帮助我们更快速的处理一些问题。我们还看到这些环境变量OBJC开头,后面又分为PRINT(打印)、DEBUG(调试)、DISABLE(禁用)这几类。

探究tls_init()

我们先看这个方法的实现 我们通过上图可以看到,这个方式是对线程池进行初始化的。

探究static_init()

先看方法实现 通过注释我们知道此方法是对系统级别的C++构造函数进行调用,而且它的调用会在dyld调用构造函数之前

探究runtime_init()

看方法实现 方法我已经注释了,这个方法作用就是进行初始化容器工作。后面会用到这些初始化的容器

探究exception_init()

看下方法实现 这个就是注册监听异常回调,系统方法在执行的过程中,出现异常触发中断,就会报出异常,如果我们在上层对这个方法处理,我们就能捕获这次异常。注意:是系统方法执行异常。我们可以在这里去监听系统异常,我们看下怎么处理。 我们看下_objc_terminate方法, 再看下700行意思就是如果有异常就会调用uncaught_handler。我们看下uncaught_handler方法

也就是它会给uncaught_handler一个默认值为_objc_default_uncaught_exception_handler,也就是如果我们没有给uncaught_handler赋值,那它就由系统处理

我们看下哪里对uncaught_handler赋值,搜索下uncaught_handler。

我们看注释意思就是这个objc_setUncaughtExceptionHandler方法为捕获Objective-C异常设置一个处理程序,并返回这个处理程序。我们看方法实现也是将传入的方法赋值给uncaught_handler。

上面说了可以通过这个方法检测异常,下面我们写个简单的demo实验一下。 准备代码:

// 创建新类:UncaughtExceptionHandle,在.m文件写如下代码
@implementation UncaughtExceptionHandle
void TestExceptionHandlers(NSException *exception) {
    NSLog(@"---->%@---->%@", exception.name, exception.reason);
}
+ (void)installUncaughtSignalExceptionHandler {

    NSSetUncaughtExceptionHandler(&TestExceptionHandlers);
}
@end

// 在ViewController.m做如下代码
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataArray = @[@"1",@"2",@"3",@"4",@"5",@"6"];
}
- (IBAction)exceptionAction:(id)sender {
    NSLog(@"%@",self.dataArray[100]);
}
@end

// 在AppDelegate调用installUncaughtSignalExceptionHandler。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [UncaughtExceptionHandle installUncaughtSignalExceptionHandler];
    return YES;
}

UncaughtExceptionHandle类中我们将TestExceptionHandlers作为异常处理方法传入,我们在ViewController做了点击按钮获取dataArray的第100位,这个时报错的。我们运行代码,点击按钮 看到截图,我们异常处理方法打印出来了,报错原因。

这个方法还是很重要的,有兴趣的可以继续探索一下,用处大,这里不再过多说明

我们继续往下看。

探究cache_init()

先看方法实现 通过方法实现我们看不出太多东西,通过方法名字倒是可以看出这个方法是cache的初始化

探究_imp_implementationWithBlock_init()

看下方法实现 方法实现可以看到这个实在OS下执行,这个方法就是对imp的Block标记进行初始化。

探究_dyld_objc_notify_register()

这个方法是重点讲的,我们看到这个方法的参数有三个分别是map_images、load_images、unmap_image。这个方法我们在dyld加载中提到了,他是个回调函数,我们进dyld源码

调用_dyld_objc_notify_register方法就会触发dyld中的registerObjCNotifiers方法,如上图所示,也就是当objc的准备工作都已完成,此时调用_dyld_objc_notify_register告诉dyld,可以进行类的加载。

我们在梳理下dyld流程,

  • 在recursiveInitialization方法中调用bool hasInitializers = this->doInitialization(context);这个方法是来判断image是否已加载
  • doInitialization这个方法会调用doModInitFunctions(context)这个方法就会进入libSystem框架里调用libSystem_initializer方法,最后就会调用_objc_init方法
  • _objc_init会调用_dyld_objc_notify_register将map_images、load_images、unmap_image传入dyld方法registerObjCNotifiers。
  • 在registerObjCNotifiers方法中,我们把_dyld_objc_notify_register传入的map_images赋值给sNotifyObjCMapped,将load_images赋值给sNotifyObjCInit,将unmap_image赋值给sNotifyObjCUnmapped
  • 在registerObjCNotifiers方法中,我们将传参复制后就开始调用notifyBatchPartial()。
  • notifyBatchPartial方法中会调用(*sNotifyObjCMapped)(objcImageCount, paths, mhs);触发map_images方法
  • dyld的recursiveInitialization方法在调用完bool hasInitializers = this->doInitialization(context)方法后,会调用notifySingle()方法
  • 在notifySingle()中会调用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());上面我们将load_images赋值给了sNotifyObjCInit,所以此时就会触发load_images方法。
  • sNotifyObjCUnmapped会在removeImage方法里触发,字面理解就是删除Image(映射的镜像文件)。 下面我们看下map_images、load_images、unmap_image都做了什么

探究map_images

我们看下map_images的实现 通过注释我们可以理解这个方法是处理映射的Image。这个方法会调用map_images_nolock方法,我们看下这个方法实现 这个方法很长,我们把一些不重要的方法进行了折叠,不重要的方法在截图上做了注释,其中这个方法比较重要的就是_read_images,下面我们看下_read_images方法

探究_read_images

这个方法很长,我把很多东西都折叠了,下面我们来进行分析

实例化存储缓存表

我们将不重要的代码折叠。这个方法我在重要的代码处做了注释。这个方法只会进来一次,第一次进来对TaggedPointer进行优化处理,通过NXCreateMapTable创建个表

修正sel

这个方法是用来处理需要修正的sel,我们看到3479判断,也就是当sels[i]和通过sel_registerNameNoLock获取的sel地址不同是,将它设置为相同。 我们打印下sels[i],以及sel 我们发现他们的名字都是class,但是地址却不同,这个方法处理后就会保证他们的地址是相同的

准备读取类

其中3492行第一次进来是不会走的,其重点方法是3504行的readClass,readClass是直接开始读取类

类的重映射

这个方法一般不走

修正协议

这个方法的重点是第3594行代码,直接读取协议

非懒加载类处理

上面的代码已经做了注释,这里不再过多解释。下面补充下内容:

  • 懒加载:类没有实现 load 方法,在使用的第一次才会加载,当我们再给这个类的发送消息,如果是第一次,在消息查找的过程中就会判断这个类是否加载,没有加载就会加载这个类。
  • 非懒加载:类的内部实现了 load 方法,类的加载就会提前

懒加载类在首次调用方法的时候,才会调用realizeClassWithoutSwift()方法去初始化加载

下面我们来看一下readClass方法

readClass

看下readClass的方法实现 上图我将部分方法进行了折叠,我折叠的判断都不会走,第3193行判断,只有superclass不存在才会进入判断内。第3209行判断此类从来未被实现才会走。第3234行判断,其中headerIsPreoptimized是外部传来的,只有该类禁用了预优化才会返回true,所以会走addNamedClass以及addClassTableEntry

下面我们看一下addNamedClass 通过注释我们知道这个方法是将name => cls添加到命名的非元类映射。其中1679行判断只有不能通过名称查找到的类才会走上面判断,所以会调用NXMapInsert方法,看NXMapInsert的传参,第一个参数:gdb_objc_realized_classes这个参数在最开始的_read_images方法里进行过初始化创建,name是类名,cls是地址

下面我们看下NXMapInsert方法 我已经将不走的判断给折叠了。下面解释下方法

  • 314-316行:1.取出第一个MapPair地址 2.通过bucketOf找到key在table中的位置 3.将地址偏移取出,该位置下的MapPair。
  • 340行:将获取的地址给index2
  • 341-353行:是个循环。
    • 341行:将index2+1和table的nbBucketsMinusOne进行与运算得到的值重新给index2,如果值不等于最开始算的index就进来
    • 342行:让pair在等于pairs偏移新的位置
    • 343-348行:如果pair的key地址不存在则进去,所以这个判断不会进。
    • 349行:如果pair->key和key相等,就进入,不等就继续循环
    • 350行:将pair的value取出给old
    • 351行:如果old值不等于传的value就将value值重新赋值给pair的value。
    • 352行:返回old值

此时已经将类的名字跟地址进行关联存储到MapTable中了。

addClassTableEntry方法

看下addClassTableEntry方法实现 这个方法注释说的比较清楚,就是将这个类添加到类表中(类表中存放所有的初始化类),allocatedClasses容器在runtime_init初始化过了。 最后readClass方法结束,将cls返回。 我们验证一下,为了看到我们自己创建的Person类(这样更好处理) 添加如下代码(红框) 运行代码,在readClass前打断点,进行如下打印: 我们将断点放在readClass后,再做相同打印 我们发现进行readClass后,cls就打印出类名,不再是地址,下面我们在打印下cls地址: 我们发现Person类的首地址就是我们在调用readClass时候的cls地址,这也验证了我们上面说的内容

探究load_images

我们看下load_images方法实现

  • 3080行:这个判断条件:didInitialAttachCategories,didCallDyldNotifyRegister他们的默认值都是false,所以3080-3083不会走
  • 3086行:如果没有load方法就直接不加锁返回,如果一个类没有load方法,到这一步就结束了,后面我们验证一下
  • 3088行:递归锁
  • 3092行:加锁
  • 3093行:获取所有要调用的+load方法
  • 3097行:调用获取到的所有load方法

prepare_load_methods方法

我们看下prepare_load_methods方法 上面方法我都做了备注,这里不再说了,我们下面看下schedule_class_load方法

schedule_class_load方法

先看方法实现 已经做了注释,通过上面我们可以确定类,分类,子类的所有方法都会被找出来,而且先加载类后父类,最后分类。schedule_class_load中第3758行是判断+load是否被加载过,加载过得会在3764做标记,第3763行add_class_to_loadable_list实现跟分类实现类似

add_category_to_loadable_list方法

这个方法调用是在分类的循环中调用,所以先后顺序跟加载顺序一致。schedule_class_load方法里第3763行add_class_to_loadable_list,它的实现和分类有些类似

call_load_methods方法

先看下这个方法实现(注释很长,截图没有截注释) 上面的截图我做了详细说明,这里我们知道在while循环的时候是先循环遍历类,父类的+load方法,后遍历分类的+load方法

unmap_image方法

至于方法_unload_image如何卸载,removeHeader如何移除,后续研究了再继续补充。

总结

我们研究_objc_init调用,梳理了_dyld_objc_notify_register方法流程,主要看了map_images、load_images、unmap_image方法实现,主要分析map_images下的_read_images方法实现。下面我们来总结下每个方法都做了些什么

_read_images方法执行流程

  • 1.判断是否使用non-pointer对isa进行优化
  • 2.对TaggedPointer的优化处理
  • 3.创建保存类的哈希表
  • 4.注册修正sel
  • 5.获取所有类,读取类并将类存储到3创建的表中
  • 6.修复需要重映射的类
  • 7.获取并修正协议
  • 8.对非懒加载类进行处理
  • 9.对懒加载类进行处理

load_images方法执行流程

  • 1.获取所有非懒加载类,将+load方法保存到loadable_categories
  • 2.获取所有分类的+load方法,保存到loadable_categories中
  • 3.先从loadable_categories拿类,父类的load方法进行调用
  • 4.再从loadable_categories拿分类的load方法进行调用

unmap_image方法执行流程

  • 1.卸载需要unmap的image数据
  • 2.移除需要unmap的image。

上面就是类的加载过程,dyld在调用map_images,load_images就是对类进行加载,对+load方法进行调用,对unmap_image调用就是先移除image包含的数据,再移除image

写到最后

文章有3个地方未探究:

  • 1.非懒加载类的初始化方法:realizeClassWithoutSwift()
  • 2.unmap_image的移除数据方法_unload_image()
  • 3.unmap_image的移除image方法removeHeader()

后面补充。