关联对象源码分析

1,082 阅读6分钟

什么是关联对象?

一个对象可以关联多个对象,可以扩展原有对象的能力,关联是拥有的关系。

Case1: Category可以使用@property添加一个属性吗?

@interface NSString (MyNSString)
@property (nonatomic, copy) NSString *name;
@end

警告是name的存取方法需要手动实现,或者通过@dynamic在运行时实现存取方法。

//强制使用
NSString *test = @"test";
test.name = @"name";
NSLog(@"name is %@", test.name);

//crash
-[__NSCFConstantString setName:]: unrecognized selector sent to instance 0x1062ff5b0

正确的做法:

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
    return objc_getAssociatedObject(self, _cmd);
}
//_cmd在Objective-C的方法中表示当前方法的selector,同self表示当前方法调用的对象实例

Case2: 关联block

//.h
typedef void(^ButtonClickCallBack)(UIButton *);
@interface UIButton (HandlerClickButton)
- (void)handleClickCallBack:(ButtonClickCallBack)callBack;
@end
//.m
static NSString *btnActionKey = @"btnAction";
@implementation UIButton (HandlerClickButton)
- (void)handleClickCallBack:(ButtonClickCallBack)callBack {
    objc_setAssociatedObject(self, &btnActionKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction {
    ButtonClickCallBack callBack = objc_getAssociatedObject(self, &btnActionKey);
    if (callBack) {
        callBack(self);
    }
}
@end
//use
[self.testBtn handleClickCallBack:^(UIButton * _Nonnull btn) {
    NSLog(@"click with call back");
}];

case3: UIView关联ErrorView

@interface UIView(ErrorHandler)  
@property (nonatomic,strong) IBOutlet UIView  * errorToastView;
...
@end
...
- (UIView*)errorToastView {
    UIView *errorToastView_ = (UIView*)objc_getAssociatedObject(self, @selector(errorToastView));
    if (errorToastView_ && !errorToastView_.superview) {
        [self addSubview:errorToastView_];
    }
    return errorToastView_;
}
- (void)setTtErrorToastView:(UIView *)errorToastView {
    objc_setAssociatedObject(self, @selector(errorToastView),errorToastView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

如何关联对象?

OC实现

Runtime提供了3个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)

objc_setAssociatedObject参数说明

id object const void *key id value objc_AssociationPolicy policy
被关联的对象 关联的key,唯一 关联的对象 内存管理的策略

key推荐使用方法的selector,可以很好的保证唯一性,并且省去使用静态指针写法的代码。

OBJC_ASSOCIATION_ASSIGN OBJC_ASSOCIATION_RETAIN_NONATOMIC OBJC_ASSOCIATION_COPY_NONATOMIC OBJC_ASSOCIATION_RETAIN OBJC_ASSOCIATION_COPY
assign nonatomic, strong nonatomic, copy atomic, strong atomic, copy

Java实现

class Computer { 
    public void work() { 
        System.out.println("working..."); 
    } 
} 
 
class Person {
    private Computer computer ;
    public Person(Computer computer) {
        this.computer = computer ;
    }
    public void develop() {
        computer.work() ;
        System.out.println("working fast and fast");
    }
}

Q1:关联对象存储方式?

Q2:有什么坑?

实现原理

源码:opensource.apple.com/tarballs/ob… (runtime.h、objc-runtime.mm、objc-references.mm)

objc_setAssociatedObject

具体实现方法:_object_set_associative_reference,下面看下几个关键类

AssociationsManager

// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock, and calling its assocations()
// method lazily allocates the hash table.

spinlock_t AssociationsManagerLock;//自旋锁

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 *AssociationsManager::_map = NULL;

AssociationsManager通过自旋锁维护一个AssociationsHashMap单例,初始化是通过加锁、采用懒汉式创建一个AssociationsHashMap单例,保证该map创建是线程安全的。

AssociationsHashMap

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
};

ObjectAssociationMap 则保存了从 key 到关联对象 ObjcAssociation 的映射,即保存了当前对象对应的所有关联对象

ObjcAssociation

class ObjcAssociation {
        //实例变量
        uintptr_t _policy; //内存管理策略
        id _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是关联对象的数据模型
  • 所有对象的关联对象由AssociationsManager管理并存储在AssociationsHashMap,是一个无序的哈希表
  • 对象的指针以及其对应ObjectAssociationMap以键值对的形式存储在AssociationsHashMap中
  • ObjectAssociationMap存储该对象所有关联对象的数据结构
  • 每一个对象都有一个标记位 has_assoc标识对象是否含有关联对象,便于删除

_object_set_associative_reference

// retain the new value (if any) outside the lock.
    //临时对象,用于持有原有的关联对象,便于最后释放值
    ObjcAssociation old_association(0, nil);
    //临时变量存下入参要关联的对象的值
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        //线程安全方式初始化AssociationsHashMap
        AssociationsManager manager;
        //初始化AssociationsHashMap
        AssociationsHashMap &associations(manager.associations());
        //取被关联对象的地址作为AssociationsHashMap的key遍历找到ObjectAssociationMap
        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;
                //根据key找到ObjcAssociation
                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
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                //实例化一个关联对象存到ObjectAssociationMap
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            //删除对应key的关联对象
            // 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);

解析

根据new_value是否为空分为更新关联对象和删除关联对象两种情况

  • new_value不为空
    1. 通过AssociationsManager以线程安全方式初始化AssociationsHashMap
    2. 取被关联对象的地址作为AssociationsHashMap的key,遍历AssociationsHashMap找到该对象的ObjectAssociationMap
    3. 如果有找到对应的ObjectAssociationMap,更新值操作
    4. 根据key遍历ObjectAssociationMap,找到对应的ObjcAssociation则更新关联对象的值
    5. 没有找到直接实例化一个ObjcAssociation,存到ObjectAssociationMap,被关联对象的isa结构体中的标志位has_assoc标记为ture,标识当前对象有关联对象
    6. 没有找到对应的ObjectAssociationMap,初始化操作
    7. 实例化一个ObjectAssociationMap
    8. 实例化一个ObjcAssociation,以key和ObjcAssociation为键值对的方式存到ObjectAssociationMap,被关联对象的isa结构体中的标志位has_assoc标记为ture,标识当前对象有关联对象
  • new_value为nil
    1. 删除对象的关联对象
    2. 根据对象的地址遍历AssociationsHashMap,找到该对象的ObjectAssociationMap
    3. 根据key遍历ObjectAssociationMap,找到目前关联对象
    4. erase删除ObjectAssociation

objc_getAssociatedObject

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // 取关联对象的hashmap
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 取得被关联对象的地址
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 根据被关联对象的地址找到对应的ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        // 遍历
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            //根据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);
    }
    //返回关联对象ObjcAssociation的值
    return value;
}

解析

  1. 获取AssociationsHashMap对象
  2. 根据被关联对象的地址找到对应的ObjectAssociationMap
  3. 根据key遍历ObjectAssociationMap找到对应的ObjcAssociation
  4. 找到返回ObjcAssociation的value值,反之返回nil

objc_removeAssociatedObjects

实现方法:_object_get_associative_reference

void objc_removeAssociatedObjects(id object)
{
    //hasAssociatedObjects确认对象是否存在关联对象
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

void _object_remove_assocations(id object) {
    //创建一个vector,存放对象关联的所有对象,加快释放
    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()) {
            // 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());
}

这个函数一般少用,因为会移除调一个对象的所有关联对象,很有可能把别人需要的关联对象移除了。

总结

  1. 被关联对象和关联对象的存储并没有直接的联系,是通过哈希表管理
  2. 使用弱引用的关联对象可能被释放了,但是没有被移除,使用这个关联对象会Crash
@property (nonatomic, assign) NSTimeInterval timestamps;
- (NSTimeInterval)timestamps {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimestamps:(NSTimeInterval)timestamps {
    objc_setAssociatedObject(self, @selector(timestamps), [NSNumber numberWithDouble:timestamps], OBJC_ASSOCIATION_ASSIGN);
}
//-[CFNumber retain]: message sent to deallocated instance 0x637892393544

第三行代码很有可能会报野指针崩溃,因为set的时候使用了OBJC_ASSOCIATION_ASSIGN内存策略,objc_setAssociatedObject执行完,关联的对象([NSNumber numberWithDouble:timestamps])就被释放了,但是ObjectAssociationMap保存了原对象的地址,所以objc_getAssociatedObject取值就会Crash了。解决方法是用OBJC_ASSOCIATION_RETAIN_NONATOMIC

思考🤔:关联对象set、get、remove是线程安全的,为啥还要原子修饰属性?

几种内存管理修饰保持跟声明属性的修饰作用是一致的,其中原子修饰是保证set和get方法对属性读写是线程安全,但是会造成性能降低问题,既然关联对象set、get、remove是线程安全的,是没有必要使用原子修饰。

参考资料

  1. cloud.tencent.com/developer/a…
  2. opensource.apple.com/tarballs/ob…
  3. nshipster.com/associated-…