iOS weak实现原理和销毁过程

前言

weak弱引用的相关内容在开发中常遇到,那么这篇文章我们主要探索weak的底层操作是什么样子的,开始吧!

准备工作

1. weak基本用法

weak是弱引用,用weak来修饰、描述所引用对象的计数器并不会增加,而且weak会在引用对象被释放的时候自动置为nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外weak也可以解决循环引用

面试拓展:为什么修饰代理使用weak而不是用assign
assign可用来修饰基本数据类型,也可修饰OC的对象,但如果用 assign修饰对象类型指向的是一个强指针,当指向的这个指针释放之后,它仍指向这块内存,必须要手动给置为nil,否则会产生野指针,如果还通过此指针操作那块内存,会导致EXC_BAD_ACCESS错误,调用了已经被释放的内存空间;而weak只能用来修饰OC对象,而且相比assign比较安全,如果指向的对象消失了,那么它会自动置为nil不会导致野指针

2. weak的实现原理

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash哈希)表,Key是所指对象的地址,Valueweak指针的地址 (这个地址的值是所指对象的地址)数组

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

  • 初始化时runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址
  • 添加引用时objc_initWeak函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表
  • 释放时:调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entryweak表中删除,最后清理对象的记录。

3. weak底层原理探索

以下案例就是使用一个临时变量obc1来存储创建出来的变量,代码如下:

    NSObject * objc1 = [[NSObject alloc] init];
    id  __weak objc2 = objc1;

注意:
一般情况下,我们不会直接用__weak来修饰一个刚创建出来的临时变量,因为__weak修饰这个变量,运行时一创建出来就会释放,而如果使用一个临时变量objc1话,创建出来后会放到自动释放池中,延缓这个变量的生命周期,变量的生命周期会跟着自动释放池自动保持,所以这样能够保证不会一创建出来就会释放

然后我们运行代码,打开汇编断点查看底层调用了什么方法,如下:

打开汇编断点

进入断点查看,如下:
进入汇编断点

通过汇编断点可以看出底层是调用了objc_initWeak方法,这是入口。

4. 初始化源码分析

进入objc_initWeak的内部添加断点,进行lldb调试。

  • objc_initWeak分析
    libobjc.dylib源码中成功定位到了初始化过程,同时objc_initWeak传入了两个参数,location即弱引用的地址存储在栈中),newObjc也就是创建的对象(存储在堆中)。见下图:
    objc_initWeak内部断点

    根据以上的源码,可以分析得出:
  • 首先会判断对象是否为空,如果为空直接返回nil
  • 如果不为空,则会调用stroeWeak方法进行存储
  • location弱引用的地址
  • newObjc是一个对象,在底层对象的实现就是objc_object

注意:object必须是一个没有被注册为__weak对象的有效指针。

4.1数据结构分析

在分析方法之前进行数据结构的分析是非常必要的!系统维护了一个SideTables,那么SideTable散列表为什么有多张?一张表不安全太多了性能不好。真机下8张表,其他环境下64张,散列表也是一张hash表。而SideTables是一个hash表,综合了链表和数组的特点。拉链法同一个hash可以放在同一个表中

1. 散列表SideTable

struct SideTable {
    //自旋锁
    spinlock_t slock;
    //引用计数
    RefcountMap refcnts;
     //对象的弱引用表
    weak_table_t weak_table;
    .....
    //后面还有一些对锁的操作
}

SideTable是个结构体,其属性包括:自旋锁引用计数表弱引用依赖表
2. 弱引用表weak_table

struct weak_table_t {
    //保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    //存储空间
    size_t    num_entries;
    //哈希算法辅助值(掩码)
    uintptr_t mask;
    //最大偏移量
    uintptr_t max_hash_displacement;
};

weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了某个对象相关的所有的弱引用信息,这是一个hash表。使用不定类型对象的地址作为key,用weak_entry_t类型结构体对象作为value。其中的weak_entries成员,即为弱引用表入口

3. weak_entry_t

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            //最开始会存放到inline_referrers, 放满之后存放到上面的 referrers
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
    ......
    //还有一些结构体提供的方法
}

弱引用实体,有两个属性,一个对象,另一个属性是一个联合体,其中包含弱引用的数组

数据关系图:

关系图

4.2 objc_storeWeak分析

结束完所需类型的数据结构分析后,我们就开始进入核心方法的流程分析了,即分析objc_storeWeak方法,该方法主要分为三个功能,如下:

    1. 判断应用是否存在值


      功能1
    1. 如果有旧值,将旧值清除


      功能2
  • 有新增,重新初始化
    功能3

    根据上面的源码可以知道,objc_storeWeak主要做了以下的三点:
  • 对当前引用的数据处理判断,判断该引用是否存在旧值以及是否指向了新值
  • 如果引用当前有指向的值,即存在旧值,则需要将旧值清除
  • 同时如果引用指向了新的对象,即存在新值,则需要进行对应对象弱引用的初始化工作。

我们从弱引用指向一个新值开始探索,整理其核心代码如下:

 if (haveNew) {
       //根据对象回去对应的散列表
        newTable = &SideTables()[newObj];
    }
 if (haveNew) {
       // 从散列表中获取弱引用表,并传入
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (!newObj->isTaggedPointerOrNil()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }

进入weak_register_no_lock流程。代码如下:

//对象对应的全局弱引用表,对象指针,弱引用指针
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    //对象
    objc_object *referent = (objc_object *)referent_id;
    //弱引用指针
    objc_object **referrer = (objc_object **)referrer_id;

  ......

    // now remember it and where it is being stored
    weak_entry_t *entry;
    //根据弱引用对象从weak_table中找出weak_entry_t
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //将弱引用指针加入entry
        append_referrer(entry, referrer);
    } 
    else {
        //通过弱引用指针与对象创建new_entry
        weak_entry_t new_entry(referent, referrer);
        //weak_table扩容
        weak_grow_maybe(weak_table);
        //将new_entry插入weak_table
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.
    //返回对象
    return referent_id;
}
  • weak_table中找到对象对应的weak_entry_t
  • 找到对应的weak_entry_t调用append_referrer将弱引用指针加入weak_entry_t(因为释放过程中有置空操作,所以找空位nil加入,这个过程中可能会进行扩容)。
  • 找不到则根据对象和弱引用指针创建weak_entry_t
    • 调用weak_grow_maybe尝试扩容。
    • 调用weak_entry_insert将创建的weak_entry_t加入weak_table

方法所传入的前三个参数分别为:弱引用表对象弱引用。该方法会判断对象是否析构,如果有就不会处理,直接返回。如果没有,则会通过weak_entry_for_referent方法获取对象对应的weak_entry_t见下图:

weak_entry_for_referent

通过hash函数获取其表中的下标,通过循环弱引用表,找对应的下标,获取对应的weak_entry_t。获取weak_entry_t后,去存储新的弱引用对象,回到方法weak_register_no_lock,在调用weak_entry_for_referent之后,会通过append_referrer(entry, referrer);方法进行弱引用的存储。见下图:
append_referrer

总结:
提供维护了一个SideTables,其中多张散列表SideTable,每一张SideTable表中有自旋锁引用计数表弱引用表weak_table_t。弱引用表中存储一些实体weak_entry_t,实体中包括了对象弱引用数组

如果弱引用存在旧值,具体操作是什么样子的呢?以下是核心代码:

    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

进入weak_unregister_no_lock方法,如下:

weak_unregister_no_lock

在此过程中,会获取弱应用对象对应的实体weak_entry,并调用remove_referrer方法,从实体中移除对应的弱引用。见下图:
remove_referrer

此过程会从对象的引用列表移除该的引用,并将弱引用个数减1

5. 弱引用的清除流程

当一个对象调用dealloc方法时,其弱引用处理流程见下面代码:

        - (void)dealloc {
            _objc_rootDealloc(self);
        }

        void _objc_rootDealloc(id obj)
        {
            ASSERT(obj);
            obj->rootDealloc();
        }

        inline void objc_object::rootDealloc()
        {
            if (isTaggedPointer()) return;  // fixme necessary?
            if (fastpath(isa.nonpointer                     &&
                         !isa.weakly_referenced             &&
                         !isa.has_assoc                     &&
        #if ISA_HAS_CXX_DTOR_BIT
                         !isa.has_cxx_dtor                  &&
        #else
                         !isa.getClass(false)->hasCxxDtor() &&
        #endif
                         !isa.has_sidetable_rc))
            {
                assert(!sidetable_present());
                free(this);
            } 
            else {
                object_dispose((id)this);
            }
        }

        id object_dispose(id obj)
        {
            if (!obj) return nil;
            objc_destructInstance(obj);    
            free(obj);
            return nil;
        }

        void *objc_destructInstance(id obj) 
        {
            if (obj) {
                // Read all of the flags at once for performance.
                bool cxx = obj->hasCxxDtor();
                bool assoc = obj->hasAssociatedObjects();

                // This order is important.
                if (cxx) object_cxxDestruct(obj);
                if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
                obj->clearDeallocating();
            }
            return obj;
        }

        inline void objc_object::clearDeallocating()
        {
            if (slowpath(!isa.nonpointer)) {
                // Slow path for raw pointer isa.
                sidetable_clearDeallocating();
            }
            else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
                // Slow path for non-pointer isa with weak refs and/or side table data.
                clearDeallocating_slow();
            }
            assert(!sidetable_present());
        }

最终会调用clearDeallocating方法对弱引用进行处理。针对有弱引用的对象,会调用clearDeallocating_slow();方法,最终的弱引用清除的处理流程在weak_clear_no_lock中。见下图:

weak_clear_no_lock

在此过程中,首先在弱引用表中获取对象对应的实体开启循环将数组中的弱引用全部设为nil,最后将实体从弱引用表中移除

补充:弱引用存储的数据结构图

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

推荐阅读更多精彩内容