题记
作为iOS开发者,对category肯定不会陌生,category一般又叫分类,当我们需要为一个类增加额外的方法属性等时,分类便是我们的首选。根据前文可知,我们对象方法是存放在类中,类方法是存放在元类对象中,那么如果我们在category增加了方法属性等,它们又是怎么存放的?苹果内部是怎么实现的呢?
准备工作
老规矩我们准备一个继承自NSObject的JJPerson类,然后为它增加一个分类JJPerson+run,增加一个age属性,一个run对象方法以及一个eat类方法。
为了方便我们去了解它的底层实现,我们可以先把它的.m文件转成c++,并拽入Xcode方便查看,为了避免不必要的报错,我们不让这个.cpp文件参与编译,前文有详细操作步骤
准备工作到此完毕,下面我们可以进入分析阶段
编译文件分析
- 底层结构
我们搜索“_category_t”可以看到,一个分类在编译后转成C++文件,就是以上图这种结构体的方式存在。我们每增加一个分类,编译后就会多一个这样的结构体,然后在运行时阶段,把所有分类的结构体全部动态合并到我们JJPerson类里面去。 - 结构体分析
我们看到结构体包含下面几种常见数据类型
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
(1)instance_methods是我们为分类增加的实例方法列表
(2)class_methods是我们为分类增加的类方法列表
(3)protocols是分类遵守的协议列表
(4)properties则是我们增加的属性列表
(5)对于结构体前面的name和class,我们可以猜测这个是类名和分类的名称相关的内容
- 我们可以看到最下面红色框内就是编译后的结构体数据,也就是我们所说“ _category_t”的结构,箭头两个参数对应着实例方法和类方法,而且这两个方法编译后也是以结构体的形式存在在上面。最后两个参数为0,是因为我们的分类没有遵守协议也没有添加属性。
- 我们每增加一个分类,编译后就会增加一个这样的结构体,格式一样,但是命名和参数以各自为准。
- 在运行时阶段,才会真正的把结构体里面的数据合并到我们的类里面去,也就是前面文章提到的,类方法存放在元类对象里,属性,协议,实例方法等存放在类对象里
源码分析
-
我们尝试从运行时源码去分析,看看苹果是怎么对分类进行处理
(1)首先我们打开下载好的objc源码,专题文章第一篇有介绍,这里不再赘述
(2)选择 objc-os.mm 文件,这里是运行时方法的入口
(3)搜索 _objc_init 方法,点击 &map_images 参数
(4)再点击 map_images_nolock 方法进入
(5)注释提到,hCount 是用来查找所有oc元数据模块的,那我们可以专门留意一下它
(6)通过搜索 hCount ,我们可以看到一个叫 _read_images(加载模块) 的方法里面有个参数是传入 totalClasses(所有类),我们可以点击进入作进一步查看
(7)然后我们往下拉可以看到一个关键注释,Discover categories,很明显接下来部分就是对找到的分类进行处理。尤其红色框框内我们还能看到熟悉的 category_t 类型,甚至还能看到方法名是叫做 _getObjc2CategoryList(获取分类列表),那么我们接下来就重点看这里的处理
(8)首先我们可以看到这里有两个是对类重新方法化的处理,分别把class和isa传了进去,这就符合了我们前面提到的,把类方法添加到元类对象的方法列表,把实例方法等添加到类对象的方法列表。所以我们需要看看这个方法的内部实现
(9)我们又看到一个名为附加协议的方法 attachCategories,再次点进去,我们就能看到真正的实现
(10)为了方便阅读,我在这里加了一些中文注释。这里会有一个参数 isMeta 记录传进来的是否是元类对象。然后调用malloc函数开辟存储空间创建一个方法列表的二维数组(属性列表和协议列表同理),然后用 while 循环,根据是否为元类对象,取出对应的类方法或者实例方法,然后把方法数组添加到前面创建的二维数组中(属性和协议处理方法同理)。
(11)最后获取类数据,把分类中所有的方法,属性,协议全部添加到类中去。我们可以再进一步看看 attachLists 这个方法里面做了什么,因为这里将是内存分配最重要一环
(12)我们可以看到,在这个方法里重新计算了列表新的长度,并且调用 realloc 重新分配了新的内存空间。接下来调用 memmove 方法,把原来的方法列表向后移动,前面留出了新列表的长度,再调用 memcpy 方法,把新方法列表插入到整个列表最前面。
(13)这一步内存移动的体现是,如果我们在分类中添加和原类中同名的方法时,我们调用该方法时会优先调用分类的方法,究其原因就是我们分类的方法在方法列表中重新插入在最前面,所以会优先调用,这也是我们常说的分类方法会覆盖原方法的原因。
总结
- 每创建一个分类,编译时就会生成一个 _category_t 的结构体,里面包含着分类的所有信息(方法,属性,协议)
- 在运行时阶段,分类的所有信息(方法,属性,协议)会被分别读取,并且逐一添加到类里面去
- 由于内存操作的原因,分类的方法会排在方法列表最前面,所以分类方法会优先于原类方法的调用(所谓的分类方法覆盖,本质是排序超前)