OC-AssociatedObject原理及weak关联对象的实现

1. 关联对象和类别添加属性的关系

一提到类别添加属性,就会联想到在get set方法里面通过关联对象来实现,有没有想过为什么,不这样做行吗?

先说为什么?

给类别添加属性get、set方法是有了,那么得有空间去存储数据,同时我们还要能够处理数据的内存避免发生内存泄漏,而此时association就恰好能满足我们的诉求。

我们通过系统提供的apiobjc_setAssociatedObject``objc_getAssociatedObject在set、get方法中进行写和读的操作;并且在宿主对象释放的时候,也会判断是否有关联对象,从而释放掉关联的对象,一切都那么的完美契合,所以一般我们都通过关联对象来实现

不用关联对象行吗?

我觉得是没有必然关系的,我们只要解决了类别中属性对应的数据的读写和内存管理就能实现类别添加属性的完整的能力,懂得自然就懂就不多说了。

2. 关联对象策略policy的retain和retain_nonatomic的区别

以前一直以为关联对象的OBJC_ASSOCIATION_RETAIN_NONATOMICOBJC_ASSOCIATION_RETAIN跟属性的类似,就是原子和非原子操作,直到在实际用的过程中,发现2者的差异,才决定去源码里面看看究竟

关联对象的整体实现可以参照探索AssociatedObject关联对象的内部实现;这里就不细说,主要探究这2者的差异

2.1 先看几个枚举定义
关联的策略

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

内部存取的策略

enum {
    OBJC_ASSOCIATION_SETTER_ASSIGN      = 0,
    OBJC_ASSOCIATION_SETTER_RETAIN      = 1,
    OBJC_ASSOCIATION_SETTER_COPY        = 3,            // NOTE:  both bits are set, so we can simply test 1 bit in releaseValue below.
    OBJC_ASSOCIATION_GETTER_READ        = (0 << 8),
    OBJC_ASSOCIATION_GETTER_RETAIN      = (1 << 8),
    OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8)
};

内部在存取关联对象的时候会根据这两个策略的组合来做不同的逻辑处理

2.2 存的逻辑

_object_set_associative_reference中会调用association.acquireValue()来retain新值

inline void acquireValue() {
        if (_value) {
            switch (_policy & 0xFF) {
            case OBJC_ASSOCIATION_SETTER_RETAIN: // 1
                _value = objc_retain(_value);
                break;
            case OBJC_ASSOCIATION_SETTER_COPY:
                _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
                break;
            }
        }
    }

看代码retain和retain_nonatomic是都会执行objc_retain

在设置完了之后,会判断是否需要释放旧值association.releaseHeldValue()

inline void releaseHeldValue() {
        if (_value && (_policy & OBJC_ASSOCIATION_SETTER_RETAIN)) { // retain or retain_nonatomic都会执行
            objc_release(_value);
        }
    }

看代码retain和retain_nonatomic是都会执行objc_release前提是之前已经设置过key对应的数据
2.3 取的逻辑

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

主要看retainReturnedValue``autoreleaseReturnedValue的差异

inline void retainReturnedValue() { // retain_nonatomic的不会走objc_retain而retain的policy则会执行
        if (_value && (_policy & OBJC_ASSOCIATION_GETTER_RETAIN)) { // OBJC_ASSOCIATION_GETTER_RETAIN = 1<<8
            objc_retain(_value);
        }
    }

    inline id autoreleaseReturnedValue() {
        if (slowpath(_value && (_policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE))) {
            return objc_autorelease(_value);
        }
        return _value;
    }

OBJC_ASSOCIATION_GETTER_RETAIN = 1<<8
OBJC_ASSOCIATION_GETTER_AUTORELEASE = 2<<8
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1
OBJC_ASSOCIATION_RETAIN = 01401

对照这几个值我lldb打印了一下:

# retain_nonatomic
     (lldb) p 1 & 1<<8
     (int) $0 = 0
     (lldb) p 1 & 2<<8
     (int) $1 = 0
 # retain
     (lldb) p 01401 & 1<<8
     (int) $0 = 256
     (lldb) p 01401 & 2<<8
     (int) $1 = 512
     (lldb)

差别就来了,OBJC_ASSOCIATION_RETAIN策略的是会走objc_retainobjc_autorelease流程,相当于返回了([[_value retain] autorelease]),而OBJC_ASSOCIATION_RETAIN_NONATOMIC则直接返回了_value
跟我了解的属性的nonatomic不是一个概念。

接下来来讲下association在实际开发中的运用,了解原理不只是为了了解原理,得灵活运用才有实际意义

3. 在对象释放的时候做些事情
  • 一般我们在做kvo、notification添加observer之后,dealloc里需要去removeObserver,忘记了就尴尬了,会发生异常
  • 假如我在分类中做了监听了,我不能在分类中覆写dealloc而且苹果也不建议我们去swizzle dealloc方法,那么我们就需要一个机制在对象释放的时候去做些清理的工作

其实这个需求可以简单描述为在对象释放的时候做一些额外的工作,了解对象dealloc的流程,我们发现对象释放时会移除关联对象如果有的话、移除weak引用如果有的话,那么我们就可以从这里来切入;

dealloc不是会移除关联对象吗,那么我们可以在关联对象释放的时候来做这些操作;

思路就是:给对象添加一个关联对象(关联对象弱引用宿主对象同时提供一个block回调可供外部设置),在关联对象释放的时候回调回来做额外的操作。

代码实现如下

typedef void(^HCBlock)(__unsafe_unretained NSObject *target);

/// 这个类主要是实现一些Association的应用场景
@interface NSObject (HCWillDealloc)

- (void)hc_doSthWhenDeallocWithBlock:(HCBlock)block;

@end

@interface HCAssociatedObject : NSObject

- (instancetype)initWithTarget:(NSObject *)target;
//- (instancetype)initWithBlock:(HCBlock)block target:(NSObject *)target;
- (void)addActionBlock:(HCBlock)block;

@end

这里支持设置多个回调,内部存储在关联对象的数组中,dealloc的时候遍历去执行回调

static char kHCAssociatedObjectKey;

@implementation NSObject (HCWillDealloc)

- (void)hc_doSthWhenDeallocWithBlock:(HCBlock)block {
    if (block) {
        // 这里尝试过设置一个HCBlock就生成一个HCAssociatedObject对象,然后将其追加到对象已有的NSMutableArray<HCAssociatedObject *>数组中,后面调试发现,在kvo remove observer的时候会crash;采用下面这种方式则没有该问题
        HCAssociatedObject *associatedObject = objc_getAssociatedObject(self, &kHCAssociatedObjectKey);
        if (!associatedObject) {
            associatedObject = [[HCAssociatedObject alloc] initWithTarget:self];
            objc_setAssociatedObject(self, &kHCAssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            // 这里用下面这句,在测试移除kvo的时候会异常。TODO:什么原因
            // objc_setAssociatedObject(self, &kHCAssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN);
        }
        [associatedObject addActionBlock:block];
    }
}

@end

@interface HCAssociatedObject ()
/*
 void *objc_destructInstance(id obj)
 {
 if (obj) {
 // Read all of the flags at once for performance.
 bool cxx = obj->hasCxxDtor();
 bool assoc = !UseGC && obj->hasAssociatedObjects();
 bool dealloc = !UseGC;
 
 // This order is important.
 if (cxx) object_cxxDestruct(obj);
 if (assoc) _object_remove_assocations(obj); // clear association
 if (dealloc) obj->clearDeallocating(); // clear weak and other
 }
 
 return obj;
 }
 */
@property (nonatomic, unsafe_unretained) NSObject *target;//这里不用weak是由于在target释放的时候,先释放关联对象,然后有weak引用会清除weak表数据,回调的地方拿到的就是nil了,使用unsafe_unretained
//@property (nonatomic, copy) HCBlock deallocBlock;
@property (nonatomic, strong) NSMutableArray<HCBlock> *deallocBlocks;

@end

@implementation HCAssociatedObject

- (instancetype)initWithTarget:(NSObject *)target {
    self = [super init];
    if (self) {
        _deallocBlocks = [NSMutableArray arrayWithCapacity:0];
        _target = target;
    }
    
    return self;
}

- (void)addActionBlock:(HCBlock)block {
    [self.deallocBlocks addObject:[block copy]];
}

//- (instancetype)initWithBlock:(HCBlock)block target:(NSObject *)target {
//    self = [super init];
//    if (self) {
//        _deallocBlock = block;
//        _target = target;
//    }
//
//    return self;
//}

- (void)dealloc {
    [_deallocBlocks enumerateObjectsUsingBlock:^(HCBlock  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        obj ? obj(_target) : nil;
    }];
}

@end

来个例子

- (void)testDoSthWhenDealloc {
    UIScrollView *tmpView = [UIScrollView new];
    [tmpView addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    [tmpView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
    [tmpView hc_doSthWhenDeallocWithBlock:^(NSObject * _Nonnull target) {
        [target removeObserver:self forKeyPath:@"backgroundColor"];
        NSLog(@"removeObserver:forKeyPath:backgroundColor");
    }];
    [tmpView hc_doSthWhenDeallocWithBlock:^(NSObject * _Nonnull target) {
        [target removeObserver:self forKeyPath:@"frame"];
        NSLog(@"removeObserver:forKeyPath:frame");
    }];
    /*
     使用OBJC_ASSOCIATION_RETAIN设置关联对象会有一下异常
     'Cannot remove an observer <ViewController 0x7f97a5f05e60> for the key path "backgroundColor" from <UIScrollView 0x7f97a787b000> because it is not registered as an observer.'
     */
}

在scrollView释放的时候就会执行设置的block回调了。

实现细节代码中有注释,有2个需要注意的点

  • 关联对象弱引用宿主对象target不能声明为weak,要使用unsafe_unretained -- 这是由于:在target释放的时候,先释放关联对象,然后有weak引用会清除weak表数据,回调的地方拿到的就是nil了
  • 在设置关联对象的时候,用OBJC_ASSOCIATION_RETAIN_NONATOMIC,用OBJC_ASSOCIATION_RETAIN会出现异常,回调的时候target已经是空了,这个可以参照上面关于这两个策略的内部实现区别
4. association属性的weak实现

看了关联对象的policy,发现咋没有weak,weak这么好用,assign又有时会出问题;能不能自己实现一个了?

4.1 先看assign的问题

- (void)testAssignCase {
     static char kTestAssignKey;
    {
        {
            UILabel *associatedLabel = [UILabel new];
            objc_setAssociatedObject(self, &kTestAssignKey, associatedLabel, OBJC_ASSOCIATION_ASSIGN);
        }
        UILabel *label = objc_getAssociatedObject(self, &kTestAssignKey); // EXC_BAD_ACCESS
    }
}

这个用例在关联对象associatedLabel是assign出了作用域没有持有者强持有它进而就释放了,然后去读就EXC_BAD_ACCESS,要是有weak就好了,释放了就置为空了,避免了异常的发生。

4.2 自己实现一个weak关联

我们要做的就是关联对象在释放的时候将宿主的该关联对象也移除,就可以避免由于assign的方式访问了非法内存的异常了

前面已经介绍了一个应用,在对象释放的时候做些事情,那么我们在关联对象释放的时候,将宿主对象对应的该key的关联对象设置为nil,那么外部读的时候就是个nil,就避免了异常

上代码:

/// 设置关联对象不支持weak的方式
/// @param object 宿主对象
/// @param key 关联key
/// @param value 关联的对象
extern void objc_setWeakAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value);

void objc_setWeakAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value) {
    if (value) {
        //__weak typeof(object) weakObj = object;
        [value hc_doSthWhenDeallocWithBlock:^(NSObject *__unsafe_unretained  _Nonnull target) {
            objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_ASSIGN); // clear association
        }];
    }
    objc_setAssociatedObject(object, key, value, OBJC_ASSOCIATION_ASSIGN); // call system imp
}

这里实现细节就是在关联对象释放的时候,调用objc_setAssociatedObject(object, key, nil, OBJC_ASSOCIATION_ASSIGN)这样就把宿主对象的该key的关联对象清除了,外部读这个key的关联对象就是nil

测试用例:

- (void)testWeakCase {
    // 如果关联对象也支持weak这种特性就好了,关联的对象释放了,自动置空,宿主对象再次获取拿到的是个nil
    static char kTestWeakKey;
    {
        {
            UILabel *associatedLabel = [UILabel new];
            objc_setWeakAssociatedObject(self, &kTestWeakKey, associatedLabel);
            //objc_setAssociatedObject(self, &kTestWeakKey, associatedLabel, OBJC_ASSOCIATION_ASSIGN);
            //objc_setAssociatedObject(self, &kTestWeakKey, nil, OBJC_ASSOCIATION_ASSIGN);
        }
        UILabel *label = objc_getAssociatedObject(self, &kTestWeakKey);
        NSLog(@"label = %@", label); // 输出结果:null
    }
}

输出结果符合预期,完事。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345