iOS——weak修饰符的学习补充

Weak修饰符的内部机制

SideTable

ObjectC中对对象的存储,实现上做了一定的优化,一旦有弱引用对象被赋值,即运行时(Runtime)会在全局的SideTables中分配一个SideTable空间,此空间是根据对象的地址相关算法获取到的一个位置(所以存在多个对象分配到同一个位置,类似哈希冲突)。其中SideTable结构如下:

cpp 复制代码
struct SideTable {
    //SideTable的结构
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    //
    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void reset() { slock.reset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

slock:自旋锁,用于多线程环境下的同步。

RefcountMap:引用计数映射表,管理对象的引用计数。键值为对象指针,对应value为引用的一些标记位以及状态

weak_table:弱引用表,管理对象的弱引用。是一个散列表。

我们先来看一下weak_table_t的结构:

cpp 复制代码
struct weak_table_t {
    weak_entry_t *weak_entries;//hash数组,用来存储弱引用对象的相关信息weak_entry_t。
    size_t    num_entries;//hash数组中的元素个数。
    uintptr_t mask;//参与判断引用计数辅助量。hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)。
    uintptr_t max_hash_displacement;//可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)。
};

这个SideTable中,比较重要的就是RefcountMapweak_table,这俩都是用来记录引用计数的散列表。其中RefcountMap的作用是记录SideTable中对象的强引用的引用计数,而weak_table,是用于存储弱引用的,并且在必要的时候更新对象的弱引用,比如说对象被释放的时候更新该对象的所有弱引用为nil,或者当该对象的弱引用指向其他对象的时候也要更新;

我们来看一下weak_entry_t 的代码:

cpp 复制代码
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
   DisguisedPtr<objc_object> referent; // 封装 objc_object 指针,即 weak 修饰的变量指向的对象
   union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1;  
                                                        
            uintptr_t        num_refs : PTR_MINUS_1;    // 引用数值,这里记录弱引用表中引用有效数字,即里面元素的数量
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;     // hash 元素上限阀值
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];     
        };
    };
};

它使用了一种优化空间使用的方式。

当弱引用的数量不多时,使用结构体内部的一个固定大小的数组(inline_referrers)。这类似于枚举中的小数据范围直接内联存储在结构体中,避免了动态内存分配的开销。

当弱引用的数量超过固定大小时,转而使用指向动态分配内存的指针(referrers),这类似于在需要更大存储空间时,枚举或其他结构体使用指针指向外部分配的存储区域。

其中out_of_line的值通常情况下是等于零的,所以弱引用表总是一个objc_objective指针数组,当超过4时, 会变成hash表。

生命状态

一个对象被创建出来后,如果被强引用就增加SideTable中的refcnts的信息,如果被弱引用就增加weak_table的信息。如果弱引用减少,会从weak_table中删除对应的引用信息;如果是refcnts对应的对象,如果是deallocating状态,会把该对象从refcnts移除掉。所以SideTables空间作为一个全局内存,会一直存在,没有回收的概念。

Weak的工作流程

weak的实现原理概括为以下三步:

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,清理对象的记录。

Weak的初始化

runtime会调用objc_initWeak函数,objc_initWeak函数会初始化一个新的weak指针指向对象的地址。

  • objc_initWeak:
cpp 复制代码
// location指针objc , newObj原始对象object
id objc_initWeak(id *location, id newObj) {
	// 查看原始对象实例是否有效
	// 无效对象直接导致指针释放
    if (!newObj) {
        *location = nil;
        return nil;
    }
    // 这里传递了三个 bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    return storeWeak<false/*old*/, true /*new*/, true/*crash*/>
    (location, (objc_object*)newObj);
}

添加引用:

objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

cpp 复制代码
// HaveOld:  true - location指向了一个旧对象
//          false - location当前没有指向任何对象,或者旧对象已经被清理
// HaveNew:  true - 需要被分配的新值,当前值可能为 nil
//          false - 不需要分配新值
// CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
//          false - 用 nil 替代存储
template bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
    // 该过程用来更新弱引用指针的指向
    // 初始化 previouslyInitializedClass 指针
    Class previouslyInitializedClass = nil;
    id oldObj;
    // 声明两个 SideTable
    // ① 新旧散列创建
    SideTable *oldTable;
    SideTable *newTable;
    // 获得新值和旧值的锁存位置(用地址作为唯一标示)
    // 通过地址来建立索引标志,防止桶重复
    // 下面指向的操作会改变旧值
retry:
	// 如果weak ptr之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTable,即获取其旧的Table
    if (HaveOld) {
        // 更改指针,获得以 oldObj 为索引所存储的值地址
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {  // 如果weak ptr之前没有弱引用过一个obj,则oldTable = nil
        oldTable = nil;
    }
    // 如果weak ptr要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTable
    if (HaveNew) {
        // 更改新值指针,获得以 newObj 为索引所存储的值地址
        newTable = &SideTables()[newObj];
    } else {  // 如果weak ptr不需要引用一个新obj,则newTable = nil
        newTable = nil;
    }
    // 加锁操作,防止多线程中竞争冲突
    SideTable::lockTwoHaveOld, HaveNew>(oldTable, newTable); 
    // 避免线程冲突重处理
    // location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改,需要返回上边重新处理
    if (HaveOld  &&  *location != oldObj) {
        SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
        goto retry;
    }
    // 防止弱引用间死锁
    // 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
    if (HaveNew  &&  newObj) {
        // 获得新对象的 isa 指针
        Class cls = newObj->getIsa();
        // 如果cls还没有初始化,先初始化,再尝试设置weak
        if (cls != previouslyInitializedClass  &&
            !((objc_class *)cls)->isInitialized()) {
            // 解锁
            SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
            // 对其 isa 指针进行初始化
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
            // 如果该类已经完成执行 +initialize 方法是最理想情况
            // 如果该类 +initialize 在线程中
            // 例如 +initialize 正在调用 storeWeak 方法
            // 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记,防止改if分支再次进入
            previouslyInitializedClass = cls;
            // 重新获取一遍newObj,这时的newObj应该已经初始化过了
            goto retry;
        }
    }
    // ② 清除旧值
    //  如果之前该指针有弱引用过一个obj那就得需要清除之前的弱引用
    if (HaveOld) {
    	// 如果weak_ptr之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    // ③ 分配新值
    // 如果weak_ptr需要弱引用新的对象newObj
    if (HaveNew) {
    	// (1) 调用weak_register_no_lock方法,将weak ptr的地址记录到newObj对应的weak_entry_t中
    	// 如果弱引用被释放 weak_register_no_lock 方法返回 nil
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
                                                      (id)newObj, location,
                                                      CrashIfDeallocating);
        // (2) 更新newObj的isa的weakly_referenced bit标志位
        if (newObj  &&  !newObj->isTaggedPointer()) {
            // 弱引用位初始化操作
            // 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
            newObj->setWeaklyReferenced_nolock();
        }
        // (3)*location 赋值,也就是将weak ptr直接指向了newObj,也就是确保其指针指向是正确的。可以看到,这里并没有将newObj的引用计数+1
        *location = (id)newObj;
    }
    else {
        // 没有新值,则无需更改
    }
    // 解锁,其他线程可以访问oldTable, newTable了
    SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
    // 返回newObj,此时的newObj与刚传入时相比,设置了weakly-referenced bit位置1
    return (id)newObj;
}

这段代码的作用是更新一个弱引用指向。这里先判断被更新的对象有没有指向旧的对象,要是有,就获取以该旧对象地址为key的在SideTable中的value,然后将这个value放到旧散列表oldTable中。要是没有,就把oldTable置为nil;

然后更新新散列表newTable,如果需要弱引用一个新值,就将newTable的值值为newObj在SideTable中的值,否则将newTable值为nil;

然后是一个避免线程冲突的处理,location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改,需要返回上边重新处理,并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向。

然后清除该指针的旧值:如果之前该指针有弱引用过一个obj那就得需要清除之前的弱引用,如果weak_ptr之前弱引用过别的对象oldObj,则调用 weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址

然后为该指针分配新值:调用weak_register_no_lock方法,将weak ptr的地址记录到newObj对应的weak_entry_t中。如果弱引用被释放 weak_register_no_lock 方法返回 nil。更新newObj的isa的weakly_referenced bit标志位。*location 赋值,也就是将weak ptr直接指向了newObj,也就是确保其指针指向是正确的。可以看到,这里并没有将newObj的引用计数+1

weak_register_no_lock:把新的对象进行注册操作,完成与对应的弱引用表进行绑定操作。

cpp 复制代码
/*	weak_table:weak_table_t结构类型的全局的弱引用表。
	referent_id:weak指针所指的对象。
	*referrer_id:weak修饰的指针的地址。
	crashIfDeallocating:如果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。
*/
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;
 
    // 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;
 
    // 确保被引用的对象可用(没有在析构,同时应该支持weak引用)
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {  //不能被weak引用,直接返回nil
        BOOL (*allowsWeakReference)(objc_object *, SEL) =
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent,
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }
    // 正在析构的对象,不能够被弱引用
    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }
 
    // now remember it and where it is being stored
    // 在 weak_table中找到referent对应的weak_entry,并将referrer加入到weak_entry中
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) { // 如果能找到weak_entry,则讲referrer插入到weak_entry中
        append_referrer(entry, referrer);     // 将referrer插入到weak_entry_t的引用数组中
    }
    else { // 如果找不到,就新建一个
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }
 
    // Do not set *referrer. objc_storeWeak() requires that the
    // value not change.
 
    return referent_id;
}
  • 如果referent为nil或referent采用了TaggedPointer计数方式,直接返回,不做任何操作。
  • 如果对象不能被weak引用,直接返回nil。
  • 如果对象正在析构,则抛出异常。
  • 如果对象没有再析构且可以被weak引用,则调用weak_entry_for_referent方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer方法向其中插入weak指针地址。否则新建一个weak_entry。

Weak的释放

调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?

当释放对象时,其基本流程如下:

1、调用objc_release。用于减少对象的引用计数。当对象的引用计数变为 0 时,对象会被释放。

2、因为对象的引用计数为0,所以执行dealloc。dealloc 是对象的析构函数,用于执行对象释放前的清理工作。

3、在dealloc中,调用了_objc_rootDealloc函数。是根析构函数,执行对象的最终释放操作。

4、在_objc_rootDealloc中,调用了object_dispose函数。用于释放对象的内存并进行相关清理操作。

5、调用objc_destructInstance。用于销毁对象实例,包括释放实例变量。

6、最后调用objc_clear_deallocating。用于处理所有指向该对象的弱引用,将它们清零(nil)。

weak指针管理中使用了3个HashTable!!!

其中两个使用oc对象作为index!!!还有一个使用弱引用指针的地址作为index计算方式

除了全局SideTables(), 其他两个的HashTable都使用开放寻址法作为Hash碰撞解决方法!!!

weak修饰的属性,如何自动置为nil的?

Runtime维护了一个Weak表,用于存储指向某个对象的所有Weak指针。 Weak表其实是一个哈希表,Key是所指对象的地址,Value是Weak指针的地址(这个地址的值是所指对象的地址)的数组。 在对象被回收的时候,经过一层层调用,会最终触发(clearDeallocating)方法将所有Weak指针的值设为nil。

weak 的实现原理可以概括为以下三步:

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

相关推荐
Swift社区3 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
#摩斯先生3 小时前
Swift从0开始学习 对象和类 day3
ios·xcode·swift
没头脑的ht3 小时前
Swift内存访问冲突
开发语言·ios·swift
#摩斯先生3 小时前
Swift从0开始学习 并发性 day4
ios·xcode·swift
没头脑的ht3 小时前
Swift闭包的本质
开发语言·ios·swift
Red Red5 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
Natural_yz6 小时前
大数据学习17之Spark-Core
大数据·学习·spark
qq_172805596 小时前
RUST学习教程-安装教程
开发语言·学习·rust·安装
一只小小汤圆7 小时前
opencascade源码学习之BRepOffsetAPI包 -BRepOffsetAPI_DraftAngle
c++·学习·opencascade
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎