ARC
现在我们使用oc编程不用进行手动内存管理得益于ARC机制。ARC帮我们免去了大部分对对象的内存管理操作,其实ARC只是帮我们在合适的地方或者时间对对象进行-retain
或-release
,并不是不用进行内存管理。
引用计数的存储
通过我之前分析的oc对象内存结构可以知道,其实对象的引用计数是存放在对象的isa
指针中,isa
在OBJC2
中是一个经过优化的指针不单存放着类对象的地址还存放着其他有用的信息,其中就包括引用计数信息的存储。 isa_t
的结构位域中有两个成员与引用计数有关分别是
uintptr_t has_sidetable_rc : 1; //isa_t指针第56位开始占1位
uintptr_t extra_rc : 8 //isa_t指针第57位开始占8位
复制代码
extra_rc
存放的是可能是对象部分或全部引用计数值减1。
has_sidetable_rc
为一个标志位,值为1时代表 extra_rc
的8位内存已经不能存放下对象的retainCount
, 需要把一部分retainCount
存放地另外的地方。
retain源码分析
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//isTaggedPointer直接返回指针
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
//标识是否需要去查找对应的SideTable
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//这里是isa没有经过指针位域优化的情况,直接进入全局变量中找出对应的SideTable类型值操作retainCount
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
//是否溢出的标识 , 如果调用addc函数后 isa的extra_rc++后溢出的话carry会变成非零值
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed extra_rc 溢出了
if (!handleOverflow) {
ClearExclusive(&isa.bits);
//这里是重新调用rootRetain参数handleOverflow = true
return rootRetain_overflow(tryRetain);
}
//执行到这里代表extra_rc已经移除了,需要把 extra_rc 减半 ,把那一半存放到对应的SideTable类型值中
// 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 (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();
return (id)this;
}
复制代码
rootRetain
主要是处理isa
中extra_rc
中加法操作: 在extra_rc ++
没有溢出的情况下不用特殊处理,如果溢出的话把extra_rc
一半的值减掉,把减掉的值存到一个SideTable
类型的变量中。
关于SideTable
struct SideTable {
spinlock_t slock; //操作内部数据的锁,保证线程安全
RefcountMap refcnts;//哈希表[伪装的对象指针 : 64位的retainCoint信息值]
weak_table_t weak_table;//存放对象弱引用指针的结构体
}
复制代码
SideTabel
其实是一个包装了3个成员变量的结构体上面已注释各成员的作用,而RefcountMap refcnts
这个成员就是我们稍后重点要分析的存放对象额外retainCount
的成员变量。
存放SideTable的StripedMap类型全局变量
获取objc_object
对应的SideTable
类型变量
alignas(StripedMap) static uint8_t
SideTableBuf[sizeof(StripedMap)];
SideTable& table = SideTables()[this];
//函数SideTables() 实现
static StripedMap& SideTables() {
return *reinterpret_cast*>(SideTableBuf);
}
复制代码
可以看出所有对象对应的SideTable
。都存储在一个全局变量SideTableBuf
中,把SideTableBuf
定义成字符数组其目的是为了方便计算StripedMap
的内存大小,从而开辟一块与StripedMap
大小相同的内存。其实可以把 SideTableBuf
看成一个全局的StripedMap
类型的变量,因为SideTables()
方法已经把返回值SideTableBuf
强转成StripedMap
类型的变量。下面分析下StripedMap
这个类
template
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
}
复制代码
从上面定义可以看出StripedMap
类其实是包装了一个结构体的成员变量array
的哈希表,该成员变量是一个装着PaddedT
类型的数组,PaddedT
这个结构构体实际上就是我们模板类传进的SideTable
。因此这里可以把array
看成是一个装着SideTable
的容器,容量为8或64(运行的平台不同而不同)。
当系统调用 SideTables()[对象指针]
时,StripedMap
这个哈希表就会在array
中找出对应数组指针的SideTable
类返回,这里可以看出其中的一个SideTable
类变量可能对应多个不同的对象指针。
extra_rc++ 溢出处理
if (slowpath(transcribeToSideTable)) {
sidetable_addExtraRC_nolock(RC_HALF);
}
复制代码
执行到下面的if语句里面的 sidetable_addExtraRC_nolock(RC_HALF);
代表经过do
语句的执行逻辑得出extra_rc
已经溢出了,接下来看下溢出处理的实现
// Move some retain counts to the side table from the isa field.
// Returns true if the object is now pinned.
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
assert(isa.nonpointer);
SideTable& table = SideTables()[this];
size_t& refcntStorage = table.refcnts[this];
size_t oldRefcnt = refcntStorage;
// isa-side bits should not be set here
assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
//已经溢出了 直接返回true
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
//把 delta_rc 左已两位后与 oldRefcnt 相加 判断是否有溢出
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
// 溢出处理
// SIDE_TABLE_FLAG_MASK = 0b11 = SIDE_TABLE_DEALLOCATING + SIDE_TABLE_WEAKLY_REFERENCED
// SIDE_TABLE_RC_PINNED 溢出标志位
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else { //没有溢出
refcntStorage = newRefcnt;
return false;
}
}
复制代码
可以看出extra_c
溢出的时候是把一半值减掉后存进对应对象指针的SideTable
的成员变量RefcountMap refcnts
中。在弄清楚上面代码逻辑前,先看下几个重要的宏定义
// The order of these bits is important.
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
复制代码
通过宏定义及RefcountMap
的实现(下面会分析)可以发现refcntStorage
其实是一个8字节(64位)大小的内存其内存结构及对应的标识位如下图
根据上面的代码用this
指针获取存放在SideTable
内部引用计数refcntStorage
后,会分别判断这3个标识位都为0时才执行计数增加的操作,在调用addc
是也会执行 delta_rc << SIDE_TABLE_RC_SHIFT
左移的操作来避开相应的标识位后在相应的内存位上。如果相加后溢出了,会把最高的移除标识位置为1。
经过sidetable_addExtraRC_nolock
处理后isa指针中的extrc_rc
在溢出的情况下成功吧一半的数值移存到了对应SideTable
的refcntStorage
哈希表中,从而释放了isa.extra_rc
的内存继续记录retainCount
。
关于DenseMap
我们先看下存放extra_rc
溢出部分的RefcountMap
定义:
typedef objc::DenseMap,size_t,true> RefcountMap;
复制代码
可以看出 RefcountMap
其实是DenseMap
的模板类的别名, DenseMap
这是继承自DenseMapBase
的类,其内部实现可以看出DenseMap
其实是一个典型的哈希表(类似oc的NSDictionary
),通过分析可以发现关于DenseMap
的几点
- 模板的
KeyT
用DisguisedPtr
包装对象指针,此类是对对象指针值(obje_object *)的封装或说是伪装,使其不收内存泄露测试工具的影响。 - 模板的
ValueT
用size_t
代替,size_t
是一个64位内存的unsigned int
- 模板的
KeyInfoT
用DenseMapInfo
代替,在此处就相当于DenseMapInfo
,DenseMapInfo
封装了比较重要的方法哈希值的获取用于查找对应Key的内容。
DenseMapInfo 实现细节
主要为哈希表提供了KeyT的判等isEqual,以及KeyT类型值的hashValue的获取下面是代码实现
//Key判等实现,直接用 == 完成判等
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
//根据Key获取对应的hash值
static unsigned getHashValue(const T *PtrVal) {
//ptr_hash调用到下面的内联函数
return ptr_hash((uintptr_t)PtrVal);
}
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
key ^= key >> 4;
key *= 0x5052acdb;
key ^= __builtin_bswap32(key);
return key;
}
#endif
复制代码
DenseMap 根据 Key 查找 Value 实现
简化了源码看主要查找实现
//重写操作符[]
ValueT &operator[](const KeyT &Key) {
return FindAndConstruct(Key).second;
}
value_type& FindAndConstruct(const KeyT &Key) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return *TheBucket;
return *InsertIntoBucket(Key, ValueT(), TheBucket);
}
//查找实现
template
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
//存放所有内容的bucket数组
const BucketT *BucketsPtr = getBuckets();
//bucket个数
const unsigned NumBuckets = getNumBuckets();
//没有内容直接返回
if (NumBuckets == 0) {
FoundBucket = 0;
return false;
}
//根据Val的哈希值算出的bucket的索引 getHashValue调用的是KeyInfo的实现
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
while (1) {
//从buckets数组拿出对应索引的值
const BucketT *ThisBucket = BucketsPtr + BucketNo;
if (KeyInfoT::isEqual(Val, ThisBucket->first)) { //符合 key == indexOfKey
//赋值外面传进来的参数
FoundBucket = ThisBucket;
return true;
}
BucketNo += ProbeAmt++;
BucketNo&= (NumBuckets-1);
}
}
复制代码
release 源码分析
首先我们看下主要处理release
逻辑的方法实现
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
复制代码
方法主要分为几大逻辑模块
extra_rc --
后位溢出的情况处理ectra_rc --
后下溢出的情况处理- 全部引用计数已经减掉的情况处理
首先分析执行extra_rc--
后正常未下溢出的情况,此情况主要是通过subc
函数让newisa.bit
与RC_ONE(1ULL<<56)
相加,最后更新isa
的值。
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//计算溢出的标识位
uintptr_t carry;
// extra_rc-- RC_ONE -> (1<<56)在isa中刚好是extra_rc的开始位
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (slowpath(carry)) {
goto underflow;//下溢出了, 直接跳转下溢出的处理逻辑
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits))); // 把newisa.bits 赋值给isa.bits ,并退出 while 循环
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
复制代码
加入经过subc
函数的运算newisa.bits
发生了下溢出的话,直接跳转到underflow
的处理逻辑中。下面分析下underflow
的主要逻辑
underflow:
newisa = oldisa;
if (slowpath(newisa.has_sidetable_rc)) { //用SideTabel的refcnts
//为对应的SideTable加锁后在操作器内存数据
if (!sideTableLocked) {
sidetable_lock();
sideTableLocked = true;
//修改下 sideTableLocked = true; 重新调用retry
goto retry;
}
// 把一部分的refCount出来赋值给 borrowed
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
if (borrowed > 0) {
//把引用计数 - 1 后赋值给 extra_rc
newisa.extra_rc = borrowed - 1;
//更新isa的extra_rc
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
//下面是处理更新isa值失败的重试操作
if (!stored) {
// 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.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
//重试更新isa值还是失败的话,把borrowed再次存进对象的SideTable中。再周一遍retry的代码逻辑(开始的do while位置)
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
//执行到这里代表成功把对应SideTable的值转移了部分值到isa.ectra_rc中,并为对应SideTable类型值加锁
sidetable_unlock();
return false;
}
else {
//来到else语句的话代表对应SideTable已经没有存储额外的retainCount。接下来要执行对象内存释放的逻辑了。
}
}
复制代码
通过上面下溢出处理的代码分析可以知道,extra_rc--
后发生下溢出的话,系统会优先去查找对象对应SideTable
值中存储的哈希表refcnts
变量,在通过refcnts
查找到对应对象存储的8字节内存的count
去一部分出来(大小为isa.extra_rc
刚好溢出的一半大小),存放到isa.extra_rc
中。如果此时refcnts
取出的值也为0了就代表对象可以释放掉内存了。
对象内存释放的调用,主要是把isa.deallocating
的标识位置为1,然后执行SEL_dealloc
释放对象内存。
// 上面如果 borrowed == 0 来到这里代表retainCount等于0 对象可以释放了
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__sync_synchronize();
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); //代表引用计数已经等于0 调用dealloc释放内存
}
return true;
复制代码
总结
对象通过retain
与release
巧妙地使内部的isa.extra_rc
与外部存储在对应其本身的SideTable
类中存储的引用计数值增减有条不紊地进行着加减法。并通过判断当两个值都满足一定条件时就执行对象的SEL_dealloc
消息,释放内存