前言
我们在上篇文章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()
后面补充。