先记录几个问题吧。
- category 的结构是什么?category 为什么不能添加变量?
- category 是怎样加载的?
load
和initialize
的区别?
在此之前,可以下载 rumtime 的源码。
category 的结构
在objc-runtime-new.h
可以找到 category 的结构。
|
|
从上述结构可以得知,category 的结构中有实例方法表,类方法表,协议表,属性表,类属性表。并没有变量表,这也是 category 无法直接添加变量的原因。
category 的加载
其实,这个问题需要从位于 objc-os.mm 中的_objc_init
方法中开始。
|
|
重点关注 _dyld_objc_notify_register 传入的三个参数:map_images、load_images、unmap_image。从代码得知,map_images -> map_images_nolock -> _read_images 这样的层层调用关系。
其中,从 _read_images 的方法中,可以得知,这个方法就是读取 class 文件,protocol,以及 category 的。
|
|
通过 _getObjc2CategoryList 获取一个 catlist ,也是存放分类的一个列表,之后进行遍历,查看每个分类文件对应的类是否已经链接,如果缺失该类,那么也会将 catlist 中对应的给置为 nil,然后继续下一个分类文件。接着会把分类注册到对应的类去,然后会重写类的方法列表,其中会先添加实例方法和属性,再进行添加类方法和类属性。一直持续到所有分类文件链接读取完毕。
而重写类的方法列表则是由 remethodizeClass 方法来进行。
|
|
从代码可以得知,category 的方法等也是在这里进行加入的,且看 attachCategories 方法。
|
|
以倒序的方式,获取到 category 的方法,属性,协议,再将其加入到对应的数组里面。之后在通过 attachLists 方法,将他们加入到对应的原数组中。有一个重要的是,category 的方法,属性,协议均会插入到类对应的数组的前头。
|
|
load
和 initialize
的区别
举个🌰吧。
|
|
先说下 load 方法是怎样一个调用的过程。
在运行时,还是会从 _objc_init 方法开始。为什么会是从这里开始呢。其实,我们在+load
中打上断点。那么在运行过程中,可以看到其是这样的一个顺序。load_images - call_load_methods - load 。将前两者在 runtime 源码中搜索,可以得到对应的方法。load_images 位于objc-runtime-new.mm
;call_load_methods 位于objc-loadmethod.mm
。而 load_images 恰恰是在 _objc_init 内部进行调用。
再者,也可以加上一个 symbolic 断点。
在运行时可以得知,load_images 是在 _objc_init 之后调用的。
|
|
load_images 会先通过调用 prepare_load_methods 方法来决定一些所需加载方法的数据。比方其内部会通过 add_category_to_loadable_list 方法来把需要加载的类的一些相关数据准备好,如 loadable_categories 等。
之后,才是调用 call_load_methods 方法来进行方法的加载。
|
|
load 方法实则就是 call_load_methods 来进行的。在这个方法中,会注意到 call_category_loads() 的方法调用,这里也就是我们在写 category 时,多个 load 方法存在时进行调用的地方了,并且只加载一次。
以上大概就是 load 加载的过程。那么 initialize 又是怎样的呢?
在父类的 initialize 方法中打上断点。可以知道其实际上是通过 _class_initialize 方法来实现。该方法位于 objc-initialize.mm 文件中。也可以知道,initialize 方法是在初始化才进行调用。
并且可以从 _class_initialize 的源码中发现,initialize 是通过 objc_msgSend 来进行调用的。
因此,load 和 initialize 的区别主要是:
- 前者是运行时就进行调用,并且是发生在 main 函数调用前;而后者是在其初始化时才进行调用,并且实则是经过 objc_msgSend 进行消息发送的
- 前者只调用一次;后者则是每初始化一次就调用一次
- 前者可以多个 load 进行调用;但是后者根据上一节的描述,会发现在 category 中重写 initialize 方法则会进行覆盖