category(分类)和 extension(类扩展)是日常开发中经常使用的,下面对它们进行对比分析。
Category (分类)
Category 使用场景
- 为一个类添加新的方法,可以为系统的类扩展功能
- 将一个类的不同功能,拆解成多个 category,减少单个文件的体积
- 创建对私有方法的前向引用: 声明私有方法,把 Framework 的私有方法公开等。直接调用其他类的私有方法时编译器会报错的,这时候可以创建一个该类的分类,在分类中声明这些私有方法(不必提供方法实现),接着导入这个分类的头文件就可以正常调用这些私有方法。
- 模拟多继承(另外使用 protocol 也可以模拟多继承)
Category 中可以添加哪些内容
可以添加实例方法、类方法、协议、属性(只生成 setter 和 getter 方法的声明,不会生成 setter 和 getter 方法的实现以及下划线成员变量)
Category 编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,这时候分类中的数据还没有合并到类中,而是在程序运行的时候通过Runtime机制将所有分类数据合并到类(类对象、元类对象)中去。这是分类最大的特点,也是分类和类扩展的最大区别,类扩展是在编译的时候就将所有数据都合并到类中去了。
OC中类的结构体定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
编译之后,objc_class 结构体的内存布局已经固定,不可能再向这个结构体中添加数据。 ivars 是一个结构体指针,指向的是一个固定区域,无法修改。不过 methodList 是指针的指针,即指针变量当中存的是一个地址,你可以改变这个地址的值从而改变最终指向的变量。所以可以修改 *methodList 的值来增加方法。因此,可以在运行时动态添加方法,不能添加成员变量。但可以添加关联对象。
Category 实现原理
通过 Clang 将以下分类代码转换为 C++ 代码,来分析分类的底层实现。
1 | //文件:Message+Test.h |
1 | //文件:Message+Test.m |
执行如下命令进行编译:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Message+Test.m |
得到编译后的文件 Message+Test.cpp
,从中选取部分源码如下:
1 | // 分类结构体 |
从上可知,分类编译之后的底层结构体是 struct category_t
,分类中可以添加实例方法、类方法、协议、属性,但是不能添加成员变量,因为没有存储成员变量对应的指针变量。
分类内容加载过程
在编译时,Category 中的数据还没有合并到类中,而是在程序运行的时候通过Runtime机制将所有分类数据合并到类(类对象、元类对象)中去。下面我们来看一下 Category 的加载处理过程:
- 通过 Runtime 加载某个类的所有 Category 数据.
- 把所有的分类数据(方法、属性、协议),合并到一个数组中.(后面参与编译的 Category 数据,会排在数组的前面)
- 将合并后的分类数据(方法、属性、协议),插入到宿主类原来数据的前面.(所以会优先调用最后参与编译的分类中的同名方法)
Extension 类扩展
Extension 有一种说法叫“匿名分类”,因为它很像分类,但没有分类名。它的作用是将原来放在 .h 中的数据放到 .m 中去,私有化,变成私有的声明。
Extension 是在编译的时候就将所有数据都合并到类中去了(编译时决议),而 Category 是在程序运行的时候通过Runtime机制将所有数据合并到类中去(运行时决议)。
类扩展一般用于为类添加私有属性,私有方法,私有成员变量。
Category | Extension |
---|---|
运行时决议 | 编译时决议 |
可以有声明,可以有实现 | 只以声明的形式存在,多数情况下寄生于宿主类的.m中 |
可以为系统的类添加分类 | 不能为系统类添加扩展 |
相关面试题
Q:Category 能否添加成员变量?如果可以,如何给 Category 添加成员变量?
因为分类底层结构的限制,不能直接给 Category 添加成员变量,但是可以通过关联对象间接实现 Category 有成员变量的效果。
Q:为什么分类中属性不会自动生成 setter、getter 方法的实现,不会生成成员变量,也不能添加成员变量?
因为类的内存布局在编译的时候会确定,但是分类是在运行时才加载,在运行时Runtime会将分类的数据,合并到宿主类中。
Q:为什么将以前的方法列表挪动到新的位置用 memmove 呢?
为了保证挪动数据的完整性。而将分类的方法列表合并进来,不用考虑被覆盖的问题,所以用 memmove 就好。
Q:为什么优先调用最后编译的分类的方法?
attachCategories()方法中,从所有未完成整合的分类取出分类的过程是倒序遍历,最先访问最后编译的分类。然后获取该分类中的方法等列表,添加到二维数组中,所以最后编译的分类中的数据最先加到分类二维数组中,最后插入到宿主类的方法列表前面。而消息传递过程中优先查找宿主类中靠前的元素,找到同名方法就进行调用,所以优先调用最后编译的分类的方法。
Q:objc_class 结构体中的 baseMethodList 和 methods 方法列表的区别?
回答此道问题需要先了解Runtime的数据结构objc_class。
baseMethodList基础的方法列表,是只读的,不可修改,可以看成是合并分类方法列表前的methods的拷贝;而methods是可读写的,将来运行时要合并分类方法列表。
Q:Category 中有 +load 方法吗?+load 方法是什么时候调用的?+load 方法能继承吗?
分类中有+load方法;
+load方法在Runtime加载类、分类的时候调用;
+load方法可以继承,但是一般情况下不会手动去调用+load方法,都是让系统自动调用。