Category
是 Objective-C 2.0
之后添加的特性,一般我们使用 Category
的场景主要可以动态地为已经存在的类扩展新的属性和方法。这样做的好处就是:
在runtime.h中查看定义中:
typedef struct objc_category *Category;
同样也是一个 objc_category
结构体,定义如下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
char *class_name OBJC2_UNAVAILABLE;
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
打开 objc
源代码,在objc-runtime-new.h中我们可以发现:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因。
注意:
OBJC2_UNAVAILABLE
之类的宏定义是苹果在 OC
中对系统运行版本进行约束的黑魔法,为的是兼容非 `Objective-C 2.0 的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。category_t
的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以添加协议,添加属性,但是不可以添加成员变量。下面看一个例子
Paste_Image.png
使用 clang
的命令去看看 category
到底会变成什么:
clang -rewrite-objc Person+Student.m
注意:
Paste_Image.png
解决方法 在终端中输入下面的命令:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
Paste_Image.png
然后打开文件目录,会发现多了一个 10w
多行的 .cpp
文件。
Paste_Image.png
通过 .cpp
文件中红我们可以看到:
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Student
和属性列表 _OBJC_$_PROP_LIST_Person_$_Student
,两者的命名都遵循了公共前缀+类名+category名字的命名方式。Student
这个 category
里面写的方法 study
,而属性列表里面填充的也正是我们在 Student
里添加的 age
属性。category
的名字用来给各种列表以及后面的 category
结构体本身命名,而且有 static
来修饰,所以在同一个编译单元里我们的 category
名不能重复,否则会出现编译错误。category
本身 _OBJC_$_CATEGORY_Person_$_Student
,并用前面生成的列表来初始化 category
本身。想深入了解 Category
的原理,请查看苹果的源码,这里是 objc4-680/
我们知道,Objective-C
的运行是依赖 runtime
,而 runtime
则依赖于 dyld
动态加载,在 objc-os.mm
文件中可以找到入口,它的调用栈简单整理如下:
Paste_Image.png
Category
被附加到类上面是在 map_images
的时候发生的,在 new-ABI
的标准下,_objc_init
里面的调用的 map_images
最终会调用 objc-runtime-new.mm
里面的 _read_images
方法,而在 _read_images
方法的结尾,有以下的代码片段:
Paste_Image.png
Paste_Image.png
从上面的代码中可以看出:
Category
和它的主类(或元类)注册到哈希表中;在这里分了两种情况进行处理:Category
中的实例方法和属性被整合到主类中;而类方法则被整合到元类中。另外,对协议的处理比较特殊,Category
中的协议被同时整合到了主类和元类中。
值得注意的是,在代码中有一小段注释 /* || cat->classProperties */
,根据注释可见苹果曾经计划利用 Category
来添加属性。addUnattachedCategoryForClass
只是把类和 category
做一个关联映射,然后在 remethodizeClass
真正的去做处理。
Paste_Image.png
通过 remethodizeClass
这个函数的主要作用是将 Category
中的方法、属性和协议整合到类(主类或元类)中,查看 attachCategories
这个函数:
Paste_Image.png
首先,通过 while
循环,遍历所有的 Category
,也就是参数 cats
中的 list
属性。对于每一个 Category
,得到它的方法列表 mlist
、proplist
和 protolist
并存入 mlists
、proplists
和 protolists
中。换句话说,它的主要作用就是将 Category
中的方法、属性和协议拼接到类(主类或元类)中,更新类的数据字段 data()
中 mlist
、proplist
和 protolist
的值。
通过以上可以看出:
Category
的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category
和原来类都有 methodA
,那么 category
附加完成之后,类的方法列表里会有两个 methodA
。Category
的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category
的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。