之前写了一篇 文章 总结了 OC 中弱引用容器实现,在小米面试中提到其中 CFFoundation 的做法,面试官问了我一个问题,这样实现后在这些元素在被销毁后,还保留在容器中会有什么问题么?我马上意识到,这些元素会变成野指针,且之前只实现了引用计数的不变,而没有实现 Weak 特质,也就是没有在销毁后置 nil,也没有被移除,那么容器外界再访问时就会崩溃。看来之前考虑得还是太片面,也没有做更周全的实验。

所以看了 Runtime 源码和文章后,订正弱引用容器的一些实现方法。

Runtime 源码的 weak 关键字实现

源码基于 Runtime-709 分析,当我们使用 __weak 关键字时,实际上调用的是 NSObject.mm 中的 objc_initWeak() 方法,然后进入核心方法 storeWeak,传入核心参数是 location(弱引用指针的地址)和 newobj(弱引用指针应该指向的)核心方法源码如下:

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
//template 关键字类似于泛型,传入三个参数其实都是 bool 类型,为各种情况提供组合优化
static id //返回 id 类型
storeWeak(id *location, objc_object *newObj)
{   
    
    assert(haveOld  ||  haveNew);
    //新值没有的情况应该被上层的方法拦截,所以这里有个断言
    if (!haveNew) assert(newObj == nil);
    
    
    Class previouslyInitializedClass = nil;
    id oldObj;
    //SideTable 结构是引用计数表,记录着对象的 weak 表,目的就是为了获取 weak 表
    SideTable *oldTable;
    SideTable *newTable;

 retry:
    if (haveOld) {
        oldObj = *location;//因为是个指向地址的指针,用 * 解引用返回地址的对象
        oldTable = &SideTables()[oldObj];//获得旧值对象所在的表的指针
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];//获得新值对象生意所在的表的指针
    } else {
        newTable = nil;
    }
    
    //加锁
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    //再次确认保证线程安全
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // 防止弱引用机制的死锁并用 init 构造保证弱引用的 isa 指针非空
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
            previouslyInitializedClass = cls;
            goto retry;
        }
    }

    //清除旧址
    if (haveOld) {
        //在 weakTable 中反注册
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    //分配新值
    if (haveNew) {
        //在 weakTable 中注册新值并返回对象
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);


        // 对 TaggedPointer 的优化
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        *location = (id)newObj;
    }
    else {
    }
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

而 WeakTable 是一个 Hash 表设计,以对象的地址为 key,value 是所有指向这个对象的 weak 指针的地址集合。通过这种设计,在废弃对象时,可以通过 weak 表快速找到 value 即所有 weak 指针并统一设置为 nil 并删除该记录。

所以苹果对于 weak 的实现其实类似于通知的实现,指明谁(weak 指针)要监听谁(赋值对象)什么事件(dealloc 操作)执行什么操作(置 nil)。

动手实现弱引用置 nil

对于弱引用不增加引用计数,之前 文章 已经探讨过,现在目的是如何仿照苹果这种设计,去实现置 nil 的操作。苹果是为了多个弱引用指针指向同一个对象才使用了表,而需要其中一个指针置 nil 的关键在于,监听 dealloc 操作。而其实在 ARC 里,重写 dealloc 方法就可以,但是怎么样不入侵整个类的 dealloc 方法呢?这时突破点在于,dealloc 中做了什么,有没有办法在 dealloc 调用的其它方法入手。

而 MRC 中的 dealloc 方法实际上做了这些事情:

  1. 对自己所有强引用的属性发送 release 消息
  2. 对自己所有强引用的关联对象发送 release 消息
  3. ….
  4. 调用 [super dealloc]

这时我们发现,只要此时关联对象引用计数为 1,那么发送 release 消息后则会调用它的 dealloc 方法并销毁。

在它 dealloc 时置我们需要的指针为 nil 是合适的选择,因为之后的时机指向的对象也会被销毁,销毁之前置 nil 刚刚好。

为了保证关联对象的引用指针为 1,在 weak 赋值时只要创建一次就好了。由于我们需要置 nil 这个操作,关联对象的 dealloc 跑一个 block 是灵活性最大的选择了,也就是由关联对象持有一个 block,并在 weak 赋值时顺便告诉这个 block 里面执行什么。

@interface CDZDeallocObserver : NSObject
@property (nonatomic, copy) void (^block)(void);
@end

@implementation CDZDeallocObserver
- (void)dealloc{
    if (self.block) {
        self.block();
    }
}
@end

用分类实现一个 setBlock 的方法,拓展给 NSObject 类。

const void *CDZDellocBlockKey = &CDZDellocBlockKey;
@implementation NSObject (DeallocBlock)

- (void)cdz_deallocBlock:(void(^)(void))block{
    CDZDeallocObserver *observer = objc_getAssociatedObject(self, CDZDellocBlockKey);
    if (!observer) {
        observer = [CDZDeallocObserver new];
        objc_setAssociatedObject(self, CDZDellocBlockKey, observer, OBJC_ASSOCIATION_RETAIN);
    }
    observer.block = block;
}
@end

弱引用容器的拓展

之前我们知道 CoreFoundation 和 MRC 法都可以使引用计数不变,那么其实我们只要在 Add 的时候顺便关联上释放时移除对象即可,因为保留一堆 nil 指针在容器内并不是一个好的选择。置 nil 其实更希望是表示这个元素不存在了。

//与 CoreFoundation 配套的 add 方法,用 unsafe_unretain 是不希望被置 nil,系统置 nil 的时机可能比实际 dealloc 的时机早
- (void)cf_addObject:(id)object{
    [self addObject:object];
    __unsafe_unretained typeof (object) unRetainObject = object;
    __weak typeof(self) weakSelf = self;
    [object cdz_deallocBlock:^{
        if (unRetainObject) {
            [weakSelf removeObject:unRetainObject];
        }
    }];
}
//跟 mrc 配套的
- (void)mrc_addObject:(id)object{
    [self addObject:object];
    __unsafe_unretained typeof (object) unRetainObject = object;
    __weak typeof(self) weakSelf = self;
    [object cdz_deallocBlock:^{
        if (unRetainObject) {
            [weakSelf removeObject:unRetainObject];
        }
    }];
    CFRelease((__bridge CFTypeRef)(object));
}

这样才能保证后续在对象销毁时,用容器获取的对象是正确的(置 nil 或被移除)。

最后

更改后的方案 Demo 已经更新,之前对于内存管理的理解不够深入而导致错误的文章希望不要误导了太多人。

如果您觉得有帮助,不妨给个 star 鼓励一下,欢迎关注&交流

参考链接