我们接着上篇 Class-Model 继续围绕着 Runtime 来更深入的了解一个继承与 NSObject
类的初始化的过程。
load
我们先来了解下 load
方法,当一个类或者分类添加到运行时时候会被调用,通常的用法是在类中实现这个方法然后我们去做 Method Swizzling 操作,实质上就是可以在运行时干预类的加载。通过简单的Demo我们可以发现,一个类和她的子类都会执行 load
方法,而且子类会后置执行 load
方法。同时,分类中的 load
方法会先于主类中的。
prepare_load_methods
我们可以找到 objc-runtime-new.mm
中的 prepare_load_methods
方法
1 | void prepare_load_methods(const headerType *mhdr) |
在 prepare_load_methods
方法中,分为两个步骤:
- 获取了所有类后,遍历列表,将其中有
load
方法的类加入loadable_class
; - 获取所有的分类,遍历列表,将其中有
load
方法的类加入loadable_categories
。
其主要的目的就是提前准备好满足 load
方法调用条件的类和分类,以供下一步使用。
schedule_class_load
我们再来看下预加载方法中会首先通过 schedule_class_load(cls->superclass)
的递归调用确保有父类中的 load
方法被加入 loadable_class
,从而保证父类的 load
方法总是在子类之前调用。因此,在覆写+load方法时,不需要调用super方法。
1 | static void schedule_class_load(Class cls) |
call_load_methods
下面就是对 load
方法的调用了,打开objc-loadmethod.mm
,找到 call_load_methods
方法
1 | void call_load_methods(void) |
调用类的 load
方法,在 call_class_loads
方法中通过在第一步读取 prepare_load_methods
步骤里的 loadable_classes
,遍历列表并调用 load
方法,然后遍历的调用分类的 load
方法。
call_class_loads
1 | static void call_class_loads(void) |
这个函数负责调用类的 load
方法,它从全局变量 loadable_classes
中取出可供调用的类之后进行重置操作。
1 | loadable_classes = nil; |
loadable_classes
指向用于保存类信息的内存的首地址,loadable_classes_allocated
标识已分配的内存空间大小,loadable_classes_used
标识已使用的内存空间大小。
循环调用所有类的 load
方法,这里是直接使用函数内存地址的方式 (*load_method)(cls, SEL_load);
对 load
方法进行调用的,而不是使用发送消息 objc_msgSend
的方式。
真是因为这种调用方法,所以父类、子类和分类中的 load
方法才会有之前提到的逻辑,子类没实现 load
方法的话在 runtime 时期也不会去调用父类的 load
方法,如果都实现则都会调用。
initialize
initialize
方法是在类和子类在第一次收到实例方法和类方法的调用消息时候调用,因此这个方法是懒加载形式调用的,如果程序一直没有给一个类发送消息那么 initialize
则永远不会被调用。
lookUpImpOrForward
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
当我们给一个类发送消息的时候会调用这个函数查找对应方法的实现,我们可以看到这个方法会判断类是否初始化,如果未初始化会调用 _class_initialize
进行初始化。
_class_initialize
1 | void _class_initialize(Class cls) |
_class_initialize
方法实现看起来比较长,但其实关键步骤也就只有两步:
- 11-13行使用了递归确保父类先于子类初始化。
- 通过判断此类的初始化状态进行相应处理。
初始化状态处理
未初始化
- 如果当前类未初始化,则会向它发送一个
setInitializing
消息,将该类的元类的信息更改为CLS_INITIALIZING
,并通过reallyInitialize
标识来与Initializing
区分。 - 设置成功
CLS_INITIALIZING
后,_setThisThreadIsInitializingClass
记录当前线程正在初始化当前类,当前线程可以向该类发送消息,而其他线程则需要等待。 - 通过
callInitialize
调用initialize
方法,也就是说与load
不同该方法是通过objc_msgSend
发送消息实现的,因此也拥有objc_msgSend
带来的特性,也就是说子类会继承父类的方法实现,而分类的实现也会覆盖元类。 initialize
完成后,更新当前类的状态。如果父类已经完成初始化,则_finishInitializing
立马更新,否则通过_finishInitializingAfter
等父类完成后再更新。
1 | static void _finishInitializingAfter(Class cls, Class supercls) |
在该方法中,通过声明一个 PendingInitialize
类型的结构体 pending
来存储当前类与父类信息,并以父类 supercls
为key值,以 pending
为value存储在 pendingInitializeMap
链表中。
- 如果完成初始化则调用
_finishInitializing
,在该方法中会首先将当前类标记为已完成初始化状态Initialized
,然后去读取pendingInitializeMap
,如果查找到该类对应的待处理子类,则将对应的消息移除并通过递归的方法将因当前类而被阻塞的子类标记为已完成初始化。
1 | static void _finishInitializing(Class cls, Class supercls) |
正在初始化
如果是当前线程在进行初始化,则不做处理。
如果是其他线程在进行初始化,则等其他线程完成后再返回,以保证线程安全。
已初始化
如果已经完成初始化,则不做处理。
不知道什么状态
还没有遇到过,但是一点控制流走到这一步会抛出一个错误,提示线程安全类在运行时的初始化有问题。
initialize 总结
initialize
方法是在main函数之后调用的;initialize
方法遵从懒加载方式,只有在类或它的子类收到第一条消息之前被调用的;- 子类中不需要调用
super
方法,会自动调用父类的方法实现; initialize
只调用一次,init可多次调用。
initialize与load对比
load | initialize | |
---|---|---|
调用时机 | main之前,runtime初始化时 | main之后,当前类或子类收到第一条消息前 |
调用方式 | 函数内存地址 | objc_msgSend |
继承父类实现 | 否 | 是 |
分类实现 | 类和分类都执行 | 覆盖父类实现 |
调用顺序 | 父类-子类-分类 | 父类-子类 |
调用次数 | 1次 | 1次或不调用 |