ARC
代码编译阶段,在上下文中自动成对插入MRC下的retain和release方法,保证通过引用计数正确的管理内存(针对堆上)。
iOS中引用计数的存储方案
1、TaggedPointer下的小内存对象,直接返回指针值作为引用计数。NSString会根据长度(小于60字节)决定是否使用。但深拷贝后变为普通指针。
2、nonpointer_isa:OC2.0+64位,使用对象的isa指针的第一位标记是否使用了优化后的isa,后8位来存储引用计数(溢出后移出部分正常管理)。
3、Runtime使用一张散列hash表(SideTables)来管理,SideTable中的RefcountMap属性(还有weak表自旋锁两个属性)。注:objc_object::isTaggedPointer() 获取TAG_MASK标识位以判断是否使用了TaggedPoint。
SideTable
SideTables全局hash数组长度64,实际是StripedMap类型,里面储存了64个SideTable(为了避免资源争抢,所以不存一个表里),许多obj共用一个SideTable来存储引用计数和弱引用表相关信息。
SideTable结构体
自旋锁 spinlock_t slock // 保证原子操作防止多线程读取问题
引用计数表 RefcountMap refcnts // 散列表结构存储对象的持有者地址和引用计数,Zombie异常时也能定位对象地址信息。
弱引用表 weak_table_t weak_table //保存了许多对象的,所有的weak引用,对象地址作为key,weak_entries作为值保存所有指向该对象的weak指针,dealloc时把所有weak指针设为nil,避免野指针。
注:static SideTable *tableForPointer(const void *p); // 获取对象地址的sidetable
注:一个sidetable对应一个weaktable,一个weaktable对象中有无数个weakentry(通过对象地址作为key获取对应的),每个weakentry保存了这个对象的所有弱引用。
获取引用计数:retainCount
objc_object的rootRetainCount()方法
1、判断储存逻辑(TaggedPointer直接获取 / 优化后的isa,指针的后19位即extra_rc变量 / 散列表中获取)
2、sidetable_retainCount(),先SideTable::tableForPointer(this)获取SideTable对象,table.refcnts即引用计数的hash表
3、it != table->refcnts.end()(如果相等则引用计数返回1)根据键值对以对象为key获取引用计数的值并+1返回,所以实际计数应该为retainCount-1。
注:refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT(=2); // 返回时做了向右位移两位的操作,因为前两个bit,WEAKLY_REFERENCED标识是否有weak对象;DEALLOCATING是否正在析构。
修改引用计数:retain和release
总结:二者的实现机制类似,概括讲就是通过第一层 hash 算法,找到 指针变量 所对应的 sideTable。然后再通过一层 hash 算法,找到存储 引用计数 的 size_t,然后对其进行增减操作。retainCount 不是固定的 1,SIZE_TABLE_RC_ONE 是一个宏定义,实际上是一个值为 4 的偏移量。
retain方法
底层调用_objc_rootRetain、objc_object::rootRetain()、objc_object::sidetable_retain()、_slow四步方法,其中最重要的是增加引用计数的id objc_object::sidetable_retain()虚函数,详细实现如下:
SideTable& table = SideTables()[this]; // 传入对象地址获取对应的SideTable对象。
size_t& refcntStorage = table.refcnts[this]; // 获取 引用计数 的引用
!(refcntStorage & SIDE_TABLE_RC_PINNED) // 没有越界
refcntStorage += SIDE_TABLE_RC_ONE; // 引用计数增加(实际增加了 1UL<<2 == 4)
注1:refcntStorage后两位被weak和析构状态占领,首位标识越界,所以不是增加1。
注2:refcnts为散列表,可能存了多个引用计数以处理引用计数越界情况,retainCount方法可以证明。
uintptr_t objc_object::sidetable_retainCount() // 引用计数总返回1+计数表,所以总不为0
{ it != table.refcnts.end() refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT; }
release方法
四步方法与retain类似,其中减少引用计数的函数uintptr_t objc_object::sidetable_release(bool performDealloc)实现总结如下:
1、内部判断是否dealloc:it == table.refcnts.end()(引用计数为表中最后一个,直接标记析构,table.refcnts[this] = SIDE_TABLE_DEALLOCATING)
2、it->second < SIDE_TABLE_DEALLOCATING(在-1前验证引用计数是否为0,如果是,标记正在析构并发送dealloc消息,否则才-1。do_dealloc=true; it->second |= SIDE_TABLE_DEALLOCATING;)
3、it->second -= SIDE_TABLE_RC_ONE(引用计数-1,实际偏移两位)
注:SIDE_TABLE_DEALLOCATING作为引用计数归0的判断,减少了标记变量内存的额外占用,也避免负数产生。注:为什么isa中的extra_rc、sidetable中的refcnts中,保存的值都是真正的引用计数-1?因为获取时是+1后返回的,保证了释放时-1不会出现负数。