一 Category 分类
1. Category源码
Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *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; // 分类所实现的协议列表
}
复制代码
从结构体可以看出,分类能
- 给类添加实例方法 (instanceMethod)
- 给类添加类方法 (classMethod)
- 实现协议 (protocol)
这个结构体里面根本没有属性列表,所以不能添加实例变量,即无法自动生成实例变量的setter和getter方法。当然,我们可以通过关联对象来实现分类对实例变量的添加.
分类不能添加属性的实质原因
我们知道在一个类中用@property声明属性,编译器会自动帮我们生成_成员变量和setter/getter,但分类的指针结构体中,根本没有属性列表。所以在分类中用@property声明属性,既无法生成_成员变量也无法生成setter/getter。
因此结论是:我们可以用@property声明属性,编译和运行都会通过,只要不使用程序也不会崩溃。但如果调用了_成员变量和setter/getter方法,报错就在所难免了。
Category编译之后的底层结构是 struct category_t(结构体),里面存储着分类的对象方法、类方法、属性、协议信息
struct category_t {
const char *name; //类的名字(name)
classref_t cls; //类(cls)
structmethod_list_t *instanceMethods;//category中所有给类添加的实例方法的列表(instanceMethods)
struct method_list_t *classMethods; //category中所有添加的类方法的列表(classMethods)
struct protocol_list_t *protocols; //category实现的所有协议的列表(protocols)
struct property_list_t *instanceProperties; //category中添加的所有属性(instanceProperties)
};
复制代码
从category_t的结构体也可以看出category可以添加实例方法,类方法,遵守协议,添加属性;但无法添加实例变量。
在category中可以有属性(property),但是该属性只是生成了getter和setter方法的声明,并没有产生对应的实现,更不会添加对应的实例变量。如果想为实例对象添加实例变量,可以尝试使用关联引用技术。
2. 关联对象(Objective-C Associated Objects)给分类增加属性
分类是不能直接添加属性,但是可以通过关联对象实现给分类添加属性。
① API介绍
关联对象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
ObjectAssociationMap
ObjcAssociation
复制代码
ObjcAssociation
中存储着value
和policy
这两个值。ObjectAssociationMap
中以key-value
的形式存储着传入的key
和ObjcAssociation
。AssociationsHashMap
中以key-value
的形式存储着disguised_object
和ObjectAssociationMap
。(第一个参数object
经过DISGUISE
函数被转化为了disguised_ptr_t
类型的disguised_object
)AssociationsManager
内部有一个AssociationsHashMap
对象,还有个自旋锁spinlock_t
原理总结/系统如何管理关联对象
首先系统中有一个全局AssociationsManager
,里面有个AssociationsHashMap
哈希表,哈希表中的key
是对象的内存地址,value
是ObjectAssociationMap
,也是一个哈希表,其中key
是我们设置关联对象所设置的key,value
是ObjcAssociation
,里面存放着关联对象设置的值和内存管理的策略。
以void objc_setAssociatedObject(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
二 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 文件中声明私有方法的非常好的方式。