08--内存管理04--Weak 原理

TOC

weak 分析思路:汇编+源码

  • 打开汇编
  • 定位到 objc_initWeak
  • 全局搜索 objc_initWeak,找到实现的地方
objc_initWeak
  • 定位到重点函数 storeWeak
storeWeak
  • 分析到函数重点 storeWeak 的核心实现
重点流程
  • 定位到重点函数 weak_register_no_lock
weak_register_no_lock

weak 修饰原理:流程

变量说明

在流程中会一直遇到以下几个变量

weak_tableweak_table_t 类型,全局弱引用表
entryweak_entry_t 类型,weak 变量的存储实体
referentobjc_object * 类型,对象的引用,这里表示被弱引用的对象指针
referrerobjc_object ** 类型,引用的地址,这里表示弱引用的对象指针的指针

入口函数

weak源码流程图

objc_initWeak
storeWeak

1. class_initialize

条件:如果这个类正在初始化,则应该先初始化class_initialize

2. weak_unregister_no_lock

条件:如果有旧的值,则先清理 weak_unregister_no_lock

  1. 非空判断,if (!referent) return;

  2. weak_table 中取 weak_entry_t——弱引用的集合entry

  3. 如果 entry 存在移除旧的弱引用:remove_referrer(entry, referrer);

  4. 如果 entry 中的 referrer 为空,则从 weak_table 中移除这个 entry

    if (empty)
        weak_entry_remove(weak_table, entry);
    

3. weak_register_no_lock

条件:如果有新的值,则存储 weak_register_no_lock

  1. 如果在 weak_table 中找到了 弱引用对象referent弱引用的集合enter

    if ((entry = weak_entry_for_referent(weak_table, referent)))
    
  • append_referrer

    • 如果下面条件成功,先扩容再插入
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4)
        grow_refs_and_insert(entry, new_referrer);
    
    • 保存新的弱引用指针的地址
    // 保存新的弱引用指针的地址
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    // 集合数量 +1
    entry->num_refs++;
    
  1. 如果不存在这个 弱引用的集合enter

    • 创建一个新的 弱引用的集合enterweak_entry_t new_entry(referent, referrer);
      作用1:创建一个新的 弱引用的集合enter
      作用2:保存这个弱引用指针的地址

    • 如果 weak_table 需要扩容,则去扩容:weak_grow_maybe(weak_table);

    • 在 weak_table 中插入这个新的 弱引用的集合enter
      weak_entry_insert(weak_table, &new_entry);

weak 释放原理:流程

在ARC机制下,当一个对象的引用计数为0的时候,会向他发送一个 release 消息,而在 release 的实现中,会执行 dealloc 方法,在 dealloc 方法中会执行 weak_clear_no_lock(&table.weak_table, (id)this); 方法来清空weak引用

if (*referrer == referent) {
    *referrer = nil;
}

通过 引用的地址 将引用(weak变量)置空

1. dealloc 流程

_objc_rootDealloc(self);

--> obj->rootDealloc();

----> object_dispose((id)this);

------> objc_destructInstance(obj);

--------> obj->clearDeallocating();

----------> clearDeallocating_slow();

------------> if (isa.weakly_referenced) {
------------>     weak_clear_no_lock(&table.weak_table, (id)this);
------------> }

2. weak_clear_no_lock 流程

参数
weak_table:全局弱引用表
referent:将要被释放的弱引用对象

  1. 获取 弱引用的集合entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
  2. 遍历 集合enter 中的弱引用指针,并置空
    *referrer = nil;
  3. 移除 entry集合
    weak_entry_remove(weak_table, entry);

3. weak_entry_remove 流程

  1. 释放 entry集合 的引用
    free(entry->referrers);
  2. weak_table计数减一
    weak_table->num_entries;
  3. 重新设置 weak_table 的大小
    weak_compact_maybe(weak_table);

Weak 修饰原理:内存表

上面介绍了 weak修饰weak释放 的流程,但他们在内存中到底是怎么存在的,并没有分析到,下面将会进行详细说明

1. 什么是 weak变量?

先来看一段代码

LGTeacher* teacher = [LGTeacher alloc];

__weak id wobjc1 = teacher;

__weak id wobjc2 = teacher;

__strong id sobjc1 = wobjc1;

__strong id sobjc2 = wobjc2;

NSLog(@"--%@, --%@, --%@, --%@,", wobjc1, wobjc2, sobjc1, sobjc2);

输出结果

--<LGTeacher: 0x1012525f0>, --<LGTeacher: 0x1012525f0>, --<LGTeacher: 0x1012525f0>, --<LGTeacher: 0x1012525f0>,

用 weak 修饰的变量是 weak变量,代码中的 wobjc1wobjc2 是用 weak修饰 的变量,sobjc1sobjc2 使用 strong修饰 的变量,但是我们从输出结果中可以看到,他们的所有值都是一样的,那 weakstrong 还有啥区别呢?

weak和strong变量的存储方式不一样,存储方式后面再做介绍。

这四个变量的值都是 teacher,teacher 表示对象的首地址,也就是输出结果中的 0x1012525f0 这个地址。如果不看 __weak__strong,上面的四个表达式就是一个单纯的赋值运算符,将右边的值赋给左边。因为有了这两个修饰符,他们有了不一样的含义,在内存中的存储方式发生了改变。

引用、对象、类型

还是上面那段代码。

引用对象类型
  • 对象:alloc分配的一块内存
  • 引用:teacher-强引用, wobjc1wobjc2 -弱引用,sobjc1sobjc2 -强引用
  • 类型:id、LGTeacher,都是类型。在对类的结构的探索中,我们可以直接拿到对象的某个属性的地址,但在开发中,我们并不能直接通过这个地址去修改属性的值,只能通过变量根据类型的setter方法去设置属性的值,这是对内存的一种保护机制。

下面开始来介绍表的原理

3. weak 修饰原理:内存表

例如:程序在执行下面这一行代码的时候,会访问哪些内存相关的操作,都在下面的流程图中。

__weak id weakRef = objc

referent:weakRef,表示这个弱引用变量
referrer:&weakRef,表示这个弱引用变量的地址

weak内存流程图

变量

weak_tableweak_table_t 类型,全局弱引用表
entryweak_entry_t 类型,weak 变量的存储实体
referentobjc_object * 类型,对象的引用,这里表示被弱引用的对象指针
referrerobjc_object ** 类型,引用的地址,这里表示弱引用的对象指针的指针

大致流程

  1. 先从 SideTable 中找到 weakTable;
  2. 然后从 weakTable 中找到 entry,如果没有就新建一个;
  3. 将 weak 引用存入到 entry中;

lldb探索

  1. 第一次进来:weak_table 中是空的
image
  1. 执行 else 流程之后,weak_table 中出现了一个 entry
image
  1. 第二次进来:entry 中有值存在

第一次存成功了,既然是第二次进来,那不把第一次存的东西找到,肯定是浑身难受的

  • 输出 entry 信息:p *entry

    entry
  • 输出 referent,weak_entry_t 的key值,对象的首地址:p undisguise(18446744069395690144)

    referent
  • 输出 referrers 中的 weak_referrer_t: p $16.referrersp $16.inline_referrers[0]。注释中解释了这个值进行了一些操作,比如指针对齐,所以无法找到二级地址以及一级地址,这个问题后面再探索。

    weak_referrer_t
  1. insert 之后,entry 中有两个值。

    image

通过上面这一波操作,weak变量已经从隐身状态显型了,呈现到了我们的眼前,再也不是仅仅存在脑海中的幻象了。

为什么 entry 中存的是 referrer(引用的地址)?

大家应该都听过一句话——weak变量会自动置为nil,原理就在这个 referrer 中。

通过 引用的地址 将引用(weak变量)置空

4. 弱引用表:weak_table_t

1. weak_table_t 结构

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

注释中说的很明白了

  • 这是一个全局的弱引用表
  • key:对象id 作为 key
  • value:weak_entry_t 结构作为 value

2. weak_entry_t 的查找流程

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

对于 hash 表来说,重点就是下标的计算,计算 weak_table 中下标的代码:

size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
    index = (index+1) & weak_table->mask;
    if (index == begin) bad_weak_table(weak_table->weak_entries);
    hash_displacement++;
    if (hash_displacement > weak_table->max_hash_displacement) {
        return nil;
    }
}

计算完下标之后,返回 weak_entry_t 的指针,以便外部可以修改,进行插入或删除的操作

return &weak_table->weak_entries[index];

5. weak实体表:weak_entry_t

1. 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 {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

可以看出来,这里并没有做多少运算的操作,主要是两个属性,

2. 属性 referent

这是保存对象地址的一个属性,作为 hash 表的 key 值。

DisguisedPtr<objc_object> referent;

3. 属性 referrers

这是一个联合体类型,存的就是引用的地址(二级指针)。

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 {
        // out_of_line_ness field is low bits of inline_referrers[1]
        weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
    };
};

4. weak_referrer_t:DisguisedPtr 类

  • weak_referrer_t 类型

    typedef DisguisedPtr<objc_object *> weak_referrer_t;
    
  • DisguisedPtr 类:提供两个静态方法,分别是:

    • 将地址取反转成整型存储,这里可以理解为 encode
    • 对存储的整型取反转成指针返回,这里可以理解为 decode
    template <typename T> // 泛型
    class DisguisedPtr {
        uintptr_t value;
        // encode
        static uintptr_t disguise(T* ptr) {
            return -(uintptr_t)ptr;
        }
        // decode
        static T* undisguise(uintptr_t val) {
            return (T*)-val;
        }
        ……   
    }
    

    其实在上面输出 referent 属性的时候,就用到了这个 decode 操作,

    referent

5. append_referrer 方法

size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
    hash_displacement++;
    index = (index+1) & entry->mask;
    if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
    entry->max_hash_displacement = hash_displacement;
}
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;

这个方法跟上面的 weakTable 找 entry 的方法流程是一致的:

  1. 计算下标 index
  2. index 处插入 weak_referrer_t

weak 原理总结

weak知识点总结

  1. 搞清楚引用、对象、类型这些名词的含义;
  2. 搞清楚 weak 存储的流程;
  3. 搞清楚 weak 释放的流程;
  4. 搞清楚 weak 是怎么存储到内存中的,是存在内存的什么地方;
  5. weak实体存的是 weak变量的地址,为了在释放的时候能够将weak变量置空;
  6. weak实体在存 weak变量的地址的时候,进行了一步取反操作,读的时候再取反读;

weak表存了“对象”,为什么不会retain

从weak的存储流程,以及各个步骤的传参、表的设计,很容易理解成weak_table:

  • key是对象的地址(这是错误的)
  • value是weak_entry

weak_table是一张全局的表,当做Dictionary来理解,对key-value是都会持有,必然会导致引用计数的+1,而weak_entry的释放必须等retainCount=0的时候才会执行,这样势必会形成一个循环等待,以至于内存泄露。很显然是某个地方分析错了。

那就再来一次

weak_entry_for_referent
  1. weak_table中的weak_entries是一个数组
  2. 根据referent的指针计算下标:hash_pointer(referent) & weak_table->mask
  3. 最容易让人调用陷阱的代码:.referent != referent,如果weak_entry不持有referent,那还怎么跟referent进行比较呢?看下面↓↓↓↓↓↓
weak_entry_t
  1. weak_entry_t的构造方法,接受referent(newReferent)对象的地址作为默认参数来设置referent属性;
  2. referent属性的类型是DisguisedPtr<objc_object>。继续看下面↓↓↓↓↓↓
DisguisedPtr

这个类只做了一件事,地址和数值之间的转换。

  1. disguise:将对象的地址转换成一个uintptr_t类型的数值
  2. undisguise:在取值的时候,再将这个数值转换成一个对象的地址

看到这里,可以确定的是,weak_table中的weak_entry并没有直接持有对象,没有拷贝对象的引用,而是保存对象地址转换而来的一个数值,所以不会造成引用计数的变化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。