TLDR:

  1. 拜访 weak 变量与读取 weak 变量的内存是两回事。差异见下图。
    whiteboard_exported_image.png
  2. >= iOS 16 苹果供给了指定类在特定线程开释的办法,可以做一个参阅。

问题提出

实在案例,都脱胎于事务代码,有前史沉淀的事务代码常常令人内牛满面。之前网上会有十分多的评论,主张咱们不要在 dealloc 里做太多逻辑,也有不少相关的收拾,例如:防止在 dealloc 中运用特点拜访,防止在 dealloc 中将 self 赋值给 __weak 变量(crash) 等相关问题。但这次还会讲一个 dealloc 中与 weak 变量相关的冷门知识,虽然不会直接溃散,但或许导致后续逻辑处理出现问题,遇到了仍是很痛的。

剖析问题

环境:Version 14.2 (14C18),objc4-866.9 ,iOS 16.2。

本文 demo 链接见:github.com/ChengzhiHua…

笼统后的问题十分简略,外部正常 [[SampleObject alloc] init] 之后,Assert 是否可以命中?

static __weak id sWeakObject = nil;
@interface SampleObject : NSObject
@end
@implementation SampleObject
- (instancetype)init {
    if (self = [super init]) {
        sWeakObject = self;
        [self testEqual];
    }
    return self;
}
- (void)dealloc {
    [self testEqual];
}
- (void)testEqual {
    BOOL testEqual = sWeakObject == self;
    NSAssert(testEqual, @"What happens?");
}
@end

顺便再放一个前置输出(其实是烟雾弹

image

- (void)testEqual {
    BOOL testEqual = sWeakObject == self;
    NSAssert(testEqual, @"WHY NOT EQUAL???");
}

假如知道会中 Assert 的话,或许是刷过一些八股。你可以进一步追问:

  1. 在 lldb 里直接 po weak 变量 跟实践代码的拜访 weak 变量 有什么不同?

  2. 拜访一个 weak 变量实践经过了哪些进程?

  3. weak 变量到底在什么时分会被置空?

  4. 咱们知道 deallocobjc_destructInstance 之前, 在 objc_destructInstance 中,咱们知道是先开释 strong 变量,再开释 关联目标,最终将一切运用 __weak 润饰的指向该目标的变量置为 nil 。但为什么在 dealloc 里拜访时,weak 变量已经是空了?

经过对这几个问题的了解来判别面试者是单纯的背八股仍是有相关了解。这题真是太阴间了。

这些答案在本文最终都会做出解析。

拜访 Weak 变量时产生了什么?

这儿由于 -rewrite-objc 仅仅改写语法,对于 ARC 增加的代码以及 ARC Runtime Support 的解说则力不从心,因而这儿就不运用 rewrite 的方式辅佐剖析了,感兴趣的同学可以自行尝试。在仓库中也贴出了对应的结果。

the interaction between the ARC runtime and the code generated by the ARC compiler. This is not part of the ARC language specification; instead, it is effectively a language-specific ABI supplement, akin to the “Itanium” generic ABI for C++.

clang.llvm.org/docs/Automa…

xcrun -sdk iphonesimulator clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mios-version-min=12.1 -fobjc-runtime=ios-12.1 -Wno-deprecated-declarations WeakVariable.m

static void _I_SampleObject_testEqual(SampleObject * self, SEL _cmd) {
    BOOL testEqual = sWeakObject == self;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-extra-args"
    do {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-extra-args"
    if (!((testEqual))) { NSString *__assert_file__ = ((NSString * _Nullable (*)(id, SEL, const char * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), (const char *)"WeakVariable.m"); __assert_file__ = __assert_file__ ? __assert_file__ : (NSString *)&__NSConstantStringImpl__var_folders_b1_0fd1b6hs7lz0fm_mh346lybm0000gn_T_WeakVariable_cc93a6_mi_0; ((void (*)(id, SEL, SEL _Nonnull, id  _Nonnull __strong, NSString * _Nonnull __strong, NSInteger, NSString * _Nullable __strong, ...))(void *)objc_msgSend)((id)((NSAssertionHandler * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSAssertionHandler"), sel_registerName("currentHandler")), sel_registerName("handleFailureInMethod:object:file:lineNumber:description:"), (SEL)_cmd, (id _Nonnull)self, (NSString *)__assert_file__, (NSInteger)33, (((NSString *)&__NSConstantStringImpl__var_folders_b1_0fd1b6hs7lz0fm_mh346lybm0000gn_T_WeakVariable_cc93a6_mi_1)), (0), (0), (0), (0), (0)); }
#pragma clang diagnostic pop
    } while(0)
#pragma clang diagnostic pop
                                            ;
}

经过查看对应的汇编,可以对逻辑进行一个简化:

(lldb) dis
demo`-[SampleObject testEqual]:
    0x1001ad9fc <+0>:   sub    sp, sp, #0x40
    0x1001ada00 <+4>:   stp    x29, x30, [sp, #0x30]
    0x1001ada04 <+8>:   add    x29, sp, #0x30
    0x1001ada08 <+12>:  stur   x0, [x29, #-0x8]
    0x1001ada0c <+16>:  stur   x1, [x29, #-0x10]
    0x1001ada10 <+20>:  adrp   x0, 8
    0x1001ada14 <+24>:  add    x0, x0, #0x5d8            ; sWeakObject
->  0x1001ada18 <+28>:  bl     0x1001ae074               ; symbol stub for: objc_loadWeakRetained
    0x1001ada1c <+32>:  ldur   x8, [x29, #-0x8]          // 此时 x0 便是 objc_loadWeakRetained 的回来值,后续 x0 一向没有变过,直到再调用 objc_release 进行开释
    0x1001ada20 <+36>:  subs   x8, x0, x8
    0x1001ada24 <+40>:  cset   w8, eq
    0x1001ada28 <+44>:  str    w8, [sp, #0x18]
    0x1001ada2c <+48>:  bl     0x1001ae098               ; symbol stub for: objc_release // 留意 objc_release 传入的参数不是 location ,而是 <+32> 获取的回来值
    0x1001ada30 <+52>:  ldr    w8, [sp, #0x18]
    0x1001ada34 <+56>:  and    w8, w8, #0x1
    0x1001ada38 <+60>:  sturb  w8, [x29, #-0x11]
    0x1001ada3c <+64>:  b      0x1001ada40               ; <+68> at ViewController.m:81:5
    0x1001ada40 <+68>:  ldurb  w8, [x29, #-0x11]
    0x1001ada44 <+72>:  tbnz   w8, #0x0, 0x1001ada98     ; <+156> at ViewController.m:81:5
    0x1001ada48 <+76>:  b      0x1001ada4c               ; <+80> at ViewController.m
    0x1001ada4c <+80>:  ldr    x1, [sp, #0x8]
    0x1001ada50 <+84>:  adrp   x8, 8
    0x1001ada54 <+88>:  ldr    x0, [x8, #0x228]
    0x1001ada58 <+92>:  bl     0x1001ae100               ; objc_msgSend$currentHandler
    0x1001ada5c <+96>:  mov    x29, x29
    0x1001ada60 <+100>: bl     0x1001ae0b0               ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x1001ada64 <+104>: ldr    x1, [sp, #0x8]
    0x1001ada68 <+108>: str    x0, [sp, #0x10]
    0x1001ada6c <+112>: ldur   x2, [x29, #-0x10]
    0x1001ada70 <+116>: ldur   x3, [x29, #-0x8]
    0x1001ada74 <+120>: adrp   x4, 3
    0x1001ada78 <+124>: add    x4, x4, #0xc0             ; @"ViewController.m"
    0x1001ada7c <+128>: mov    x5, #0x51
    0x1001ada80 <+132>: adrp   x6, 3
    0x1001ada84 <+136>: add    x6, x6, #0xe0             ; @
    0x1001ada88 <+140>: bl     0x1001ae120               ; objc_msgSend$handleFailureInMethod:object:file:lineNumber:description:
    0x1001ada8c <+144>: ldr    x0, [sp, #0x10]
    0x1001ada90 <+148>: bl     0x1001ae098               ; symbol stub for: objc_release
    0x1001ada94 <+152>: b      0x1001ada98               ; <+156> at ViewController.m:81:5
    0x1001ada98 <+156>: b      0x1001ada9c               ; <+160> at ViewController.m:82:1
    0x1001ada9c <+160>: ldp    x29, x30, [sp, #0x30]
    0x1001adaa0 <+164>: add    sp, sp, #0x40
    0x1001adaa4 <+168>: ret    
(lldb) 

汇编或许有些门槛,咱们可以简化一下:获取一个 weak 变量,ARC Compiler 替咱们隐式地插入了两个函数,变成了如下的样子。

id returnWeakValue = objc_loadWeakRetained(sWeakObject);
// after last use sWeakObject
objc_release(returnWeakValue);

在咱们运用 sWeakObject 的期间,咱们是先 retain ,再 release 的,这样的规划契合 weak 的语义:假如你获得了 weak 变量的时分,weak 变量不是 nil,那这段运用时间内,这个变量都不会被开释。否则 weak 就变成用着用着或许突然消失了,这个肯定不是合理的规划。

这儿还需求留意的是,objc_release 接受的参数并不是 sWeakObect ,而是 returnWeakValue 。这是有差异的,这儿留一个作业,假如传入了 sWeakObect 会产生什么?

答案是 double free 。

objc_loadWeakRetained()

首要调用到了 obj->rootTryRetain() 办法,假如满足条件,就会直接回来 nil 。而在 dealloc 中拜访上面 demo 中的 sWeakObject 就会回来 NO ,进而直接回来 nil 。

关于一个链路上调用的 CustomRR 的解析,具体可以看:附录:CustomRR 。

/*
  Once upon a time we eagerly cleared *location if we saw the object 
  was deallocating. This confuses code like NSPointerFunctions which 
  tries to pre-flight the raw storage and assumes if the storage is 
  zero then the weak system is done interfering. That is false: the 
  weak system is still going to check and clear the storage later. 
  This can cause objc_weak_error complaints and crashes.
  So we now don't touch the storage until deallocation completes.
*/
id
objc_loadWeakRetained(id *location)
{
    id obj;
    id result;
    Class cls;
    SideTable *table;
 retry:
    // fixme std::atomic this load
    obj = *location;
    if (_objc_isTaggedPointerOrNil(obj)) return obj;
    table = &SideTables()[obj];
    table->lock();
    if (*location != obj) {
        table->unlock();
        goto retry;
    }
    result = obj;
    cls = obj->ISA();
    if (! cls->hasCustomRR()) { // 正常 cls 都不会有
        // Fast case. We know +initialize is complete because
        // default-RR can never be set before then.
        ASSERT(cls->isInitialized());
        if (! obj->rootTryRetain()) {
            result = nil;
        }
    }
    else {
        // Slow case. We must check for +initialize and call it outside
        // the lock if necessary in order to avoid deadlocks.
        // Use lookUpImpOrForward so we can avoid the assert in
        // class_getInstanceMethod, since we intentionally make this
        // callout with the lock held.
        if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
            BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
                lookUpImpOrForwardTryCache(obj, @selector(retainWeakReference), cls);
            if ((IMP)tryRetain == _objc_msgForward) {
                result = nil;
            }
            else if (! (*tryRetain)(obj, @selector(retainWeakReference))) {
                result = nil;
            }
        }
        else {
            table->unlock();
            class_initialize(cls, obj);
            goto retry;
        }
    }
    table->unlock();
    return result;
}

rootRetain

实践按当时的链路传入时,tryRetain 必定为 YESvariant 必定为 RRVariant::Fast

运转着运转着就会走到对 newisa.isDeallocating() 的判别,继而在完结解锁等操作后,回来 nil

那接下来的问题便是 isDeallocating() 的完结以及何时被修正值了。

ALWAYS_INLINE bool
objc_object::rootTryRetain()
{
    return rootRetain(true, RRVariant::Fast) ? true : false;
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    oldisa = LoadExclusive(&isa().bits);
    // 省掉大量边界处理
    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa().bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa().bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa().bits, &oldisa.bits, newisa.bits)));
    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }
        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }
    return (id)this;
}

Before & In Dealloc

isDeallocating()

可以看到 isa_t 是一个 union ,其间一切成员都共享同一个内存方位。也便是说其间的 bits / cls / ISA_BITFIELD 都在同一块内存区域,仅仅读取形式会有所不同。

一些老版本的 runtime 的 ISA_BITFIELD 会有不同的处理,例如 uintptr_t deallocating : 1; 会有独自一位记载,而最新版本中已经不再需求了,所以这一位变成了 unused (为后续再次修正增加骚操作留下了空间)。

鄙人面的完结中咱们也可以看到 deallocating 是可以经过其他两位计数位核算出来的(核算特点)。

#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t has_cxx_dtor      : 1;                                       \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    uintptr_t bits;
private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;
public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h ,其实是复杂的展开,每一个 bit 的界说在最上面
    };
    bool isDeallocating() {
        // 判别特定的位数是否为 0 
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif
    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

因而接下来便是看是哪里触发了 setDeallocating() 函数,以及触发 setDeallocating() 函数在 dealloc 里整体的方位在哪里,当然也有或许 setDeallocating() 函数没有被调用,外部直接修正 extra_rchas_sidetable_rc

这儿先说定论:其实便是没有外部手动调用的,毕竟本来 extra_rchas_sidetable_rc 就不会一起存在,而且引证技能是一个个减或许加的,也因而是不需求外部手动调用 setDeallocating() 函数。

extra_rc && has_sietable_rc

这两个特点背过八股文的同学都知道,便是用来贮存引证计数的,假如引证计数持续增加,直到 extra_rc 不行存了,就会存到 sidetable 里,这时分 sidetable_rc 就会成为 1 了。

SideTables 内包含一个 RefcountMap,用来保存引证计数,根据目标地址取出其引证计数,类型是 size_t

更重要的是,假如 主动引证计数 为 1,extra_rc 实践上为 0,由于它保存的是额定的引证计数,咱们经过这个行为可以削减许多不必要的函数调用。

黑箱中的 retain 和 release

了解了 extra_rchas_sidetable_rc 的意思后,咱们就更能了解为什么可以经过这两个特点核算出 是否 deallocating 了,由于只要额定的引证计数一旦为 0 了(即 没有其他强引证了),而且又在 release 的逻辑中,那就会触发实践的开释,这个是契合知识的。

开端 Dealloc 之前产生了什么?

咱们知道 dealloc 是从子类调用到父类,因而是从咱们自己完结的 dealloc 开端调用起。从咱们的表现来看,在进行 -(void)dealloc 之前,就已经完结了设置。因而需求先剖析 dealloc 是怎么触发的,在触发的链路上咱们来看对 extra_rchas_sidetable_rc 的处理。

先上调用仓库。会有两种情况,但迥然不同,首要便是有没有触及 sidetable 罢了。

- objc_object::rootRelease
    - objc_object::sidetable_release
        - objc_object::performDealloc
- objc_object::rootRelease
    - objc_object::performDealloc
objc_object::performDealloc()

大部分类不会自己完结 _objc_initiateDealloc 办法,假如必定需求自界说的话,需求调用 _class_setCustomDeallocInitiation 办法。这个跟本文剖析无关,想详细了解可以看:附录:_class_setCustomDeallocInitiation。

因而会在 performDealloc() 中直接经过消息转发调用 dealloc 办法,走到正常的路径中。

void
objc_object::performDealloc()
{
    if (ISA()->hasCustomDeallocInitiation())
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(_objc_initiateDealloc));
    else
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
objc_object::sidetable_release

sidetable_release 是被 objc_object::rootRelease 调用的。

// rdar://20206767
// return uintptr_t instead of bool so that the various raw-isa 
// -release paths all return zero in eax
uintptr_t
objc_object::sidetable_release(bool locked, bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa().nonpointer);
#endif
    SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    if (!locked) table.lock();
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second;
    if (it.second) {
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        this->performDealloc();
    }
    return do_dealloc;
}
objc_object::rootRelease
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;
    bool sideTableLocked = false;
    isa_t newisa, oldisa;
    oldisa = LoadExclusive(&isa().bits);
    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa().bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }
    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa().bits);
            return false;
        }
    }
retry:
    do {
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa().bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits)));
    if (slowpath(newisa.isDeallocating()))
        goto deallocate;
    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;
 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    // abandon newisa to undo the decrement
    newisa = oldisa;
    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa().bits);
            return rootRelease_underflow(performDealloc);
        }
        // Transfer retain count from side table to inline storage.
        if (!sideTableLocked) {
            ClearExclusive(&isa().bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            oldisa = LoadExclusive(&isa().bits);
            goto retry;
        }
        // Try to remove some retain counts from the side table.        
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;
            bool stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
            if (!stored && oldisa.nonpointer) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }
            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa().bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa().bits);
                goto retry;
            }
            // Decrement successful after borrowing from side table.
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();
            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
deallocate:
    // Really deallocate.
    ASSERT(newisa.isDeallocating());
    ASSERT(isa().isDeallocating());
    if (slowpath(sideTableLocked)) sidetable_unlock();
    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
    if (performDealloc) {
        this->performDealloc();
    }
    return true;
}

可以看到在 rootRelease 中当额定的引证技能归零的时分,就会 goto deallocate; ,并触发实践的开释流程。

Dealloc 内部的调用链路

最终咱们补齐下 dealloc 的内部次序,算是完结最终一块拼图。


// Replaced by NSZombies
// in NSObject implementation
- (void)dealloc {
    _objc_rootDealloc(self);
}
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 {
        // 咱们由于这个目标肯定是有 weakly_referenced 的,因而走这个分支
        object_dispose((id)this);
    }
}
id
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);
    free(obj);
    return nil;
}
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is 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); // 在这儿调用触发 .cxx_destruct 办法,开释 strong 的变量们
        if (assoc) _object_remove_associations(obj, /*deallocating*/true);
        obj->clearDeallocating(); // 清空引证计数并铲除弱引证表,将一切运用 __weak 润饰的指向该目标的变量置为nil
    }
    return obj;
}

实践最终去将 __weak 润饰的指向该目标的变量置为 nil 在最终一步。执行完才会实践将内存里清空。

定论

  1. 在 lldb 里直接 po weak 变量 跟实践代码的拜访有什么不同?

lldb 的拜访是直接读取内存,而代码的拜访套了 objc_loadWeakRetained() 办法。所见不即所得。

  1. 拜访一个 weak 变量实践经过了哪些进程?

先调用 objc_loadWeakRetained() 拜访计数 + 1,确保在拜访进程中不会被开释。在运用完毕后再将 引证计数 -1 。

  1. weak 变量到底在什么时分会被置空?

内存中的实践情况话,是在 objc_destructInstance 中的最终一步,不在事务代码中能触及到的规模中。自己完结的子类的 dealloc 办法远早于这个机遇。

  1. 咱们知道 deallocobjc_destructInstance 之前, 在 objc_destructInstance 中,咱们知道是先开释 strong 变量,再开释 关联目标,最终将一切运用 __weak 润饰的指向该目标的变量置为 nil 。但为什么在 dealloc 里拜访时,weak 变量已经是空了?

objc_loadWeakRetained() 替咱们做出了保护。

附录:_class_setCustomDeallocInitiation

假如你调用了这个办法,可以完结相似:我这个类必定要在主线程里被开释 相似的问题。咱们的播放器之前会遇到相似在 dealloc 里 dispatch 的风险操作(必定要在主线程中中止播放器),可以考虑用这个计划。

static inline void XXXRunOnMainThread(void (^block)(void)) {
    if (!block) return;
    if ([NSThread isMainThread]) {
        block();
    } else {
        dispatch_async(dispatch_get_main_queue(), block);
    }
}
// maybe called in global queue
- (void)dealloc {
    id someStrongProperty = _aStrongProperty;
    XXXRunOnMainThread(^{
        [someStrongProperty doSomethingMustInMainThread];
    });
}

本地验证可行,iOS 16 对应的新版本 runtime 里新加的,而且苹果也是用这个办法确保 ViewController 必定在主线程中开释的。

image.png
github.com/SwiftOldDri…

/**
 * Mark a class as having custom dealloc initiation.
 *
 * NOTE: if you adopt this function for something other than deallocating on the
 * main thread, please let the runtime team know about it so we can be sure it
 * will work properly for your use case.
 *
 * When this is set, the default NSObject implementation of `-release` will send
 * the `_objc_initiateDealloc` message to instances of this class instead of
 * `dealloc` when the refcount drops to zero. This gives the class the
 * opportunity to customize how `dealloc` is invoked, for example by invoking it
 * on the main thread instead of synchronously in the release call.
 *
 * A default implementation of `_objc_initiateDealloc` is not provided. Classes
 * must implement their own.
 *
 * The implementation of `_objc_initiateDealloc` is expected to eventually call
 * `[self dealloc]`. Note that once `_objc_initiateDealloc` is sent, the object
 * is in a deallocating state. This means:
 *
 * 1. Retaining the object will NOT extend its lifetime.
 * 2. Releasing the object will NOT cause another call to `dealloc` or
 *    `_objc_initiateDealloc`.
 * 3. Existing weak references to the object will produce `nil` when read.
 * 4. Forming new weak references to the object is an error.
 *
 * Because the implementation of `_objc_initiateDealloc` will call
 * `[self dealloc]`, it necessarily runs before any subclass overrides of
 * `dealloc`. Overrides of `dealloc` often rely on the superclass state still
 * being intact and usable, so ensure that `_objc_initiateDealloc` does not free
 * resources that a subclass might still try to access. Most or all of your
 * object teardown work should continue to be in `dealloc` to preserve the
 * expected sequence of events.
 *
 * This call primarily exists to support classes which need to deallocate on the
 * main thread. This can be accomplished by setting the class to use custom
 * dealloc initiation, and then implementing `_objc_initiateDealloc` to call
 * dealloc on the main thread. For example:
 *
 * ```
 * _class_setCustomDeallocInitiation([MyClass class]);
 *
 * - (void)_objc_initiateDealloc {
 *     if (pthread_main_np())
 *         [self dealloc];
 *     else
 *         dispatch_async_f(dispatch_get_main_queue(), self,
 *             _objc_deallocOnMainThreadHelper);
 * }
 * ```
 *
 * (We use `dispatch_async_f` to avoid an unsafe capture of `self` in a block,
 * which could result in the object being released by Dispatch after being
 * freed.)
 *
 * @param cls The class to modify.
 */
OBJC_EXPORT void
_class_setCustomDeallocInitiation(_Nonnull Class cls);

这个也便是体系在 iOS 16 上确保 VC 必定在主线程开释的计划。

//
//  ViewController.m
//  demo
//
//  Created by ByteDance on 2022/11/29.
//
#import "ViewController.h"
#import <objc/message.h>
#import <pthread/pthread.h>
extern void _class_setCustomDeallocInitiation(_Nonnull Class cls);
extern void _objc_deallocOnMainThreadHelper(void * _Nullable context);
static __weak id sWeakObject = nil;
@interface SampleObject : NSObject
@end
@implementation SampleObject
- (instancetype)init {
    if (self = [super init]) {
        sWeakObject = self;
        [self testEqual];
    }
    return self;
}
- (void)dealloc {
    [self testEqual];
}
- (void)testEqual {
    BOOL testEqual = sWeakObject == self;
}
- (void)_objc_initiateDealloc {
    if (pthread_main_np()) {
        [self performSelector:NSSelectorFromString(@"dealloc")]; // make ARC(clang) happy
    }
    else {
        dispatch_async_f(dispatch_get_main_queue(), (__bridge void * _Nullable)(self), _objc_deallocOnMainThreadHelper);
    }
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    _class_setCustomDeallocInitiation(SampleObject.class);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __autoreleasing SampleObject *object = [[SampleObject alloc] init];
    });
}
@end

image

附录:CustomRR

大部分 Class 都不会覆写,ARC 下是不允许覆写的,可是 MRC 下是可以的。

RR 的语义猜测是指 Retain/Release (网上没有找到清晰的阐明),代指最常见的两个办法(实践不止这两个)。

// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<2)
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // 省掉大量函数
    // ...
    bool hasCustomRR() const {
        return !bits.getBit(FAST_HAS_DEFAULT_RR);
    }
    void setHasDefaultRR() {
        bits.setBits(FAST_HAS_DEFAULT_RR);
    }
    void setHasCustomRR() {
        bits.clearBits(FAST_HAS_DEFAULT_RR);
    }
}

这个计数位是经过 RRScanner 来统计的,便是在 Class & MetaClass 目标初始化的时分动态得去判别 Class List 里是否存在。这个阶段也是在所谓的 runtime lock 阶段的。

/***********************************************************************
* Locking: write-locks runtimeLock
**********************************************************************/
void
objc_class::setInitialized()
{
    Class metacls;
    Class cls;
    ASSERT(!isMetaClass());
    cls = (Class)this;
    metacls = cls->ISA();
    mutex_locker_t lock(runtimeLock); // 对 runtimeLock 加锁,脱离 Scope 后开释
    // Special cases:
    // - NSObject AWZ  class methods are default.
    // - NSObject RR   class and instance methods are default.
    // - NSObject Core class and instance methods are default.
    // adjustCustomFlagsForMethodChange() also knows these special cases.
    // attachMethodLists() also knows these special cases.
    objc::AWZScanner::scanInitializedClass(cls, metacls);
    objc::RRScanner::scanInitializedClass(cls, metacls);
    objc::CoreScanner::scanInitializedClass(cls, metacls);
    // 省掉下面的代码了
    // ... 
}

RRScanner 承继自 scanner::Mixin ,还有众多 Scanner 都承继自这个类,例如 AWZScanner 与 CoreScanner ,起到了不同的 Runtime 扫描作用。这些都有对应的 技能位 用来贮存信息,太具体得咱们可以自行翻看。

AWZScanner 重视 +alloc / +allocWithZone:

CoreScanner 重视 +new, ±class, ±self, ±isKindOfClass:, ±respondsToSelector

// Retain/Release methods that are extremely rarely overridden
//
// retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
struct RRScanner : scanner::Mixin<RRScanner, RR, PrintCustomRR
#if !SUPPORT_NONPOINTER_ISA
, scanner::Scope::Instances
#endif
> {
    static bool isCustom(Class cls) {
        return cls->hasCustomRR();
    }
    static void setCustom(Class cls) {
        cls->setHasCustomRR();
    }
    static void setDefault(Class cls) {
        cls->setHasDefaultRR();
    }
    static bool isInterestingSelector(SEL sel) {
        return sel == @selector(retain) ||
               sel == @selector(release) ||
               sel == @selector(autorelease) ||
               sel == @selector(_tryRetain) ||
               sel == @selector(_isDeallocating) ||
               sel == @selector(retainCount) ||
               sel == @selector(allowsWeakReference) ||
               sel == @selector(retainWeakReference);
    }
    template <typename T>
    static bool scanMethodLists(T *mlists, T *end) {
        SEL sels[8] = {
            @selector(retain),
            @selector(release),
            @selector(autorelease),
            @selector(_tryRetain),
            @selector(_isDeallocating),
            @selector(retainCount),
            @selector(allowsWeakReference),
            @selector(retainWeakReference),
        };
        return method_lists_contains_any(mlists, end, sels, 8);
    }
};

这儿可以引申出去,你假如在 ARC 环境下,动态得去 overrite/hook retain 等函数覆盖原有完结,可以断点到吗?其实断不到,除非你再把这个 FAST_HAS_DEFAULT_RR 对应的技能位改掉,否则会直接调用到 C 函数,只要计数位对了才会经过 lookUpImpOrForward 进行转发,并走到自己界说的办法里。

当然我没试过,这儿后续给一个 demo 试一下。

引证链接:

github.com/zhangferry/…

探秘Runtime – Runtime加载进程 –

iOS weak 底层完结原理(四):ARC 和 MRC 下 weak 变量的拜访进程 –

OC内存办理–引证计数器 –

draveness.me/rr/

kikido.github.io/2019/06/24/…