iOS-底层-关联对象

前两篇文章我们学习了关于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. 字典一直在内存中,内存泄漏问题
  2. 不同的对象可能会在不同的线程同时访问这个字典,线程安全问题
  3. 比较麻烦

二. 使用关联对象给分类添加成员变量

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);
 }

三. 关联对象的原理

首先要知道,关联对象不是存储在原来实例对象和类对象里面

实例对象和类对象在内存中是这样的:

对象内存结构.png

但是关联对象内部是怎么实现的呢?

关联对象是通过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在哪呢?观察下图:

关联对象原理.png

其实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一个一个取值的。

总结:

  1. 关联对象不是存储在原来实例对象和类对象里面
  2. 关联对象存储在全局的统一的一个AssociationsManager中
  3. 设置某个关联对象为nil,就相当于是移除某个关联对象
  4. 移除所有的关联对象用objc_removeAssociatedObjects

Demo地址:关联对象

你可能感兴趣的:(iOS-底层-关联对象)