分类(Category) VS 扩展(Extension)
一、Category 分类
1. Category底层实现
我们知道在一个类中用@property声明属性,编译器会自动帮我们生成成员变量(下划线开头的变量)和setter/getter。但是在分类中添加属性,系统不会生成对应的成员变量以及set和get方法实现,只会生成set和get方法的声明,更不会添加对应的实例变量。所以如果在分类中用@property声明属性,编译和运行都会通过,只要不使用程序也不会崩溃。但如果调用了成员变量和setter/getter方法,报错就在所难免了。下面我们来探索下分类不能添加实例变量的原因。
通过clang编译之后发现Category的底层结构是struct category_t
(结构体),里面存储着分类的对象方法、类方法、属性、协议信息
struct _category_t {
const char *name; // 分类名
struct _class_t *cls; // 分类所属的类名
const struct _method_list_t *instance_methods;// 实例方法列表
const struct _method_list_t *class_methods; // 类方法列表
const struct _protocol_list_t *protocols; // 分类所实现的协议列表
const struct _prop_list_t *properties;//属性列表
};
从结构体可以看出,分类能
- 给类添加实例方法 (instanceMethod)
- 给类添加类方法 (classMethod)
- 实现协议 (protocol)
- 添加属性 (property) (一般通过关联对象的方式)
通过
可以看出方法列表,属性列表,协议列表都是可读可写的,但是成员变量列表是只读的。这也说明一个类生成之后,编译时就已经把成员列表信息放在class_ro_t
中,不允许再动态的修改。以上都证明不能在分类中添加成员变量。
面试题:Category中能不能添成员变量?为什么?
答:Category中不能添加成员变量。因为objc_class结构体中ivars成员变量信息是放在只读的class_ro_t结构体中,类一旦生成,就不能动态的添加成员变量。(这也说明了一个类生成之后,编译时就已经把成员列表信息放在class_ro_t中,不允许再动态的修改。) Category本身的底层结构category_t中也只保存了方法、属性和协议等信息,并没有保存成员变量信息。综合来说是Category中不能添加成员变量。
2. 关联对象给分类增加属性
分类可以通过关联对象(Objective-C Associated Objects)的方式添加属性。
关联对象Runtime提供了下面几个接口:
//关联对象,传入nil则可以移除已有的关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除一个对象的所有关联对象。
void objc_removeAssociatedObjects(id object)
key
值必须保证唯一性,有以下三种推荐的key 值
- 声明static char kAssociatedObjectKey;使用&kAssociatedObjectKey作为key值;
- 声明static void *kAssociatedObjectKey = &kAssociatedObjectKey;使用kAssociatedObjectKey作为key值;
- 用 selector,使用getter方法的名称作为key值。
objc_AssociationPolicy
的枚举值和说明
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象。相当于@property(assign)/@property(unsafe_unretained)
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性。@property(nonatomic,strong)
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性。@property(nonatomic,copy)
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性。@property(atomic,strong)
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性。@property(atomic,copy)
};
在绝大多数情况下,我们都会使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
的关联策略,这可以保证我们持有关联对象。
objc_removeAssociatedObjects
函数我们一般是用不上的,因为这个函数会移除一个对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给objc_setAssociatedObject函数传入 nil 来移除某个已有的关联对象。
② 使用示例
为系统类添加属性:OC类型name和简单类型age
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) int age;
- (void)setName:(NSString *)name {
/**
* 为某个类关联某个对象
*
* @param object#> 要关联的对象 description#>
* @param key#> 要关联的属性key description#>
* @param value#> 你要关联的属性 description#>
* @param policy#> 添加的成员变量的修饰符 description#>
*/
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
/**
* 获取到某个类的某个关联对象
*
* @param object#> 关联的对象 description#>
* @param key#> 属性的key值 description#>
*/
return objc_getAssociatedObject(self, @selector(name));
}
NSString * const recognizerAge = @"kAge";
- (void)setAge:(int)age{
objc_setAssociatedObject(self, (__bridge const void *)(kAge), @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (int)age{
return [objc_getAssociatedObject(self, (__bridge const void *)(kAge)) intValue];
}
3. Category的代码格式
@interface 待扩展的类(分类的名称)
@end
@implementation 待扩展的名称(分类的名称)
@end
4. Category的方法会“覆盖”原来类的同名方法吗?
-
Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA
-
Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,很开心的返回了,不会在理会后面的同名方法。
-
同名方法的调用,是根据编译顺序决定的,对于“覆盖”掉的方法,会先找到最后一个编译的category里的对应方法。可查看项目的 Build Phases -> Compile Sources,位置越往后,越晚编译。
5. Category 作用
- 减少单个文件的体积
- 把不同的功能分配到不同的分类里,便于管理
- 可以按需加载想要的分类
- 把Framework私有方法公开
- 模拟多继承(另外可以模拟多继承的还有protocol) 例如:
ibireme大神开源的YYCategories,针对系统的类使用分类拓展的小功能,很实用。
空白页框架DZNEmptyDataSet,通过对UIScrollView使用分类功能,完美的处理了空白页面的展示
6. Objective-C Associated Objects 的实现原理
关联对象的方法是
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
实现关联对象技术的核心对象有四个:
AssociationsManager
:内部有一个AssociationsHashMap
哈希表AssociationsHashMap
:以key-value
的形式存储着disguised_object
和ObjectAssociationMap
。(第一个参数object
经过DISGUISE
函数被转化为了disguised_ptr_t
类型的disguised_object
)ObjectAssociationMap
:以key-value
的形式存储着设置关联对象传入的key
和ObjcAssociation
。ObjcAssociation
:存储着设置关联对象传入的值(value)和内存管理的策略(policy)
关系链:AssociationsManager-->AssociationsHashMap-->ObjectAssociationMap-->ObjcAssociation
以void objc_setAsso ciatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)
为例,首先会通过AssociationsManager
获取AssociationsHashMap
,然后以object的内存地址为key,从AssociationsHashMap
中取出ObjectAssociationMap
,若没有,则新创建一个ObjectAssociationMap
,然后通过key获取旧值,以及通过key和policy生成新值ObjcAssociation(policy, new_value)
,把新值存放到ObjectAssociationMap中,若新值不为nil,并且内存管理策略为retain,则会对新值进行一次retain,若新值为nil,则会删除旧值,若旧值不为空并且内存管理的策略是retain,则对旧值进行一次release
// 简化后的伪代码:
class AssociationsManager {
static AssociationsHashMap *_mapStore;
}
// DenseMap是个map,存放key,value
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
class ObjcAssociation {
uintptr_t _policy;
id _value;
}
原理图如下
二、Extension扩展
Extension是Category的一个特例。类扩展与分类相比只少了分类的名称,所以称之为“匿名分类”。类扩展听上去很复杂,但其实我们很早就认识它了.就是我们平时在.m文件里使用的
@interface ViewController ()
//私有属性
//私有方法
@end
1. Extension的作用
- 声明私有属性
- 声明私有方法
- 声明私有成员变量
2. Extension的特点
- 编译时决议
- 只以声明的形式存在,多数情况下寄生在宿主类的.m中
- 一般的私有属性写到.m文件中的类扩展中
- 不能为系统类添加扩展
3. Extension的代码格式
@interface XXX ()
//私有属性
//私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
@end
三、Category和Extension的区别
- 分类是运行时决议;扩展是编译时决议;(所以扩展中声明的方法没有被实现,编译器会报警,但是分类种的方法没有被实现编译器是不会有任何警告的)
- 分类原则上只能增加方法,并且是公开的(无法直接添加属性,可以通过runtime添加属性,原因通过runtime可以解决无setter/getter的问题);扩展能添加方法,实例变量,默认是@private类型的,且只能作用于自身类,而不是子类或者其他地方;
- 分类有自己的实现部分;扩展无自己的实现部分,只能依托对应类的实现部分来实现;
- 分类可以为系统类添加分类;扩展不可以为系统类添加扩展(必须有一个类的源码才能添加一个类的Extension);
- 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。
转载自:https://juejin.cn/post/6981314318558035998