当前位置: 首页 > article >正文

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

Weak修饰符的内部机制

SideTable

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

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的结构:

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 的代码:

#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:
// 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() 的作用是更新指针指向,创建对应的弱引用表。

// 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:把新的对象进行注册操作,完成与对应的弱引用表进行绑定操作。

/*	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表中删除,最后清理对象的记录。


http://www.kler.cn/a/292052.html

相关文章:

  • RabbitMQ教程:路由(Routing)(四)
  • 使用 Grafana api 查询 Datasource 数据
  • 在 Node.js 中解决极验验证码:使用 Puppeteer 自动化
  • 【Pikachu】XML外部实体注入实战
  • 【Oracle篇】掌握SQL Tuning Advisor优化工具:从工具使用到SQL优化的全方位指南(第六篇,总共七篇)
  • 【PYTORCH】使用MTCNN和InceptionResnetV1简单进行人脸检测和相似度匹配
  • flutter和android原生 界面显示的原理是什么,有什么异同。
  • 利用Python脚本批量管理Linux服务器部署服务
  • html+css网页设计 合十文化2个页面
  • c++ 定义函数
  • 为什么要有mybatis?——mybatis
  • Gitlab删除本地标签和分支
  • 使用 RabbitMQ 和 Go 构建异步订单处理系统
  • Apple “Glowtime”活动:iPhone 16、Apple Intelligence亮相
  • SQL进阶技巧:给定数字的频率查询中位数 | 中位值计算问题
  • vscode 20 个实用插件
  • 计算机毕业设计选题推荐-高校实验室教学管理系统-Java/Python项目实战
  • c语言中的动态内存管理
  • 面向可信和节能的雾计算医疗决策支持系统的优化微型机器学习与可解释人工智能
  • uni-app应用更新(Android端)
  • C语言预处理详解
  • 彻底解决 node/npm, Electron下载失败相关问题, 从底层源码详解node electron 加速配置
  • 无需更换摄像头,无需施工改造,降低智能化升级成本的智慧工业开源了。
  • ClickHousez中如何定时清理过期数据库?
  • 生信机器学习入门4 - scikit-learn训练逻辑回归(LR)模型和支持向量机(SVM)模型
  • Qt (13)【Qt窗口 —— 颜色对话框 QColorDialog】