Category的原理
回顾OC对象的本质,每个OC对象都含有一个isa指针,arm64
之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在 arm64
架构之后,apple对isa进行了优化,变成了一个共用体 union
结构,同时使用位域来存储更多的信息。OC对象的isa指针斌不是直接指向类对象或者是元类对象的,而是需要 &ISA_MASK
通过位运算才能取到相应的地址,但是为什么要这样做。
Category
我们先写一段代码,之后的分析都基于这段代码
1 |
|
之前讲到过实例对象的 isa
指向类对象,类对象的 isa
指向元类对象,当 people
实例对象调用 run
方法时,类对象的 isa
找到类对象的 isa
,然后在类对象中查找对象方法,如果没有找到,就通过类对象的 superclass
找到父类对象,接着去寻找 run
方法。
分类中的对象方法依然是存储在类对象中的,同对象方法在同一个地方,那么调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
Category的结构
在 objc-runtime-new.h
中我们可以找到分类的定义
1 | struct category_t { |
从源码可以找到我们平时使用的categroy、对象方法、类方法、协议和属性对应的存储方式。并且分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成 get
set
方法的声明,需要我们自己去实现。
我们通过命令行将 People+Test.m
文件转化为c++文件 People+Test.cpp
,查看其中的编译过程。
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc People+Test.m |
_category_t
在 People+Test.cpp
中可以看出 _category_t
结构体中,存放着类名、对象方法列表、类方法列表、协议列表以及属性列表。
1 | struct _category_t { |
对象方法列表
我们也可以看到 _method_list_t
类型的结构体
1 | static struct /*_method_list_t*/ { |
我们发现这个结构体 _OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test
从名称可以看出是 INSTANCE_METHODS
对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且从上图中找到分类中我们实现对应的对象方法,test
、setAge
、age
三个方法
类方法列表
接下来我们发现同样为 _method_list_t
类型的类方法结构体
1 | static struct /*_method_list_t*/ { |
同上面对象方法列表一样,这个我们可以看出是类方法列表结构体 _OBJC_$_CATEGORY_CLASS_METHODS_People_$_Test
,同对象方法结构体相同,同样可以看到我们实现的类方法 classMethod
。
协议方法列表
1 |
|
通过上述源码可以看到先将协议方法通过 _method_list_t
结构体存储,之后通过 _protocol_t
结构体存储在 _OBJC_CATEGORY_PROTOCOLS_$_People_$_Test
中同 _protocol_list_t
结构体一一对应,分别为 protocol_count
协议数量以及存储了协议方法的 _protocol_t
结构体。
属性列表
1 | static struct /*_prop_list_t*/ { |
属性列表结构体 _OBJC_$_PROP_LIST_People_$_Test
同 _prop_list_t
结构体对应,存储属性的占用空间、属性属性数量以及属性列表,从上图中可以看到我们自己写的age属性。
OBJC$_CATEGORY_People_$_Test
对比一下 _category_t
结构体的实现
1 | struct _category_t { |
1 | extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_People; |
上下一一对应,并且我们看到定义 _class_t
类型的 OBJC_CLASS_$_People
结构体,最后将 _OBJC_$_CATEGORY_People_$_Test
的 cls
指针指向 OBJC_CLASS_$_People
结构体地址,cls
指针指向的应该是分类的主类类对象的地址。
分类在运行时的操作
通过分析我们发现分类源码中是将我们定义的对象方法、类方法、属性等都存放在 catagory_t
结构体中。接下来我们在回到 runtime
源码查看方法、类方法、属性等是如何存储在类对象中的。
1 | /*********************************************************************** |
我们找到 _read_images
函数,找到加载分类相关的代码
1 | // Discover categories. |
这段代码是用来查找是否有分类的。通过 _getObjc2CategoryList
函数获取到分类列表之后,进行遍历,获取其中的方法、协议、属性等。可以看到最终都调用了 remethodizeClass(cls)
函数,我们来到 remethodizeClass(cls)
函数内部查看。
1 | /*********************************************************************** |
通过代码我们发现 attachCategories
函数接收了类对象 cls
和分类数组 cats
,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在 category_t
结构体中,那么多个分类则保存在 category_list
中。
我们再看 attachCategories
函数
1 | // Attach method lists and properties and protocols from categories to a class. |
1 | void attachLists(List* const * addedLists, uint32_t addedCount) { |
attachLists
函数中最重要的两个方法为 memmove
内存移动和 memcpy
内存拷贝
1 | // memmove :内存移动。 |
1 | // array()->lists 原来方法、属性、协议列表数组 |
经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。
1 | // array()->lists 原来方法、属性、协议列表数组 |
我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过 memmove
和 memcpy
方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看
1 | - (void)printMethodNamesOfClass:(Class)cls { |
经过以上代码我们会发现输出了两次 run
方法
总结
分类的实现原理是将 category
中的方法、属性、协议数据放在 category_t
结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。category
可以添加属性,但是并不会自动生成成员变量及 set/get
方法。因为 category_t
结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经布局完成。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。