1、Category简介

一个类有不同的功能,可以创建不同的分类进行区分。
比如有一个Person类,自己有一个run方法:

  1. @interface Person : NSObject
  2. - (void)run;
  3. @end

给Person类新建两个分类:Person+Test.h和Person+Eat.h,并分别添加tes和eat方法方法:

  1. @interface Person (Test)
  2. - (void)test;
  3. @end
  4. @interface Person (Eat)
  5. - (void)eat;
  6. @end

导入Person.h和Person+Test.h头文件后,即可调用run和test方法

  1. Person *person = [[Person alloc] init];
  2. [person run];
  3. [person test];
  4. [person eat];

思考:如果person对象想调用test方法,需要通过isa指针找到类对象,在类对象中的方法列表中找到test方法进行调用,那么test方法是如何从分类中合并到主类中的?

2、Category的底层结构

将Person+Test.m转成.cpp文件(参考:OC代码转换),可以看到分类的结构体类型_category_t

  1. struct _category_t {
  2. const char *name;// 类名
  3. struct _class_t *cls;
  4. const struct _method_list_t *instance_methods; //对象方法列表
  5. const struct _method_list_t *class_methods; //类方法列表
  6. const struct _protocol_list_t *protocols; //协议列表
  7. const struct _prop_list_t *properties; //属性列表
  8. };

和通过_category_t创建的Test分类

  1. static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
  2. {
  3. "Person",
  4. 0, // &OBJC_CLASS_$_Person,
  5. (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
  6. 0,
  7. 0,
  8. 0,
  9. };

总结:每个分类都会在编译时生成一个_category_t类型的结构体,用来保存分类里的信息。

3、Cagegory的加载过程

通过查看 objc源码 ,在objc-runtime-new.mm中的attachCategories方法

  1. attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count, int flags)

传入类对象和分类列表(数据结构可能是:cls -> [Person class] 和 cats_list -> [category_t、category_t、…]),方法内部定义了三个数组,用于保存主类所有分类的方法、属性和协议信息。

  1. method_list_t *mlists[ATTACH_BUFSIZ];
  2. property_list_t *proplists[ATTACH_BUFSIZ];
  3. protocol_list_t *protolists[ATTACH_BUFSIZ];

再遍历所有分类结构体,将分类里的信息保存到三个数组中,最后再将数组中的数据,附加到主类的数据中

  1. // 获取主类对象的数据rwe
  2. auto rwe = cls->data()->extAllocIfNeeded();
  3. // 将所有分类的对象方法附加到主类对象的对象方法列表中
  4. rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
  5. // 将所有分类的属性附加到主类对象的属性列表中
  6. rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
  7. // 将所有分类的协议附件到主类对象的协议列表中
  8. rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);

attachLists方法的工作原理类似于动态数组的扩容过程,将插入的数据到原列表的前面。根据attachCategories分类遍历顺序,后编译的分类会放在数组(方法、属性、协议)的前面位置,以Person的方法列表为例,合并过程如下:
image.png

*实际上合并后的方法列表是个二维数组,这里为了方便观察顺序,图中没有体现。

Test分类因为是最后编译,所以test方法会放在合并后的方法列表里的最前面,且分类方法都会在主类方法前。
(分类的编译顺序由Xcode-Target-Build Phases-Complie Sources中文件排列顺序决定,排在上面的文件优先编译)
总结:
1、通过Runtime加载某个类的所有Category数据
2、把所有Category的方法、属性、协议数据,合并到一个大数组中
后面参与编译的Category数据,会在数组的前面
3、将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
4、如果分类中实现了主类方法,分类中的方法会被优先调用,如果多个分类都实现了,则后编译的分类优先被调用

4、Class Extension

Class Extension(类扩展)主要用于声明一些私有方法和属性

  1. @interface Person ()
  2. @property (nonatomic, assign) int age;
  3. - (void)jump;
  4. @end

Class Extension在编译的时候,它的数据已经包含在类信息中。
Category是在运行时,才会将数据信息合并到类信息中。