iOS 关联对象

由问题 category能否添加成员变量,如果可以,如何给category添加成员变量?

答案:不可以直接给category添加成员变量,但是可以通过关联对象实现添加成员变量的效果

关联对象的本质

关联对象由AssociationsManager管理并在AssociationsHashMap存储
所有对象的关联内容都在同一容器中
...

我们在一个MJPerson类里用@property声明一个属性

@property (assign, nonatomic) int age;

比如上句
其实做了三件事

@interface MJPerson : NSObject
{
    int _age;             //第一步声明_age成员变量
}
- (void)setAge:(int)age;  //第二步声明age的set和get方法
- (int)age;
@end

@implementation MJPerson
- (void)setAge:(int)age   //第三步实现age的set和get方法
{
    _age = age;
}
- (int)age
{
    return _age;
}
@end

如果是在MJPerson的分类里用@property声明一个属性

@property (assign, nonatomic) int age;

比如上句,只会做下面事

- (void)setAge:(int)age;  //只会声明age的set和get方法
- (int)age;

既然分类的属性不帮我们实现成员变量和age的set和get方法的实现,那么我们能不能自己来实现呢。首先在分类里定义成员变量系统会报错,不允许。那么我们还是先把set和get方法实现,如下。

@implementation MJPerson
- (void)setAge:(int)age
{
    //_age = age;   //分类里成员变量不允许生成,所以这句话报废了
}
- (int)age
{
    //return _age;  //分类里成员变量不允许生成,所以这句话也报废了
}
@end

由此发现我们自己实现的set和get方法实现都是空架子,怎么办呢?

下面一步步来改造

新建MJPerson类
#import 
@interface MJPerson : NSObject
@property (nonatomic , assign) int age;
@end

#import "MJPerson.h"
@implementation MJPerson
@end

新建MJPerson分类
#import "MJPerson.h"
@interface MJPerson (Category)
@property (nonatomic , assign) int num;
@end

#import "MJPerson+Category.h"
@implementation MJPerson (Category)

int num_;//新建一个全局变量,既然分类不给我实现成员变量,那么我就自己找个变量中转下。在@implementation里声明的变量,其getter/setter 方法只在该类中可以。

- (void)setNum:(int)num {
    num_ = num;
}

- (int)num {
    return num_;
}
@end

然后控制器里如下调用
- (void)viewDidLoad {
    [super viewDidLoad];

    MJPerson *person = [[MJPerson alloc]init];
    person.age = 1;
    person.num = 2;
    NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
    
    MJPerson *person1 = [[MJPerson alloc]init];
    person1.age = 3;
    person1.num = 4;
    NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
    NSLog(@"person1的age = %d,person1的num = %d", person1.age, person1.num);
}
看到打印
2019-11-27 10:02:40.820360+0800 Test[1445:35155] person的age = 1,person的num = 2
2019-11-27 10:02:40.820468+0800 Test[1445:35155] person的age = 1,person的num = 4
2019-11-27 10:02:40.820542+0800 Test[1445:35155] person1的age = 3,person1的num = 4

虽然可以实现分类里的num的保存,但是可以看到因为num_是全局变量,所以person和person1的num用了同一份变量,显然这种方法不太靠谱,下面接着改造

新建MJPerson类
#import 
@interface MJPerson : NSObject
@property (nonatomic , assign) int age;
@end

#import "MJPerson.h"
@implementation MJPerson
@end

新建MJPerson分类
#import "MJPerson.h"
@interface MJPerson (Category)
@property (nonatomic , assign) int num;
@end

#import "MJPerson+Category.h"
@implementation MJPerson (Category)

NSMutableDictionary *dic;

+(void)load{
    dic = [NSMutableDictionary dictionary];
}

- (void)setNum:(int)num {
    NSString *key = [NSString stringWithFormat:@"%p",self];
    dic[key] = @(num);
}

- (int)num {
    NSString *key = [NSString stringWithFormat:@"%p",self];
    return [dic[key] intValue];
}
@end

然后控制器里如下调用
- (void)viewDidLoad {
    [super viewDidLoad];

    MJPerson *person = [[MJPerson alloc]init];
    person.age = 1;
    person.num = 2;
    NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
    
    MJPerson *person1 = [[MJPerson alloc]init];
    person1.age = 3; //3是存储在person1的对象内部
    person1.num = 4; //4是存储在全局的字典对象里面
    NSLog(@"person的age = %d,person的num = %d", person.age, person.num);
    NSLog(@"person1的age = %d,person1的num = %d", person1.age, person1.num);
}
看到打印
2019-11-27 10:08:57.343514+0800 Test[1551:38984] person的age = 1,person的num = 2
2019-11-27 10:08:57.343618+0800 Test[1551:38984] person的age = 1,person的num = 2
2019-11-27 10:08:57.343700+0800 Test[1551:38984] person1的age = 3,person1的num = 4

由此看到实现了num的保存,且互不干扰,但是这种做法有一定的线程安全问题,同时如果分类里属性过多,需要很多字典,异常复杂。

关联对象(将值和对象关联起来,所以叫关联对象)
假设分类里有个age属性,可以把set和get方法改造成下列形式

const void *ageKey = &ageKey; //ageKey指向的内存存的是自己的地址,定义为const void *是因为关联方法的key是这种类型的
- (void)setAge:(int)age
    //objc_setAssociatedObject添加关联对象
    //四个值
    //object :对象
    //key: 唯一值
    //value: 值
    //objc_AssociationPolicy:关联模式,可选copy,retain,assin。没有weak,没有weak意味着如果释放对象是直接销毁,不是置nil
    objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (int)age
    return [objc_getAssociatedObject(self, ageKey) intValue];
}

由此,完成了关联对象

如果新增分类属性name是string类型

const void *nameKey = &nameKey;
- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
    return objc_getAssociatedObject(self, nameKey);
}

上面代码定义为const全局变量了,能被外界访问,不太好(比如控制器里用extern来声明一个变量extern const void *nameKey,那么通过NSLog(@"%p",nameKey);就能打印出nameKey里面存的值(就是&nameKey))
改进
在const前个 static 让其变为局部变量

static const void *nameKey = &nameKey;
- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
    return objc_getAssociatedObject(self, nameKey);
}

因为key是void *类型,也就是只要是指针就行,所以key还可以更简单
static const char nameKey; //还保留const是因为key里面有const,所以为了一致性
然后key需要为&nameKey,因为不关心nameKey指针里存了什么,而是指针地址值

static const char nameKey;
-(void)setName:(NSString *)name {
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
}

还有一种很简单的方法,连key值都不用定义了

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC); //写@"name"也行的原因是因为key接收的是指针,@"name"等同于将NSString *str = @"name"的str传进去了,所以是可行的;
}
- (NSString *)name
{

    //return objc_getAssociatedObject(self, @selector(name));
  
    // get方法里 _cmd == @selector(name),_cmd代指当前方法,所以可以用_c md替代
    return objc_getAssociatedObject(self, @"name");
    
}

还有一种连key值都不用定义了的很简单的方法

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{

    //return objc_getAssociatedObject(self, @selector(name));
  
    // get方法里 _cmd == @selector(name),_cmd代指当前方法,所以可以用_cmd替代
    return objc_getAssociatedObject(self, _cmd);
}

上述论证很精彩,那么关联对象原理呢?
看源码
搜索objc_setAssociatedObject可以在objc-runtime.mm文件里看到这个方法

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可以在objc-references.mm文件看到方法_object_set_associative_reference

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;
    
    assert(object);
    
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;  //根据传进来的value以及策略生成一个new_value,基本也就是原值,额外匹配了内存管理的意思。
    {
        //关联对象管理类,C++实现的一个类
        AssociationsManager manager;
        
        //全局容器
        AssociationsHashMap &associations(manager.associations());
        
        //DISGUISE函数对传进来的object地址进行位运算,然后作为容器的key
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            //根据disguised_object指针查找对应的一个ObjectAssociationMap
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) { //找到了ObjectAssociationMap
                // 获取ObjectAssociationMap
                ObjectAssociationMap *refs = i->second;
                //根据key去查找ObjcAssociation
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {//查找到了ObjcAssociation
                    old_association = j->second;
                    //生成新的ObjcAssociation替换掉ObjectAssociationMap里key对应的 的value
                    j->second = ObjcAssociation(policy, new_value);
                } else {//没有查到ObjcAssociation
                    //生成新的ObjcAssociation作为ObjectAssociationMap的value
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {//没找到ObjectAssociationMap,即该object第一次关联对象
                // 创建新的ObjectAssociationMap (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                //ObjectAssociationMap作为disguised_object对应的值
                associations[disguised_object] = refs;
                //然后根据策略和new_value组装一个ObjcAssociation,作为ObjectAssociationMap里的key对应的 value
                (*refs)[key] = ObjcAssociation(policy, new_value);
                //设置这个object是有了关联对象的
                object->setHasAssociatedObjects();
            }
        } else { //如果value是空
           //根据disguised_object指针查找对应的一个ObjectAssociationMap
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                //根据key去查找ObjcAssociation
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    //找到了并擦除ObjcAssociation。所以如果想取消已关联对象的一个值,可以传这个值为nil
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

点击AssociationsManager看到如下AssociationsManager定义如下:

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

在看这段代码之前,先看一下几个数据结构以及它们之间的关系。
实现关联对象技术的核心对象有 (看到map可以理解为字典)
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation

1.AssociationsManager 是顶级的对象,维护了一个从 spinlock_t 锁到 AssociationsHashMap 哈希表的单例键值对映射;
2.AssociationsHashMap 是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;
3.ObjectAssociationMap 是一个 C++ 中的 map ,维护了从 key 到 ObjcAssociation 的映射,即关联记录;
4.ObjcAssociation 是一个 C++ 的类,表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。
每一个对象地址对应一个 ObjectAssociationMap 对象,而一个 ObjectAssociationMap 对象保存着这个对象的若干个关联记录。

上述核心对象关系简言之就是:
首先objc_setAssociatedObject会创建一个AssociationsManager;AssociationsManager里面维护了一个AssociationsHashMap 哈希表的单例键值对映射,由AssociationsHashMap来存储所有的关联对象(这相当于把所有对象的关联对象都存在一个全局map里面),这个 map的的key是传进来的这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value对应ObjectAssociationMap。ObjectAssociationMap里面维护了从 key 到ObjcAssociation的映射,即关联记录,其中key就是关联对象里面自己定义的key值,value对应ObjcAssociation。ObjcAssociation表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。

上述过程比如有MJPerson类,并为其创建分类,那么初始化MJPerson的实例对象时,初始化几个实例对象,每个实例对象都分别调用分类的属性,那么这几个实例对象的指针地址分别会在AssociationsHashMap的key里占有一席之位。
AssociationsHashMap里就会存储如下
MJPerson的实例对象1的指针 :ObjectAssociationMap
MJPerson的实例对象2的指针 :ObjectAssociationMap
...

上述_object_set_associative_reference源码的大致意思就是
根据传进来的value以及策略生成一个new_value(看源码基本也就是原值,额外匹配了内存管理的意思)。
如果new_value为空,那么根据对象地址找到ObjectAssociationMap,然后擦除
如果new_value不为空,那么根据对象地址获取ObjectAssociationMap,如果ObjectAssociationMap不存在,那么会创建一个新的ObjectAssociationMap关联对象。获取ObjectAssociationMap后,根据外界传进来的key查找ObjcAssociation,如果ObjcAssociation不存在会新建并直接存入新的ObjcAssociation。如果ObjcAssociation存在,会持有旧的ObjcAssociation,存入新的ObjcAssociation,然后释放旧的ObjcAssociation。

看懂了 objc_setAssociatedObject 函数后,objc_getAssociatedObject 函数对我们来说就是小菜一碟了。这个函数先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil。

关联对象提供了以下API

添加关联对象
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)

如何定义 Weak Associated Object

然而有时候,安全起见,我们需要在对象和 associated object 之间建立 weak 关系,而非 assign 关系,如何实现呢?

首先定义一个简单对象,该对象提供一个被 weak 修饰的属性:
@interface WeakAssociatedObjectWrapper : NSObject

@property (nonatomic, weak) id object;

@end

@implementation WeakAssociatedObjectWrapper

@end

其次
@interface UIView (ViewController)

@property (nonatomic, weak) UIViewController *vc;

@end

@implementation UIView (ViewController)

- (void)setVc:(UIViewController *)vc {
    WeakAssociatedObjectWrapper *wrapper = [WeakAssociatedObjectWrapper new];
    wrapper.object = vc;
    objc_setAssociatedObject(self, @selector(vc), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIViewController *)vc {
    WeakAssociatedObjectWrapper *wrapper = objc_getAssociatedObject(self, _cmd);
    return wrapper.object;
}

@end

提醒

关联对象并不是存储在被关联对象本身内存中
关联对象存储在全局的统一的一个AssociationsManager中
设置关联对象为nil,就相当于是移除关联对象 // 类(分类).属性 = nil

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