前两篇文章我们学习了关于Category的知识Category分类和load和initialize,现在再看一个问题,Category能否添加成员变量?如果可以,如何给Category添加成员变量?
带着疑问,我们进行本文学习。
一. 如何给分类添加成员变量
我们知道,如果在类中添加如下属性,
@property (assign, nonatomic) int age;
编译器会自动帮我们做下面三件事:
1.生成_开头的成员变量
{
int _age;
}
2.生成set、get方法的声明
- (void)setAge:(int)age;
- (int)age;
3.生成set、get方法的实现
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
在Category分类中,我们知道Category的底层是这样的:
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; //属性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
可以看出,分类也可以添加属性,但是在分类中添加属性只会生成set、get方法的声明,不会生成_开头的成员变量,也不会生成set、get方法的实现。
- (void)setAge:(int)age;
- (int)age;
可能你会想,既然编译器不做另外两件事,我们自己手动添加不就好了,我们在分类中添加如下代码:
{
int _age;
}
运行,直接报错:
Instance variables may not be placed in categories
现在我们知道了,分类中不能直接添加成员变量,其实从分类的底层结构也能看出来,分类并没有存放成员变量的地方。
分类中可以添加属性,但是分类中添加属性只会生成set、get方法的声明,分类中又不能直接添加成员变量,但是可以使用关联对象间接实现Category有成员变量的效果。
set、get方法的实现我们可以自己写,这个好解决,成员变量怎么办呢?首先你肯定会想到用全局变量
int name_;
- (void)setName:(int)name
{
name_ = name;
}
- (int)name
{
return name_;
}
但是,不能使用全局变量,使用全局变量所有的person对象使用的都是一个name_值了。
上面没有做到一对一,如果使用字典就能做到一对一了,可以实现需求吗?
//把当前对象的指针当做key
#define MJKey [NSString stringWithFormat:@"%p", self]
@implementation MJPerson (Test)
NSMutableDictionary *names_;
+ (void)load
{
names_ = [NSMutableDictionary dictionary]; //姓名
}
- (void)setName:(NSString *)name
{
names_[MJKey] = name;
}
- (NSString *)name
{
return names_[MJKey];
}
运行:
MJPerson *person = [[MJPerson alloc] init];
person.age = 10; // 10存放在peron对象内部
person.name = @"haha"; //"haha"存放在全局的字典里面
打印:
person - age is 10, name is haha
可以发现,使用字典把当前对象的指针当做key,也是可以实现,但是这样也有问题:
- 字典一直在内存中,内存泄漏问题
- 不同的对象可能会在不同的线程同时访问这个字典,线程安全问题
- 比较麻烦
二. 使用关联对象给分类添加成员变量
1. 关联对象API
默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现。
关联对象提供了以下API:
① 添加关联对象:
/**
设置关联对象
@param object 传入的对象,会和value关联起来,最后一个对象对应一个value
@param key 传入一个指针作为key,取值的时候用
@param value 关联的值,一个value关联一个object
@param policy 关联策略
*/
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
关于关联策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, //相当于assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //相当于strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //相当于copy, nonatomic
OBJC_ASSOCIATION_RETAIN = 01401, //相当于strong, atomic
OBJC_ASSOCIATION_COPY = 01403 //相当于copy, atomic
};
② 获取关联对象:
/**
获取关联对象
@param object 和value关联的对象
@param key 根据传入的指针key取值
@return 取值返回的就是value
*/
id objc_getAssociatedObject(id object, const void * key)
③ 移除所有的关联对象:
/**
移除某个对象所有的关联对象
*/
void objc_removeAssociatedObjects(id object)
关于key,这个key要求传入一个指针,设置关联对象传入的key和获取关联对象传入的key要一样,下面有四种方式设置key:
static void *MyKey = &MyKey;// MyKey存的是自己的地址值,而且没必要赋值,因为我们只用地址
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
static char MyKey; //char类型的MyKey只占一个字节,内存占用更小
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
//使用属性名作为key
//直接将字符串字面量传进去,就相当于将字符串的地址值传进去,因为NSString *p = @"property"
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
//使用get方法的@selecor作为key,就相当于将SEL对象的地址值传进去 (推荐)
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
这四种方式都可以,优劣性请参考注释,推荐第四种,简单易懂可读性高。
关于@selector(getter),只要传入的名字是一样的,那么返回的SEL对象(底层是指向结构体的指针)地址值都是一样的。
2. 关联对象的使用
理论讲完了,下面我们看看如何使用:
MJPerson类里面添加一个age属性,MJPerson+Test.h分类里面添加name和weight属性:
#import "MJPerson.h"
@interface MJPerson (Test)
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) int weight;
@end
分类中添加属性只会生成set、get方法的声明,接下来在MJPerson+Test.m分类里面设置关联对象:
//name
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, @selector(name));
}
//weight
- (void)setWeight:(int)weight
{
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight
{
return [objc_getAssociatedObject(self, @selector(weight)) intValue];
}
我们使用了第四种key的写法,是不是很简单,接下来使用一下试试:
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.name = @"jack";
person.weight = 30;
MJPerson *person2 = [[MJPerson alloc] init];
person2.age = 20;
person2.name = @"rose";
person2.name = nil;
person2.weight = 50;
NSLog(@"person - age is %d, name is %@, weight is %d", person.age, person.name, person.weight);
NSLog(@"person2 - age is %d, name is %@, weight is %d", person2.age, person2.name, person2.weight);
打印:
person - age is 10, name is jack, weight is 30
person2 - age is 20, name is (null), weight is 50
打印可知,使用完全没有问题。
补充1:
上面的object是id类型的,所以理论上给什么对象添加关联都可以,上面我们是给实例对象添加关联对象,因为每个实例对象都不一样。如果给类对象添加关联对象,也是可以的,但是由于类对象只有一个,添加的关联对象也是唯一的,这样做没什么意义(MJExtension中有给MJProperty类对象添加关联对象)。
补充2:关于_cmd
_cmd是当前方法的@selector,就是@selector(当前方法名)。
其实每个方法都有两个隐式参数:self和_cmd,比如上面的name方法也可以写成:
- (NSString *)name:(id)self _cmd:(SEL)_cmd
{
// 隐式参数
// _cmd == @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
三. 关联对象的原理
首先要知道,关联对象不是存储在原来实例对象和类对象里面。
实例对象和类对象在内存中是这样的:
但是关联对象内部是怎么实现的呢?
关联对象是通过Runtime实现的,想要知道它内部是怎么实现的可以直接查看源码,打开objc4,搜索“ objc_setAssociatedObject(”
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
进入_object_set_associative_reference
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
//根据传入的object生成一个key
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
可以发现,关联对象就是通过上面这个函数实现的,我们发现实现关联对象技术的核心对象有:
AssociationsManager 关联对象管理者
AssociationsHashMap (以后看到Map就直接联想为字典)
ObjectAssociationMap
ObjcAssociation
我们就先梳理他们四个之间的关系:
首先进入AssociationsManager,发现里面有一个AssociationsHashMap
class AssociationsManager {
// associative references: object pointer -> PtrPtrHashMap.
static AssociationsHashMap *_map;
......
}
进入AssociationsHashMap,发现里面又有一个map,其中key是disguised_ptr_t,value是ObjectAssociationMap
class AssociationsHashMap : public unordered_map
进入ObjectAssociationMap,发现里面也有一个map,其中key是void *,value是ObjcAssociation
class ObjectAssociationMap : public std::map
进入ObjcAssociation,发现里面只有_policy和_value两个值:
class ObjcAssociation {
uintptr_t _policy;
id _value;
......
}
可以发现,我们传进去的_policy和_value,存放在ObjcAssociation里面,并没有在原来实例对象和类对象里面。
那么我们传进去的object和key在哪呢?观察下图:
其实disguised_ptr_t就是object,void *就是key,_object_set_associative_reference函数的源码就不分析了,其实它的内部就是按照上图根据两个嵌套map实现的。
同样,我们查看objc_getAssociatedObject源码:
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
发现,的确也是根据两个key一个一个取值的。
总结:
- 关联对象不是存储在原来实例对象和类对象里面
- 关联对象存储在全局的统一的一个AssociationsManager中
- 设置某个关联对象为nil,就相当于是移除某个关联对象
- 移除所有的关联对象用objc_removeAssociatedObjects
Demo地址:关联对象