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/DeveloperPaste_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 的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。