实例对象的isa指针指向类对象,类对象的isa指针指向元类对象。我们创建一个Person对象p,当p调用run方法时,通过实例对象的isa指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找run方法。
那么当我们调用分类的方法时,是否跟上面的调用顺序一样呢?下面我们创建分类来验证一下:
首先我们声明一个Person类
//Person.h
//Person.m
创建Person的分类:
在New File的iOS文件下选择Objective-C File
imageFile Type选择Category,Class父类选择Person
image
//Person+test.h
//Person+test.m
以上我们就完成创建了Person的Test分类。
在此先告诉大家结论:分类中的对象方法是存储在类对象中的,和类对象方法在同一个地方,调用步骤也和调用对象方法一样。如果是类方法的话,同样也是存储在元类对象中。
这一点大致可以从分类的底层结构中看出来:
struct category_t {
从分类的源码中可以看出Categroy在底层是以categroy _t的结构存在,里面包括对象方法,类方法,协议,和属性。注意分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成set、get方法的声明,需要我们自己去实现。
至此我们可以得出结论:
- 分类的实现原理是将分类中的方法,属性,协议信息放在
category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。- 分类中可以添加属性,但是并不会自动生成
成员变量及set、get方法。因为底层的category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的,那么我们就无法在程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量
由于上述结论的验证是依据底层源码,过程比较枯燥,也不能保证大家阅读一次就能弄清楚整个流程,所以将结论提前告知。不愿意阅读源码的读者也可以忽略以下内容,掌握上面的结论即可。
—————————————我是分割线————————————-
首先把Person+Test.m文件通过命令行转化为c++文件,查看底层编译过程。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
然后将生成的.cpp文件拖拽至Xcode中查看。在.cpp文件中搜索category_t,通过搜索结果我们可以看到,_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表:
在.cpp文件中继续往下看,我们可以看到_method_list_t *instance_methods结构体的内容:
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
通过结构体名称_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test可以看出是INSTANCE_METHODS—对象方法。我们可以看到结构体中存储了方法占用的内存,方法数量以及方法列表。并且从上图中可以看到在分类中我们实现的test,setAge, age和run四个方法。
同样,我们继续往下阅读,查看看到_method_list_t *class_methods结构体的内容:
image
同样通过结构体名称 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test可以看出是CLASS_METHODS—类方法。同样可以看到我们实现的abc类方法。
继续往下查看时,我们可以看到属性列表结构体_prop_list_t:
image
属性列表结构体_OBJC_$_PROP_LIST_Person_$_Test即_prop_list_t结构体,里面存储了属性的占用空间,属性数量以及属性列表,从上图中可以看到我们声明的age属性。
同时我们发现,.cpp文件中没有protocol_list_t *protocols协议信息列表结构体的相关信息。这是由于我们创建分类是并没有遵守任何协议,自认分类里面也就没有任何协议相关的信息。我们返回分类Person+Test,使其遵守NSCopying协议,再通过命令行将分类的.m文件编译成.cpp文件后查看:
image
通过上图可以看到分类底层先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test中,分别为protocol_count—协议数量以及存储协议方法的_protocol_t结构体。
在.cpp文件末尾处,我们看到系统定义了_category_t类型的_OBJC_$_CATEGORY_Person_$_Test结构体:
image
将_OBJC_$_CATEGORY_Person_$_Test结构体跟上文提到的catrgory_t结构体对照:
image
不难看出,上下两图中两个结构体内容一一对应,并且我们在红框标注的方法中看到,定义的_class_t类型的OBJC_CLASS_$_Person结构体,最后将_OBJC_$_CATEGORY_Person_$_Test的cls指针指向OBJC_CLASS_$_Person结构体地址。我们可以得出结论,cls指针指向的应该是分类的主类类对象的地址。
通过以上分析我们发现,分类确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。那么catagory_t结构体又如何让将这些信息存储到类对象中呢?我们通过分析runtime的源码来进一步了解。
runtime源码
我们通过opensource网站(https://opensource.apple.com/tarballs/objc4/)下载最新的源码来进一步分析。
首先来到runtime初始化函数
image
接着我们来到&map_images读取模块,来到map_images_nolock函数中找到_read_images函数,在_read_images函数中我们找到分类相关代码:
image
从上述代码中for循环中的判断我们可以知道这段代码是用来检查有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls)函数。我们来到remethodizeClass(cls)函数内部查看:
image
通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,当然一个类可以有多个分类,分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
我们来到attachCategories函数内部:
image
上述源码中可以看出,首先根据方法列表,属性列表,协议列表通过malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。
之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。
之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性和协议进行了合并。
我们来看一下attachLists函数内部查看:
image
上述源代码中有两个重要的数组
array()->lists:类对象原来的方法列表,属性列表,协议列表。
addedLists:传入所有分类的方法列表,属性列表,协议列表。attachLists函数中最重要的两个方法为memmove内存移动和memcpy内存拷贝。我们先来分别看一下这两个函数
// memmove :内存移动。
// memcpy :内存拷贝。
下面我们图示经过memmove和memcpy方法过后的内存变化:
首先未经过内存移动和拷贝时:
image
经过memmove方法之后,内存变化为:
// array()->lists 原来方法、属性、协议列表数组
如图所示:
image
经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。
memcpy方法之后,内存变化为:
// array()->lists 原来方法、属性、协议列表数组

image
我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。
那么为什么要将分类方法的列表追加到本来的对象方法前面呢?
其实这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。
但是经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的,这一点可以通过打印所有类的所有方法名来查看,我们自己实现一个方法,打印所有类的所有方法名:
- (void)printMethodNamesOfClass:(Class)cls
我们在控制器中引入Person类,在控制器的viewDidLoad方法中创建Person对象,并且调用run方法和上面的打印所有类的所有方法名的方法:
- (void)viewDidLoad {
通过打印台打印内容可以发现,调用的是分类中的run方法,并且Person类中存储着两个run方法。
资料推荐
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
