探寻Objective-C引用计数本质

简介: 本文涉及到的CPU架构为arm64,其它架构大同小异。源码来自苹果开源-runtime。Objective-C中采用引用计数机制来管理内存,在MRC时代,需要我们手动retain和release,在苹果引入ARC后大部分时间我们不用再关心引用计数问题。

本文涉及到的CPU架构为arm64,其它架构大同小异。
源码来自苹果开源-runtime

Objective-C中采用引用计数机制来管理内存,在MRC时代,需要我们手动retainrelease,在苹果引入ARC后大部分时间我们不用再关心引用计数问题。但是为了深入Objective-C本质,引用计数究竟是怎么实现的还是值得我们去探寻的。

ISA

OC中的对象的实质其实是结构体,其中大部分对象都有isa,指向类对象(有一种神奇的存在叫做Tagged Pointer),源码中关于对象结构体objc_object定义如下:

// objc-private.h
struct objc_object {
private:
    isa_t isa;
public:
    id retain();
    void release();
    id autorelease();
    ... //省略了其它方法,感兴趣可以直接看源码

Tagged Pointer

除了有一种特殊的对象Tagged Pointer,这种类型的对象值就存在指针当中,存取性能高。可以用来存储少量数据的对象,例如NSNumber、NSDate、NSString。(更多Tagged Pointer知识,推荐这篇文章)。也就没有引用计数、内存释放的问题。

NONPOINTER ISA

arm64架构isa占64位,苹果为了优化性能,存储类对象地址只用了33位,剩下的位用来存储一些其它信息,比如本文讨论的引用计数。

NONPOINTER ISA存储的字段定义如下:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        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 deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

extra_rc

那引用计数存在哪里呢?秘密就在extra_rc中。

extra_rc只是存储了额外的引用计数,实际的引用计数计算公式:引用计数=extra_rc+1

extra_rc占了19位,可以存储的最大引用计数:$2^{19}-1+1=524288$,超过它就需要进位到SideTables。SideTables是一个Hash表,根据对象地址可以找到对应的SideTableSideTable内包含一个RefcountMap,根据对象地址取出其引用计数,类型是size_t
它是一个unsigned long,最低两位是标志位,剩下的62位用来存储引用计数。我们可以计算出引用计数的理论最大值:$2^{62+19}=2.417851639229258e24$。

其实isa能存储的524288在日常开发已经完全够用了,为什么还要搞个Side Table?我猜测是因为历史问题,以前cpu是32位的,isa中能存储的引用计数就只有$2^{7}=128$。因此在arm64下,引用计数通常是存储在isa中的。

retain

有了前面的铺垫,我们知道引用计数怎么存储的了,那引用计数又是怎么改变的呢?通过剖析retain源码我们就可以得出结论了。
objc_object的方法全部定义在objc-object.h文件中,全是内联函数,应该是为了性能的考虑。

我们来看看retain的函数定义

inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

这层比较简单,做了三件事情:

  1. 判断指针是不是Tagged Pointer
  2. 判断是否有自定义retain,如果有调用自定义的。
  3. 最后调用rootRetain

我们来看看关键函数rootRetain的实现(为了便于阅读,代码有所删减)

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    isa_t oldisa;
    isa_t newisa;

    // 加锁,用汇编指令ldxr来保证原子性
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    
    if (newisa.nonpointer = 0) {
        // newisa.nonpointer = 0说明所有位数都是地址值
        // 释放锁,使用汇编指令clrex
        ClearExclusive(&isa.bits);
        
        // 由于所有位数都是地址值,直接使用sidetable来存储引用计数
        return sidetable_retain();
    }
    
    // 存储extra_rc++后的结果
    uintptr_t carry;
    // extra_rc++
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
    
    if (carry == 0) {
        // extra_rc++后溢出,进位到side table
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
        sidetable_addExtraRC_nolock(RC_HALF);
    }
        
    // 将newisa写入isa
    StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
    return (id)this;
}

有一个细节可以了解下,如何用汇编来实现原子性操作。

static ALWAYS_INLINE
uintptr_t 
LoadExclusive(uintptr_t *src)
{
    uintptr_t result;
    // 在多核CPU下,对一个地址的访问可能引起冲突,ldxr解决了冲突,保证原子性。
    asm("ldxr %x0, [%x1]" 
        : "=r" (result) 
        : "r" (src), "m" (*src));
    return result;
}

release

release代码逻辑基本上就是retain反过来走一遍,有点不同的是在引用计数减到0时,会调用对象的dealloc方法。

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    isa_t oldisa;
    isa_t newisa;
    
retry:
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    if (newisa.nonpointer == 0) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return sidetable_release(performDealloc);
    }
    
    uintptr_t carry;
    // extra_rc--
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
    if (carry == 0) {
        // 需要从SideTable借位,或者引用计数为0
        goto underflow;
    }
    
    // 存储引用计数到isa
    StoreReleaseExclusive(&isa.bits,
                          oldisa.bits, newisa.bits)
    return false;
    
underflow:
    // 从SideTable借位
    // 或引用计数为0,调用delloc
    
    // 此处省略N多代码
    // 总结一下:修改Side Table与extra_rc,
    
    // 引用计数减为0时,调用dealloc
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

小结

引用计数存在哪?

  1. Tagged Pointer不需要引用计数
  2. NONPOINTER ISA(isa的第一位为1)的引用计数优先存在isa中,大于524288了进位到Side Tables
  3. NONPOINTER ISA引用计数存在Side Tables

retain/release的实质

  • 找到引用计数存储区域,然后+1/-1
  • 如果是NONPOINTER ISA,还要处理进位/借位的情况
  • release在引用计数减为0时,调用dealloc

博客链接,文中有什么不对的地方,欢迎各位在评论区留言讨论。


参考

目录
相关文章
|
6月前
|
安全 编译器 Swift
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
394 2
|
4月前
|
开发工具 iOS开发 容器
【Azure Blob】关闭Blob 匿名访问,iOS Objective-C SDK连接Storage Account报错
iOS Objective-C 应用连接Azure Storage时,若不关闭账号的匿名访问,程序能正常运行。但关闭匿名访问后,上传到容器时会出现错误:“Public access is not permitted”。解决方法是将创建容器时的公共访问类型从`AZSContainerPublicAccessTypeContainer`改为`AZSContainerPublicAccessTypeOff`,以确保通过授权请求访问。
【Azure Blob】关闭Blob 匿名访问,iOS Objective-C SDK连接Storage Account报错
|
6月前
|
缓存 开发工具 iOS开发
优化iOS中Objective-C代码调起支付流程的速度
优化iOS中Objective-C代码调起支付流程的速度
104 2
|
6月前
|
安全 JavaScript 前端开发
IOS开发基础知识:介绍一下 Swift 和 Objective-C,它们之间有什么区别?
IOS开发基础知识:介绍一下 Swift 和 Objective-C,它们之间有什么区别?
264 0
|
iOS开发 容器
iOS 代码规范格式 Objective-C(上)
iOS 代码规范格式 Objective-C
430 0
iOS 代码规范格式 Objective-C(上)