什么是关联对象?
一个对象可以关联多个对象,可以扩展原有对象的能力,关联是拥有的关系。
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不为空
- 通过AssociationsManager以线程安全方式初始化AssociationsHashMap
- 取被关联对象的地址作为AssociationsHashMap的key,遍历AssociationsHashMap找到该对象的ObjectAssociationMap
- 如果有找到对应的ObjectAssociationMap,更新值操作
- 根据key遍历ObjectAssociationMap,找到对应的ObjcAssociation则更新关联对象的值
- 没有找到直接实例化一个ObjcAssociation,存到ObjectAssociationMap,被关联对象的isa结构体中的标志位has_assoc标记为ture,标识当前对象有关联对象
- 没有找到对应的ObjectAssociationMap,初始化操作
- 实例化一个ObjectAssociationMap
- 实例化一个ObjcAssociation,以key和ObjcAssociation为键值对的方式存到ObjectAssociationMap,被关联对象的isa结构体中的标志位has_assoc标记为ture,标识当前对象有关联对象
- new_value为nil
- 删除对象的关联对象
- 根据对象的地址遍历AssociationsHashMap,找到该对象的ObjectAssociationMap
- 根据key遍历ObjectAssociationMap,找到目前关联对象
- 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;
}
解析
- 获取AssociationsHashMap对象
- 根据被关联对象的地址找到对应的ObjectAssociationMap
- 根据key遍历ObjectAssociationMap找到对应的ObjcAssociation
- 找到返回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());
}
这个函数一般少用,因为会移除调一个对象的所有关联对象,很有可能把别人需要的关联对象移除了。
总结
- 被关联对象和关联对象的存储并没有直接的联系,是通过哈希表管理
- 使用弱引用的关联对象可能被释放了,但是没有被移除,使用这个关联对象会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是线程安全的,是没有必要使用原子修饰。