iOS 底层 内存管理之引用计数存储 - AlvinSunny/OC-TheUnderlying GitHub Wiki

写在前面:

自从64位操作系统以后,引用计数被直接存储在优化过的ISA(共用体)指针的extra_rc中,extra_rc拥有19位的内存来存储引用计数减一, 随着持有者的增多引用计数也随之增大,当引用计数值过大,extra_rc中无法存储时,就会把大于extra_rc的值存储到一个SideTable类的refcnts属性中;获取时先取出extra_rc + refcnts 计算后返回

图解:

引用计数存储.png

SideTable本质上是一个结构体,如下图:

  • 源码定义
struct SideTable {
    spinlock_t slock;             
    RefcountMap refcnts;     
    weak_table_t weak_table;
}
  • slock: 自旋锁,用来操作SideTable时加锁解锁
  • refcnts: 是Reference Counting的简写,是一个存放着对象引用计数的散列表
  • weak_table : 散列表, 存储指向OC对象弱引用指针

这一顿操作猛如虎,我咋还晕着呢 ?SideTable到底是个啥 ?

不要慌!不要慌!不要慌!😁😁😁😁😁😁😁

  • 为了管理程序中所有对象的引用计数weak指针,runtime创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。来管理引用计数和weak指针。

获取一个OC对象的引用计数方法是什么 ?

MRC环境下直接调用对象的retaionCount方法获取

此方法在调试内存管理问题时没有价值。
因为许多框架对象可能保留了一个对象来保存对它的引用,而同时autorelease池可能保存了一个对象上的任意数量的延迟发布,
所以您不太可能从这个方法中获得有用的信息。
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE;

注意:只有在对象没有被添加在自动缓存池且没有被其他框架引用的情况下,返回的值才绝对有效

ARC环境使用CoreFoundation中CFGetRetainCount()获取

此函数可能有助于调试内存泄漏,返回核心基础对象的引用计数
CFIndex CFGetRetainCount(CFTypeRef cf);

有存储肯定也有擦除

擦除的具体实现在runtime底层源码的NSObject.mm文件的:void objc_object::clearDeallocating_slow()

  • 核心方法:table.refcnts.erase
  • erase:擦除
  • 实现逻辑:找到当前对象对应的引用计数表中的对象,for循环free掉它 !

需要了解全部过程请移步iOS 底层 - 内存管理之weak指针


NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    //获取当前obj对应的SideTable
    SideTable& table = SideTables()[this];
    //加锁
    table.lock();
    
    //有weak指针指向
    if (isa.weakly_referenced) {
        //传入弱引用表和当前对象,清空
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    
    //引用计数存储在sidetable中
    if (isa.has_sidetable_rc) {
        //传入当前对象,擦除存储
        table.refcnts.erase(this);
    }
    //解锁
    table.unlock();
}

用lldb打印一个对象的引用计数

NSObject *obj = [[NSObject alloc] init];
在obj实例化后打断点,并在lldb中输出isa来验证:

(lldb) p obj
(NSObject *) $0 = 0x000000010064b810
//读取obj对象的内存,第一个为成员isa
(lldb) x/4gx $0
0x10064b810: 0x001d800100350141 0x0000000000000000
0x10064b820: 0x64696c53534e5b2d 0x206b636172547265
//打印isa的值
(lldb) p 0x001d800100350141
(long) $1 = 8303516111405377
//这里需要声明成isa_t的结构才能输出
(lldb) p (isa_t)$1
(isa_t) $2 = {
  cls = NSObject
  bits = 8303516111405377
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 537305128
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}
(lldb) 

关于引用计数讲解很好的一篇文章 --> iOS原理 引用计数