ObjC中Category的原理简析

请注意,本文编写于 349 天前,最后修改于 191 天前,其中某些信息可能已经过时。

Objective-C类别也叫分类,是一种不需要继承即可给类添加方法的语法技术。

Category的使用

通常我们是用Category为一个类添加一些方法。我们可以直接用类似对象对方法调用的样子直接对Category中的方法进行调用。比如下面的例子,为Person(Person类定义在.h和.m文件中了,图片没有给出)类定义了一个名为Test的Category。

main.m
main.m

调用Category中的test方法与实例直接调用实例方法一样,也是通过消息发送机制(objc_sendMethod)进行调用。

我们都知道类似这种方法调用是通过实例的isa指针查找类对象,在类对象中查找到相应的实例方法进行调用的,当然还包括superclass查找父类,这里就不做赘述了。因为通过isa指针查找方法已经在往深处看-ObjC对象中有过介绍。

那么Category定义的方法存放在哪里呢?其实它们跟类的实例对象一样,也是存放在类对象中。同样,Category中定义的类方法,也是存放在元类对象中的。只不过将Category中的信息合并到类或元类对象中的操作是在运行时完成的而非编译的时。

Category的底层结构

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc mian.m -o main.cpp 将上述main.m转换成main.cpp文件,可以看到Person的Category (Test) 被转换成了下面这样一个_category_t类型的结构体。

Person+Test
Person+Test

查看_category_t结构体的结构:其中存放有:名称、类信息、实例方法和类方法列表、协议列表和属性列表。

_category_t
_category_t

实际上在runtime源码的最新版本objc4-723中对category_t的描述中添加了获取方法列表的方法和元类属性的方法,并且将properties分成了instanceProperties和_classProperties。

runtime-723中的category_t-w1298
runtime-723中的category_t-w1298

Category初始化传值

我们可以看到在_OBJC_$_CATEGORY_Person_$_test中,传入的值:

instance_methods
instance_methods

class_methods
class_methods

protocols
protocols

properties
properties

在runtime源码中窥探Category

上面说过Category中的方法、属性、协议是在运行时合并到类中的,所以我们在运行时的入口objc_os.mm文件中找到_objc_init方法。我们可以看到在方法上面有注释表示这是入口函数,会通过dyid去加载一些模块。

顺着这个思路我们查看map_image是通过map_images_nolock(count, paths, mhdrs)获得的,在这个方法中存在一个加载模块的方法。

_read_images
_read_images

加载模块的函数中有一个重排方法的函数,在这个函数中有一个 attachCategories(cls, cats, true /*flush caches*/);函数。这个函数是实现附加分类的作用。参数分别传递了cls对象和Category列表。

attachCategories()中将Category中的属性、方法和协议取出放到数组中,然后用cls取出类中的class_rw_t结构体,取出的methods、properties、protocols分别调用attachLists方法,并且将方法数组、属性数组、协议数组和数组的中元素的个数传递到attachLists中。

在attachLists函数中将之前存放方法、属性、协议的数组扩容至oldCount+addedCount大小。将原方法、属性、协议的数组从大数组的第一个位置移动到最后的位置,然后将attachCategories传入的列表拷贝到大数组的前面,完成Category中的方法、属性、协议向类对象或元类对象的合并。

attachList函数片段
attachList函数片段

将Category中的内容合并到类或元类在runtime中的操作顺序:

首先类或元类方法列表是一个二维数组:

方法列表
方法列表

runtime合并Category信息到类或元类对象中在runtime源码中的函数用轨迹:

合并Category到类或元类中
合并Category到类或元类中

attachLists图示:

attachLists
attachLists

attachLists最终结果
attachLists最终结果

使用Category中方法的一个问题

我们可以看到在没有将Category的方法列表加到类或元类对象中之前,数组中只有这一个列表,而合并之后,它们就被放到了类或元类对象的方法列表的最后面,所以我们调用在Category中重写的类中的实例方法或者类方法都会优先执行Category中的方法,因为isa在类对象或者元类对象中寻找方法的时候会首先在Category中找到,既然找到了就不会继续往下找了。

但是假如有多个扩展都重写了类的某个方法,首先必然是执行某个Category的方法,这个Category在方法列表的最前面,那么多个Category的方法列表在类或元类的方法列表中的顺序是怎么决定的呢?

我们看到attachLists中扩展中的方法、属性和协议列表是在attachCategories中生成的,生成的过程:

生成方法、属性和协议列表
生成方法、属性和协议列表

通过i--来进行一个while循环,其中i是int i = cats->count;即cats的个数。而mlist、proplists和protolists是进行++操作的,操作的结果造成了先取出cats列表最后一个放到mlist,propcounts的最前面。cats是按照编译顺序排列的,所以后编译的cat中取出的方法、属性和协议列表,分别放在mlist、proplists和protolists的最前面。

所以isa指针也会按照上述顺序查找方法,即后编译的cats中的方法会先调用。

注:编译顺序可以在Xcode中查看和改变

文件编译顺序
文件编译顺序

Comments

添加新评论