iOS - weak 底层原理

[toc]

参考

weak

https://blog.csdn.net/future_one/article/details/81606895

https://www.cnblogs.com/guohai-stronger/p/10161870.html

https://www.jianshu.com/p/2b12666b351f

问答

weak作用?

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

weak也可以解决循环引用。

为什么修饰代理使用 weak 而不是用assign?

assign 可用来修饰基本数据类型, 也可修饰OC的对象, 但如果用 assign 修饰对象类型指向的是一个强指针, 当指向的这个指针释放之后, 它仍指向这块内存, 必须要手动给置为nil, 否则会产生野指针, 如果还通过此指针操作那块内存, 会导致 EXC_BAD_ACCESS 错误, 调用了已经被释放的内存空间;

而 weak 只能用来修饰OC对象, 而且相比assign比较安全, 如果指向的对象消失了, 那么它会自动置为nil, 不会导致野指针。

weak 本质?
ARC帮我们做了什么?

ARC 是 LLVM + Runtime 互相协作的结果

开启ARC后, LLVM编译器会自动帮我们在相应位置生成 release、retain、autorelease

在作用域 (大括号) 结束的位置添加 release

弱引用这种, 是通过runtime处理的

编译解析

OC代码
int main(){
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}
C++代码
int main(){
    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    id __attribute__((objc_ownership(weak))) obj1 = obj;
}

编译之后的weak, 通过 objc_ownership(weak) 实现 weak 方法, objc_ownership 字面意思是:获得对象的所有权, 是对对象weak的初始化的一个操作。

通过 weak 编译解析, 可以看出 weak 是通过 runtime 初始化并维护的;

weak 和 strong 都是 OC ARC 的修饰词, 而 strong 是通过 runtime 维护的一个自动计数表结构。

结构

weak_table_t
  • weak_table_t 存储在 struct SideTable 中。(参考《isa指针》)
  • weak_table_t 是 Runtime 维护的一个 weak 引用的全局hash表, 用于存储指向某个对象的所有weak指针。★
    • key: 所指向对象的地址
    • value: weak_entry_t 类型结构体数组(weak指针的数组) 。 value 是数组的原因是: 因为一个对象可能被多个弱引用指针指向 ★

weak 指针的地址值是所指对象指针的地址

// objc-weak.h
/**
 * 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; // 保存了所有指向指定对象的weak指针   weak_entries的对象
    size_t    num_entries; // 弱引用数量
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
weak_entry_t

weak 全局表 weak_table_t 中的存储 weak 定义的对象的表结构 weak_entry_t,

weak_entry_t 是存储在弱引用表中的一个内部结构体, 它负责维护和存储指向一个对象的所有弱引用hash表

// 单个弱引用, 存放在哈希表中
struct weak_entry_t {
    // 对泛型对象的指针做了一个封装, 通过这个泛型类来解决内存泄漏的问题。
    // 相当于 objc_object *referent;
    DisguisedPtr referent;
    union {
        struct {
            // weak_referrer_t 是 objc_object ** 的别名, 通过一个二维指针地址偏移, 用下标作为 hash 的 key, 做成了一个弱引用散列。
            weak_referrer_t *referrers; // weak变量地址
            uintptr_t        out_of_line_ness : 2;
            // 引用数值。这里记录弱引用表中引用有效数字, 因为弱引用表使用的是静态 hash 结构, 所以需要使用变量来记录数目。
            uintptr_t        num_refs : PTR_MINUS_2; 
            uintptr_t        mask;
            // hash 元素上限阀值。
            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];
        };
    };

    // 最低有效位, 也是标志位。当标志位 0 时, 增加引用表指针纬度。
    // 当其为0的时候,  weak_referrer_t 成员将扩展为多行静态 hash table。
    // 通常情况下是等于零的, 所以弱引用表总是一个 objc_objective 指针二维数组。
    // 一维 objc_objective 指针可构成一张弱引用散列表, 通过第三纬度实现了多张散列表, 并且表数量为 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;
    }
    // objc_object: weak_entry_t对象中的范型对象, 用于标示weak引用的对象。
    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;
        }
    }
};

#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2


weak_referrer_t
// The address of a __weak variable.
// These pointers are stored disguised(伪装的) so memory analysis tools
// don't see lots of interior(内部的) pointers from the weak table into objects.
typedef DisguisedPtr weak_referrer_t;

/* 苹果对 DisguisedPtr 的注释
DisguisedPtr acts like pointer type T*, except the stored value is disguised to hide it from tools like `leaks`.
nil is disguised as itself so zero-filled memory works as expected, 
which means 0x80..00 is also disguised as itself but we don't care.
Note that weak_entry_t knows about this encoding.
*/

可以看到 DisguisedPtr 相当于 objc_object **;

weak_referrer_t 就是 objc_object ** 的别名

存储过程

初始化weak变量时, runtime会调用 objc_initWeak 函数, 初始化新的weak指针指向对象的地址;

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

objc_initWeak
id objc_initWeak(id *location, id newObj) {
    // 首先判断指针指向的类对象是否有效, 若无效直接释放指针并返回;
    if (!newObj) {
        *location = nil;
        return nil;
    }
    // 注册为一个指向value的_weak对象
    // 更新指针的指向, 创建弱引用表。
    return storeWeak
        (location, (objc_object*)newObj);
}
storeWeak()
// 更新指针指向, 创建对应弱引用表
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;

    // 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(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo(oldTable, newTable);
        goto retry;
    }

    // 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(oldTable, newTable);
            class_initialize(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;
        }
    }
    // 解除旧对象与弱引用表关联绑定
    // 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(oldTable, newTable);

    return (id)newObj;
}
weak_unregister_no_lock()
/** 
 * Unregister an already-registered weak reference.
 * This is used when referrer's storage is about to go away, but referent
 * isn't dead yet. (Otherwise, zeroing referrer later would be a
 * bad memory access.)
 * Does nothing if referent/referrer is not a currently active weak reference.
 * Does not zero referrer.
 * 
 * FIXME currently requires old referent value to be passed in (lame)
 * FIXME unregistration should be automatic if referrer is collected
 * 
 * @param weak_table The global weak table.
 * @param referent The object.
 * @param referrer The weak reference.
 */
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_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;

    // 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, 
                                           @selector(allowsWeakReference));
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, @selector(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_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;
}

在最后会调用 clearDeallocating 函数。而 clearDeallocating 函数首先根据对象的地址获取weak指针地址的数组, 然后紧接着遍历这个数组, 将其中的数组开始置为nil, 把这个 entry从weak 表中删除, 最后一步清理对象的记录。

释放过程

weak 指向的对象被释放时, 编译器如何让 weak 指针置为nil的呢?

将弱引用存到一个弱引用表(哈希表)中, 对象销毁时, 就从表中取出当前对象的弱引用并清除

  1. 调用 objc_release

  2. 因为对象的引用计数为0, 所以执行 dealloc

  3. 在 dealloc 中, 调用了 _objc_rootDealloc 函数

  4. _objc_rootDealloc 中, 调用了object_dispose 函数

  5. 调用 objc_destructInstance

  6. 最后调用objc_clear_deallocating,详细过程如下:

    a. 从weak表中获取废弃对象的地址为键值的记录

    b. 将包含在记录中的所有附有 weak修饰符变量的地址, 赋值为 nil

    c. 将weak表中该记录删除

    d. 从引用计数表中删除废弃对象的地址为键值的记录

释放过程 - 实验代码

@implementation Person
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __strong Person *person1;
    __weak Person *person2;
    __unsafe_unretained Person *person3;
    
    NSLog(@"111");
    
    {
        Person *person = [[Person alloc] init];
        person2 = person;
        person3 = person;
    }
    
    NSLog(@"222 - %@", person2); // 打印 null , 说明弱指针 person2 在其指向的对象销毁时, 会自动置空
    NSLog(@"333 - %@", person3); // 会报坏EXC_BAD_ACCESS内存访问, 在这里person3依然有值, 只不过是个野指针, 指向的person对象已被销毁
}
@end

释放过程 - 源码

注: 关于 SideTableweak_table_tweak_entry_t 参考 《isa指针》

dealloc
// Replaced by CF (throws an NSException)
+ (void)dealloc {
}

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}
rootDealloc()
void _objc_rootDealloc(id obj) {
    ASSERT(obj);
    obj->rootDealloc();
}

inline void objc_object::rootDealloc() {
    if (isTaggedPointer()) return;  // fixme necessary?
    
    // 如果该对象是优化的isa(arm64)且 没有 弱引用 / 关联对象 / C++析构器 / 引用计数表, 直接释放
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc)) {
        assert(!sidetable_present());
        free(this);
    } 
    // 普通isa不管有没有用过上述资源, 都走这里
    // 需要释放该对象相关的其他资源
    else {
        object_dispose((id)this);
    }
}
object_dispose()
// 有弱引用必然会走这里
id object_dispose(id obj) {
    if (!obj) return nil;
    // 释放 obj 相关资源
    objc_destructInstance(obj); 
    // 释放自身
    free(obj);

    return nil;
}
objc_destructInstance()
// 释放 obj 相关资源
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); // 清除c++析构函数相关成员变量
        if (assoc) _object_remove_assocations(obj); // 移除关联对象
        
        // 根据当前对象的地址值, 找到对应的
        obj->clearDeallocating(); // 将指向当前对象的 weak 弱指针置为nil ★
    }
    return obj;
}
clearDeallocating()
// 
inline void objc_object::clearDeallocating() {
    // 普通isa指针, 直接指向 class / metaclass 
    if (slowpath(!isa.nonpointer)) { 
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    // arm64优化过的isa
    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_slow()
// arm64优化过的isa 走这里
NEVER_INLINE void objc_object::clearDeallocating_slow() {
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    // 
    SideTable& table = SideTables()[this];
    table.lock();
    // 有弱引用 ★
    if (isa.weakly_referenced) {
        // SideTable 有个弱引用表 weak_table, 是个哈希表 
        // (SideTable 数据结构可到《isa》中查看)
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    // 引用计数长度超出19bit, 存放在 SideTable
    if (isa.has_sidetable_rc) {
        // 清除引用计数
        table.refcnts.erase(this);
    }
    table.unlock();
}
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);
}
weak_entry_for_referent()
/** 根据 要销毁的对象地址值 获取 弱引用入口
 * 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;
    
    // 用 以要销毁的对象地址值 referent, 按位与 weak_table的掩码, 得出哈希表索引
    // 可以看出, weak_table 内部有一个哈希表 weak_entries
    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;
        }
    }
    // 从哈希表中取出index对应的弱引用
    return &weak_table->weak_entries[index];
}
weak_entry_remove()
/**
 * 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
    // 释放weak变量地址, entry->referrers ★★
    if (entry->out_of_line()) free(entry->referrers);
    // 释放entry
    bzero(entry, sizeof(*entry));
    // 弱引用数量-1
    weak_table->num_entries--;

    weak_compact_maybe(weak_table);
}

// 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
    }
}
sidetable_clearDeallocating
// arm64之前的 普通isa 使用该方法释放
void objc_object::sidetable_clearDeallocating() {
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

你可能感兴趣的:(iOS - weak 底层原理)