详解iOS中的关联对象

首发于个人我的博客html

从给分类添加属性提及

详解iOS中分类Cateogry 一文中,咱们提出一个问题,c++

Category可否添加成员变量?若是能够,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,可是能够间接实现Category有成员变量的效果,用关联对象技术

那这里就详细说明git

添加属性,实际上都作了什么

首先咱们要回忆一下,添加属性,实际上作了三件事github

  • 生成成员变量
  • 生成set方法和get方法的声明
  • 生成set方法和get方法的实现

eg: 定义一个 YZPerson 类,并定义age属性数组

#import <Foundation/Foundation.h>

@interface YZPerson : NSObject

@property (assign, nonatomic) int age;


@end

复制代码

就至关于干了三件事安全

  • 生成成员变量_age
  • 生成set方法和get方法的声明
  • 生成set方法和get方法的实现 以下
#import <Foundation/Foundation.h>


@interface YZPerson : NSObject

{
    int _age;
}
- (void)setAge:(int)age;
- (int)age;

@end



#import "YZPerson.h"

@implementation YZPerson
- (void)setAge:(int)age{
    _age = age;
}

- (int)age{
    return _age;
}
@end

复制代码

那在分类中添加属性怎么就不行?

先说结论

  • 生成成员变量_age
  • 不会生成set方法和get方法的声明
  • 不会生成set方法和get方法的实现

不会生成set方法和get方法的实现

定义一个分类 YZPerson+Ext.h,而后添加属性weightbash

#import "YZPerson.h"
@interface YZPerson (Ext)
@property (nonatomic ,assign)  int weight;
@end
复制代码

使用app

YZPerson *person = [[YZPerson alloc] init];
person.weight = 10;
复制代码

会直接报错,ide

iOS-关联对象[1009:10944] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff3550d063 __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x00007fff6ac8e06b objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff355961bd -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff354b34b4 ___forwarding___ + 1427
	4   CoreFoundation                      0x00007fff354b2e98 _CF_forwarding_prep_0 + 120
	
	6   libdyld.dylib                       0x00007fff6c0183f9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Program ended with exit code: 9
复制代码

reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10' 可知,分类中添加属性,没有生成set方法和get方法的实现函数

会生成set方法和get方法的声明

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)
- (void)setWeight:(int)weight{
    
}
- (int)weight{
    return 100;
}
@end

复制代码

而后再调用

YZPerson *person = [[YZPerson alloc] init];
person.age = 25;
person.weight = 10;
NSLog(@"person.age = %d",person.age);
NSLog(@"person.weight = %d",person.weight);
复制代码

输出

2019-07-10 08:28:04.406972+0800 iOS-关联对象[1620:18520] person.age = 25
2019-07-10 08:28:04.407291+0800 iOS-关联对象[1620:18520] person.weight = 100
复制代码

进一步证实了,不会生成set方法和get方法的实现,可是会生成set方法和get方法的声明,由于若是没有生成set方法和get方法的声明,这个方法就不能调用。

咱们还能够这样:在YZPerson+Ext.h文件中声明了weight,而后再YZPerson+Ext.m中写实现的时候,会有提示的

更加说明了是有声明的。

分类中不能直接定义成员变量

#import "YZPerson.h"


@interface YZPerson (Ext)
{
    int _weight; // 报错 Instance variables may not be placed in categories
}
@property (nonatomic ,assign)  int weight;
@end

复制代码

会直接报错Instance variables may not be placed in categories,成员变量不能定义在分类中

源码角度证实

前面的文章详解iOS中分类Cateogry 中分析过源码,objc-runtime-new.h中分类结构体是这样的

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);
};
复制代码

可知,这个结构体中,没有数组存放成员变量,只有属性,协议等。

怎么来完善属性

有什么办法能够实如今分类中添加属性和在类中添加属性同样的效果么?答案是有的

方案一 用全局变量

分类YZPerson+Ext.m中定义全局变量 _weight

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

int _weight;

- (void)setWeight:(int)weight{
    _weight = weight;
}
- (int)weight{
    return _weight;
}
@end

复制代码

使用时候

YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);
复制代码

输出为

iOS-关联对象[1983:23793] person.weight = 103
复制代码

看起来确实能够,而后实际上咱们不能这么用,由于,全局变量是共享的,假设有两个 Person,第二个Person修改了weight属性,而后打印第一个Person.weight

YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);

YZPerson *person2 = [[YZPerson alloc] init];
person2.weight = 10;
NSLog(@"person.weight = %d",person.weight);
复制代码

输出为

iOS-关联对象[1983:23793] person.weight = 103
iOS-关联对象[1983:23793] person.weight = 10
复制代码

可知,修改了Person2.weight 会改变Person.weight的值,由于是全局变量的缘故。因此这种方法不行

方案二 用字典

既然前面方案不能用的缘由是全局变量,共享一份,那咱们是否是只要保证,一对一的关系,是否是就能够了呢?

定义 字典weights_ 以对象的地址值做为key来,weight的值做为value来存储和使用

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

NSMutableDictionary *weights_;

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 写在这里,保证s只初始化一次
        weights_ = [NSMutableDictionary dictionary];
    });
}

- (void)setWeight:(int)weight{
    NSString *key = [NSString stringWithFormat:@"%p",self];//self 地址值做为key
    weights_[key] = @(weight);//字典中的value不能直接放int,须要包装成对象
}
- (int)weight{
     NSString *key = [NSString stringWithFormat:@"%p",self];
    return  [weights_[key] intValue];
}

@end
复制代码

这样的话,使用起来,就不会由于不一样对象而干扰了 结果以下

存在的问题

  • 由于是全局的,存在内存泄露问题
  • 线程安全问题,多个线程同时访问的话,有线程安全问题
  • 代码太多,若是每次增长一个属性,都要写好多代码。不利于维护

关联对象方案

关联对象的使用

下面先简单说明关联对象的使用

动态添加

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
复制代码
  • 参数一:id object : 给哪一个对象添加属性,这里要给本身添加属性,用self
  • 参数二:void * == id key : key值,根据key获取关联对象的属性的值,在objc_getAssociatedObject中经过次key得到属性的值并返回。
  • 参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
  • 参数四:objc_AssociationPolicy policy: 策略,属性以什么形式保存。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
};
复制代码

整理成表格以下

objc_AssociationPolicy 对应的修饰符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

eg: 咱们在代码中使用了 OBJC_ASSOCIATION_RETAIN_NONATOMIC 就至关于使用了 nonatomicstrong 修饰符。

注意点 上面列表中,没有对应weak修饰的策略, 缘由是 object通过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object

disguised_ptr_t disguised_object = DISGUISE(object);
复制代码

weak修饰的属性,当没有拥有对象以后就会被销毁,而且指针置为nil,那么在对象销毁以后,虽然在map中仍然存在值object对应的AssociationsHashMap,可是由于object地址已经被置为nil,会形成坏地址访问而没法根据object对象的地址转化为disguised_object了,这段话能够再看彻底文以后,再回来体会下。

取值

objc_getAssociatedObject(id object, const void *key);
复制代码
  • 参数一:id object : 获取哪一个对象里面的关联的属性。
  • 参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即经过key值取出value

移除关联对象

- (void)removeAssociatedObjects
{
    // 移除关联对象
    objc_removeAssociatedObjects(self);
}
复制代码

具体应用

#import "YZPerson.h"

@interface YZPerson (Ext)
@property (nonatomic,strong) NSString *name;
@end


#import "YZPerson+Ext.h"
#import <objc/runtime.h>
@implementation YZPerson (Ext)

const void *YZNameKey = &YZNameKey;

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

- (NSString *)name{
   return objc_getAssociatedObject(self, YZNameKey);
}

- (void)dealloc
{
    objc_removeAssociatedObjects(self);
}

@end
复制代码

使用的时候,正常使用,就能够了

YZPerson *person = [[YZPerson alloc] init];
person.name = @"jack";

YZPerson *person2 = [[YZPerson alloc] init];
person2.name = @"rose";
        
NSLog(@"person.name = %@",person.name);
NSLog(@"person2.name = %@",person2.name);
复制代码

输出

iOS-关联对象[4266:52285] person.name = jack
iOS-关联对象[4266:52285] person2.name = rose
复制代码

使用起来就是这么简单

关联对象原理

四个核心对象

实现关联对象技术的核心对象有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

源码解读

关联对象的源码在 Runtime源码

objc_setAssociatedObject

查看objc-runtime.mm类,首先找到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());
        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);
}

复制代码

如图所示

_object_set_associative_reference函数内部咱们能够找到咱们上面说过的实现关联对象技术的四个核心对象。接下来咱们来一个一个看其内部实现原理探寻他们之间的关系。

AssociationsManager

查看 AssociationsManager 咱们知道AssociationsManager 内部有static AssociationsHashMap *_map;

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;
    }
};
复制代码

AssociationsHashMap

接下来看 AssociationsHashMap

上图中 AssociationsHashMap的源码咱们发现AssociationsHashMap继承自unordered_map首先来看一下unordered_map内的源码

unordered_map源码中咱们能够看出 参数 _Key_Tp 对应着map中的KeyValue,那么对照上面AssociationsHashMap的源码,能够发现_Key中传入的是unordered_map<disguised_ptr_t_Tp中传入的值则为ObjectAssociationMap *

而后 咱们查看ObjectAssociationMap的源码,上图中ObjectAssociationMap已经标记出,咱们能够知道ObjectAssociationMap中一样以keyValue的方式存储着ObjcAssociation

ObjcAssociation

接着咱们来到ObjcAssociation中,能够看到

class ObjcAssociation {
        uintptr_t _policy; // 策略
        id _value; // value值
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
        ObjcAssociation() : _policy(0), _value(nil) {}

        uintptr_t policy() const { return _policy; }
        id value() const { return _value; }
        
        bool hasValue() { return _value != nil; }
    };
复制代码

从上面的代码中,咱们发现ObjcAssociation存储着_policy_value,而这两个值咱们能够发现正是咱们调用objc_setAssociatedObject函数传入的值,换句话说咱们在调用objc_setAssociatedObject函数中传入valuepolicy这两个值最终是存储在ObjcAssociation中的。

如今咱们已经对四个核心对象AssociationsManagerAssociationsHashMapObjectAssociationMapObjcAssociation之间的关系有了初步的了解,那么接下继续仔细阅读源码,看一下objc_setAssociatedObject函数中传入的四个参数分别放在哪一个对象中充当什么做用

细读 _object_set_associative_reference

_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);
    // 根据value的值经过acquireValue函数获取获得new_value
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        // 获取 manager 内的 AssociationsHashMap 也就是 associations
        AssociationsHashMap &associations(manager.associations());
        // object 通过 DISGUISE 函数被转化为了disguised_ptr_t类型的disguised_object
        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;
                    // policy和new_value 做为键值对存入了ObjcAssociation
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // policy和new_value 做为键值对存入了ObjcAssociation
                    (*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 {
            // 来到这里说明,value为空
            // 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);
}
复制代码

**acquireValue**内部实现 经过对策略的判断返回不一样的值

static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}
复制代码
  • 首先根据咱们传入的value通过acquireValue函数处理返回了new_valueacquireValue函数内部实际上是经过对策略的判断返回不一样的值
typedef uintptr_t disguised_ptr_t;
    inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
    inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
复制代码
  • 以后建立AssociationsManager manager,获得manager内部的AssociationsHashMapassociations。 以后咱们看到了咱们传入的第一个参数object通过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
复制代码
  • 以后被处理成new_valuevalue,和policy一块儿被存入了ObjcAssociation中。 而ObjcAssociation对应咱们传入的key被存入了ObjectAssociationMap中。 disguised_objectObjectAssociationMap则以key-value的形式对应存储在associations中也就是AssociationsHashMap中。

value为空

若是传入的value为空,那么就删除这个关联对象

// 来到这里说明,value为空
            // 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);
                }
            }
复制代码

本文参考资料:

表格总结

用表格总结来展现这几个核心类的关系以下

小结

  • 关联对象并不存储在被关联对象自己内存中,而是有一个全局统一的 AssociationsManager
  • 一个实例对象就对应一个ObjectAssociationMap
  • ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation
  • ObjcAssociation中存储着关联对象的valuepolicy策略

objc_getAssociatedObject

objc_getAssociatedObject内部调用的是_object_get_associative_reference

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}
复制代码

_object_get_associative_reference函数

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);
        // 查找 disguised_object
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            //查看key 和value
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // 存在key 和value 就取出对应的值
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        // 不存在key value 就把这个关联对象擦除
        objc_autorelease(value);
    }
    return value;
}
复制代码

关键代码已经在上文中给了注释

objc_removeAssociatedObjects函数

objc_removeAssociatedObjects函数用来删除全部关联对象,内部调用了_object_remove_assocations

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
复制代码

_object_remove_assocations

再来看看_object_remove_assocations

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) { // 遍历AssociationsHashMap 取出值
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            // 删除
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}
复制代码

代码中能够看出,接受一个object对象,而后遍历删除该对象全部的关联对象

总结

用表格总结来展现这几个核心类的关系以下

  • 关联对象并不存储在被关联对象自己内存中,而是有一个全局统一的 AssociationsManager
  • 一个实例对象就对应一个ObjectAssociationMap
  • ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation
  • ObjcAssociation中存储着关联对象的valuepolicy策略
  • 删除的时候接收一个object对象,而后遍历删除该对象全部的关联对象
  • 设置关联对象_object_set_associative_reference的是时候,若是传入的value为空就删除这个关联对象

本文参考资料:

本文相关代码github地址 github

Runtime源码

iOS底层原理

更多资料,欢迎关注我的公众号,不定时分享各类技术文章。

相关文章
相关标签/搜索