iOS内存管理|你真的理解Retain和Release了么?结合一个题目聊聊

627 阅读3分钟

前言

之前我们在WWDC20-runtime优化中有初步接触到了 TaggedPointer 。本文就讲结合一个面试题加深对它的理解。

一、 一个面试题

分析一下下面的代码会有什么问题

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) dispatch_queue_t  queue;
@property (nonatomic, strong) NSString *nameStr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self taggedPointerDemo];

}

- (void)taggedPointerDemo {
  
    self.queue = dispatch_queue_create("Ryukie", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<100000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"Ryukie"];
             NSLog(@"%@",self.nameStr);
        });
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (int i = 0; i<100000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"RyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukie"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

@end

1.1 分析

乍一看本题考验的是多线程读写,很难想到和 TaggedPointer 有关。本题两处中的不同点是字符串的长度而已。而根据我们之前对 TaggedPointer 的了解,其中的 Payload 会用来存储数据。但是 Payload 的长度是有限的,过长的数据就无法完整保存进去。

所以,这里分析两处的指针类型是不同的。

1.2 断点调试

较短的字符串

01-01.png

这里发现指针类型为 NSTaggedPointerString 。明显是一个 TaggedPointer

较长的字符串

01-02.png

这里的指针类型为 __NSCFString 是一个普通的指针。

1.3 问题

01-03.png

在第二段代码运行中出现了野指针的崩溃。

1.4 思考

根据奔溃的现象,你可能会想到。 TaggedPointerRetainRelease 难道和普通的有不同么?

二、 源码分析

在 OC 源码中找到:

- (id)retain {
    return _objc_rootRetain(self);
}

进一步找到具体实现:

objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;
    ...
}

通过 Retain 的源码,我们可以看出,在判断是 TaggedPointer 之后就直接 return 了,没有进行任何操作。

那么 Release 呢?我们探索一下源码:

objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;
    ...
}

发现这里和 Retain 是一样的处理。

三、 题目分析

初步了解了 Retain 和 Release 对与 TaggedPointer 之后,我们就很容易理解上面的面试题所发生的现象了

  • TaggedPointer 的较短的字符串没有进行 Retain Release 所以多线程异步读写过程中没有出现野指针错误
  • TaggedPointer 的较长的字符串,由于进行了多线程异步读写操作,不断的 Retain Release ,所以可能出现野指针错误。

小结

  • TaggedPointer 是用于特定类型的小对象的,并不是那些类型就都是 TaggedPointer 。前提是 Payload 要能够装得下。
  • Retain ReleaseTaggedPointer 的特殊处理,也提高了 TaggedPointer 的效率,同时在多线程场景下的安全性也比较高。

到现在我们接触到了三种指针: TaggedPointerNonpointer isa纯isa 。这里我们继续深入对 Retain Release 源码进行探索,看看他们在内存管理上有什么区别。

四、 Retain

objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    // 不处理 TaggedPointer 直接 return
    if (slowpath(isTaggedPointer())) return (id)this;

    ...

    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            // 非 nonpointer 通过 sidetable 处理
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        
        ...

        uintptr_t carry; // 标志 extra_rc 容量是否还能够承受
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // 超出 extra_rc 容量
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // 保留一般的引用计数,并将另一半拷贝到 side table
            // 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;
}

小结

  • TaggedPointer 不处理,直接 return
  • Nonpointer isa
    • BITFIELDSideTable 结合使用
      • BITFIELDextra_rc 容量足够的时候持续 ++
      • BITFIELDextra_rc 容量不够的时候
        • extra_rc 减半,拷贝一半到 SideTable 进行管理
        • 同时标志位 has_sidetable_rc 改变
  • 纯isa
    • 通过 SideTable 管理

五、 Release

objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    // 不处理 TaggedPointer 直接 return
    if (slowpath(isTaggedPointer())) return false;

    ...

retry:
    do {
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            // 非 nonpointer 通过 sidetable 处理
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        ...
        // 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()
            // extra_rc 见底 跳转执行 underflow
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        goto deallocate;

    ...
    return false;

 underflow:
    // extra_rc 见底后,从 SideTable 拿回引用计数,或者销毁
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        // 如果有使用 SideTable
        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;
        }

        // 从 SideTable 移除引用计数
        // 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;
            // 修改 extra_rc 及标志位
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;
            ...
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

deallocate:
    // Really deallocate.
    // 销毁
    ...

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

小结

  • TaggedPointer 不处理,直接 return
  • Nonpointer isa
    • BITFIELDSideTable 结合使用
      • BITFIELDextra_rc 容量未见底的时候持续 --
      • BITFIELDextra_rc 容量见底的时候
        • 检查标志位 has_sidetable_rc
          • 使用了 SideTable
            • SideTable 中的引用计数转移回 BITFIELDextra_rc 进行处理
          • 未使用 SideTable
            • 需要销毁
  • 纯isa
    • 通过 SideTable 管理

总结

通过对 RetainRelease 源码的探索,发现使用 TaggedPointerNonpointer isa 在内存管理的效率上会更高。一旦 SideTable 介入,流程就回变的复杂还要上锁解锁,影响效率。虽然一般开发过程中很难遇到这种情况,但是理解了这些后,对一些问题一些现象就能更清楚的看到本质。

参考

对象本质探究与isa

内存管理