Objective-C 小记(10)__weak

本文使用的 runtime 版本为 objc4-706

__weak 修饰的指针最重要的特性是其指向的对象销毁后,会自动置为 nil,这个特性的实现完全是依靠运行时的。实现思路是非常简单的,对于下面的语句来说:

id __weak weakObj = strongObj;

便是用 strongObj 当作 key,weakObj 当作 value 存入一个表里。当 strongObj 销毁时,从表里找到所有的 __weak 引用,将其置为 nil

当然,实际的实现肯定是要比这要充斥着更多的细节。

变量的创建和销毁

还是上面那个例子,实际上编译器会进行一些变动:

{
    id __weak weakObj = strongObj;
}
// 会变成
{
    id __weak weakObj; objc_initWeak(&weakObj, strongObj); // 离开变量的范围,进行销毁 objc_destroyWeak(&weakObj); } 

objc_initWeakobjc_destroyWeak 都可以在 NSObject.mm 文件中找到:

id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) { *location = nil; return nil; } return storeWeak<false/*old*/, true/*new*/, true/*crash*/> (location, (objc_object*)newObj); } void objc_destroyWeak(id *location) { (void)storeWeak<true/*old*/, false/*new*/, false/*crash*/> (location, nil); } 

可以看到都是对 storeWeak 函数模板的调用(为什么要使用模板呢?会更快吗?C++ 小白内心的问题…… )。

赋值

当已有的 __weak 变量被重新赋值时会怎么样呢?

weakObj = anotherStrongObj;

// 会变成下面这样
objc_storeWeak(&weakObj, anotherStrongObj);

它的实现如下:

id
objc_storeWeak(id *location, id newObj)
{
    return storeWeak<true/*old*/, true/*new*/, true/*crash*/> (location, (objc_object *)newObj); } 

但实际上也还是对 storeWeak 函数模板的封装。

storeWeak

storeWeak 的实现还是有点长的,一点一点来分析:

// Update a weak variable.
// If HaveOld is true, the variable has an existing value 
//   that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be // assigned into the variable. This value might be nil. // If CrashIfDeallocating is true, the process is halted if newObj is // deallocating or newObj's class does not support weak references. // If CrashIfDeallocating is false, nil is stored instead. template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating> static id storeWeak(id *location, objc_object *newObj) { assert(HaveOld || HaveNew); if (!HaveNew) assert(newObj == nil); Class previouslyInitializedClass = nil; id oldObj; SideTable *oldTable; SideTable *newTable; 

函数前的注释表明了三个模板参数的作用,当然在后面的代码里也能直观的看到。函数一开始进行了变量的声明,可以注意到 SideTable 这个类型,SideTable 是现在的运行时中用来存放引用计数和弱引用的结构体,它的结构是这样的(省略了结构体函数):

struct SideTable {
    spinlock_t slock; RefcountMap refcnts; weak_table_t weak_table; } 

其中 slock 是一个自旋锁,用来对 SideTable 实例进行操作时的加锁。refcnts 则是存放引用计数的地方。weak_table 则是存放弱引用的地方(后面将详细分析 weak_table_t)。

回到 storeWeak 函数:

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 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; } 

这一段即获取 oldObjoldTablenewTable,并将获取的两个表上锁。注意到获取 oldTablenewTable 时,其实是用对象的地址当作 key 从 SideTables 获取的,SideTables 返回的就是一个哈希表,存储着若干个 SideTable,一般是 64 个。

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized 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)); // If this class is finished with +initialize then we're good. // If this class is still running +initialize on this thread // (i.e. +initialize called storeWeak on an instance of itself) // then we may proceed but it will appear initializing and // not yet initialized to the check above. // Instead set previouslyInitializedClass to recognize it on retry. previouslyInitializedClass = cls; goto retry; } } 

上面这一段代码也有着很好的注释,就是要确保对象的类已经走过 +initialize 流程了。

    // Clean up old value, if any.
    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (HaveNew) { newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, CrashIfDeallocating); // weak_register_no_lock returns nil if weak store should be rejected // Set is-weakly-referenced bit in refcount table. if (newObj && !newObj->isTaggedPointer()) { newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. *location = (id)newObj; } else { // No new value. The storage is not changed. } SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable); return (id)newObj; } 

最后一段的逻辑也是很清晰的。首先,如果有旧的值(HaveOld),则使用 weak_unregister_no_lock 函数将其从 oldTableweak_table 中移除。其次,如果有新的值(HaveNew),则使用 weak_register_no_lock 函数将其注册到 newTableweak_table 中,并使用 setWeaklyReferenced_nolock 函数将对象标记为被弱引用过。

storeWeak 的实现就告一段落了,其重点就在 weak_register_no_lockweak_unregister_no_lock 函数上。

weak_table_t

在分析这两个函数之前,先看看 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; }; 
  • weak_entries 便是存放弱引用的数组;
  • num_entries 是存放的 weak_entry_t 条目的数量;
  • mask 则是动态申请的弱引用数组 weak_entries 长度减 1 的值,用来对哈希后的值取余和记录数组大小;
  • max_hash_displacement 则是哈希碰撞后最大的位移值。

其实 weak_table_t 就是一个动态增长的哈希表。

继续看看其相关的操作,首先是对整个表的扩大:

#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)

// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table) { size_t old_size = TABLE_SIZE(weak_table); // Grow if at least 3/4 full. if (weak_table->num_entries >= old_size * 3 / 4) { weak_resize(weak_table, old_size ? old_size*2 : 64); } } 

可以看到,当 weak_table 里的弱引用条目达到它容量的四分之三时,便会将容量拓展为两倍。值得注意的是第一次拓展也就是是 mask 为 0 的情况,初始值是 64。实际对弱引用表大小的操作则交给了 weak_resize 函数。

除了扩大,当然也还有缩小:

// Shrink the table if it is mostly empty.
static void weak_compact_maybe(weak_table_t *weak_table) { size_t old_size = TABLE_SIZE(weak_table); // Shrink if larger than 1024 buckets and at most 1/16 full. if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) { weak_resize(weak_table, old_size / 8); // leaves new table no more than 1/2 full } } 

缩小的话则是需要表本身大于等于 1024 并且存放了不足十六分之一的条目时,直接缩小 8 倍。实际工作也是交给了 weak_resize 函数:

static void weak_resize(weak_table_t *weak_table, size_t new_size) { size_t old_size = TABLE_SIZE(weak_table); weak_entry_t *old_entries = weak_table->weak_entries; weak_entry_t *new_entries = (weak_entry_t *) calloc(new_size, sizeof(weak_entry_t)); weak_table->mask = new_size - 1; weak_table->weak_entries = new_entries; weak_table->max_hash_displacement = 0; weak_table->num_entries = 0; // restored by weak_entry_insert below if (old_entries) { weak_entry_t *entry; weak_entry_t *end = old_entries + old_size; for (entry = old_entries; entry < end; entry++) { if (entry->referent) { weak_entry_insert(weak_table, entry); } } free(old_entries); } } 

weak_resize 函数的过程就是新建一个数组,将老数组里的值使用 weak_entry_insert 函数添加进去,注意到代码中间 mask 在这里被赋值为新数组的大小减去 1,max_hash_displacementnum_entries 也都清零了,因为 weak_entry_insert 函数会对这两个值进行操作。接着对 weak_entry_insert 函数进行分析:

/** 
 * Add new_entry to the object's table of weak references.
 * Does not check whether the referent is already in the table.
 */
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry) { weak_entry_t *weak_entries = weak_table->weak_entries; assert(weak_entries != nil); size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask); size_t index = begin; size_t hash_displacement = 0; while (weak_entries[index].referent != nil) { index = (index+1) & weak_table->mask; if (index == begin) bad_weak_table(weak_entries); hash_displacement++; } weak_entries[index] = *new_entry; weak_table->num_entries++; if (hash_displacement > weak_table->max_hash_displacement) { weak_table->max_hash_displacement = hash_displacement; } } 

这个函数就是个很正常的哈希表插入的过程,hash_pointer 函数是对指针地址进行哈希,哈希后的值之所以要和 mask 进行 & 操作,是因为弱引用表的大小永远是 2 的幂(一开始是 64,之后不断乘以 2),mask 则是大小减去 1 即为一个 0b111...11 这么一个数,和它进行 & 运算相当于取余。hash_displacement 则是记录了哈希相撞后偏移的大小。

既然有插入,也就有删除:

/**
 * Remove entry from the zone's table of weak references.
 */
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry) { // remove entry if (entry->out_of_line()) free(entry->referrers); bzero(entry, sizeof(*entry)); weak_table->num_entries--; weak_compact_maybe(weak_table); } 

很直接的清零 entry,并给 weak_tablenum_entries 减 1,最后检查看是否需要缩小。

最后还有一个根据指定对象查找存在条目的函数:

/** 
 * Return the weak reference table entry for the given referent. 
 * If there is no entry for referent, return NULL. 
 * Performs a lookup.
 *
 * @param weak_table 
 * @param referent The object. Must not be nil.
 * 
 * @return The table of weak referrers to this object. 
 */
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent) { assert(referent); weak_entry_t *weak_entries = weak_table->weak_entries; if (!weak_entries) return nil; 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]; } 

也是很正常的哈希表套路。

weak_entry_t

那弱引用是怎么存储的呢,继续分析 weak_entry_t

#define WEAK_INLINE_COUNT 4

#define REFERRERS_OUT_OF_LINE 2 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; } } }; 

首先 DisguisedPtr<T> 类型和 T* 的行为是一模一样的,这个类型存在的目的是为了躲过内存泄漏工具的检查(注释原文:「DisguisedPtr<T> acts like pointer type T*, except the stored value is disguised to hide it from tools like leaks.」)。所以 DisguisedPtr<objc_object> referent 可以看作是 objc_object *referent

referent 这个指针记录的便是被弱引用的对象。接下来的联合里有两种结构体,先分析第一种:

  • referrersreferrers 是一个 weak_referrer_t 类型的数组,用来存放弱引用变量的地址,weak_referrer_t 的定义是这样的:typedef DisguisedPtr<objc_object *> weak_referrer_t;
  • out_of_line_ness:2 bit 标记位,用来确定联合里的内存是第一个结构体还是第二个结构体;
  • num_refsPTR_MINUS_2 便是字长减去 2 位,和 out_of_line_ness 一起组成一个字长,用来存储 referrers 的大小;
  • maskmax_hash_displacement:和前面分析的一样,做哈希表用到的东西。

可以发现第一种结构体也是一个哈希表,第二种结构体则是一个和第一种结构体一样大的数组,所谓的 inline 存储。存放思路则是首先 inline 存储,当超过 WEAK_INLINE_COUNT 也就是 4 时,再变成第一种的动态哈希表存储。代码下方的构造函数便体现了这个思路。

可以注意到 weak_entry_t 重载了赋值操作符,将赋值变成了一个拷贝内存的操作。

相关操作也是和上面 weak_table_t 的类似,只不过加上了 inline 存储情况的变化,就不详细分析了。

weak_register_no_lock

开始分析 weak_register_no_lock 函数:

/** 
 * Registers a new (object, weak pointer) pair. Creates a new weak
 * object entry if it does not exist.
 * 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
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; if (!referent || referent->isTaggedPointer()) return referent_id; 

第一段,约等于什么都没干。referent 是被弱引用的对象,referrer 则是弱引用变量的地址。

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else { 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); } 

这一段很有意思,如果对象没有自定义的内存管理方法(hasCustomRR),则将 deallocating 变量赋值为 rootIsDeallocating 也就是是否正在销毁。但是如果有自定义的内存管理方法的话,发送的是
allowsWeakReference 这个消息,即是否允许弱引用。不管怎么样,我们得到了一个 deallocating 变量。

    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; } } 

从上面一段可以知道,deallocatingtrue 的话肯定是有问题的,所以这一段处理一下。

    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    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; } 

最后一段终于做了正事了!首先先用 weak_entry_for_referent 函数搜索对象是否已经有了 weak_entry_t 类型的条目,有的话则使用 append_referrer 添加一个变量位置进去,没有的话则新建一个 weak_entry_t 条目,使用 weak_grow_maybe 函数扩大(如果需要的话)弱引用表的大小,并使用 weak_entry_insert 将弱引用插入表中。

weak_unregister_no_lock

接下来是 weak_unregister_no_lock 函数:

void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id) { objc_object *referent = (objc_object *)referent_id; objc_object **referrer = (objc_object **)referrer_id; weak_entry_t *entry; if (!referent) return; if ((entry = weak_entry_for_referent(weak_table, referent))) { remove_referrer(entry, referrer); bool empty = true; if (entry->out_of_line() && entry->num_refs != 0) { empty = false; } else { for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i]) { empty = false; break; } } } if (empty) { weak_entry_remove(weak_table, entry); } } // Do not set *referrer = nil. objc_storeWeak() requires that the // value not change. } 

主要功能实现思路很简单,使用 weak_entry_for_referent 函数找到对应的弱引用条目,并用 remove_referrer 将对应的弱引用变量位置从中移除。最后判断条目是否为空,为空则使用 weak_entry_remove 将其从弱引用表中移除。

自动置为 nil

对象销毁后,弱引用变量被置为 nil 是因为在对象 dealloc 的过程中调用了 weak_clear_no_lock 函数:

/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { objc_object *referent = (objc_object *)referent_id; weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { /// XXX shouldn't happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } 

首先初始化一下,获取到弱引用条目,顺便处理没有弱引用的情况。

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) { referrers = entry->referrers; count = TABLE_SIZE(entry); } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } 

获取弱引用变量位置数组和个数。

    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } weak_entry_remove(weak_table, entry); } 

循环将它们置为 nil,最后移除整个弱引用条目。

访问弱引用

在访问一个弱引用时,ARC 会对其进行一些操作:

obj = weakObj;

// 会变成
objc_loadWeakRetained(&weakObj);
obj = weakObj;
objc_release(weakObj);

objc_loadWeakRetained 函数的主要作用就是调用了 rootTryRetain 函数:

ALWAYS_INLINE bool 
objc_object::rootTryRetain()
{
    return rootRetain(true, false) ? true : false; } 

实际上就是尝试对引用计数加 1,让弱引用对象在使用时不会被释放掉。

有关 rootRetain 的实现:《Objective-C 小记(7)retain & release》

总结

存放一个弱引用还真是哈希了很多次:

  1. SideTable 哈希一次,这里分开来应该是为了性能原因;
  2. weak_table_t 哈希一次;
  3. weak_entry_t 哈希一次。

对于开销,直观感受上也并没有什么很大开销,想用就用呗……



作者:KylinRoc
链接:https://www.jianshu.com/p/eff6b9443800
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载于:https://www.cnblogs.com/feng9exe/p/8401354.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值