前言
iOS-內存管理分析(上)一文我們分析了內存的五大區,taggedPointer,retain,release的底層分析, 這篇文章繼續分析內存管理的其它相關知識。
1 散列表結構分析
散列表到底是什么東西,它有什么作用,我們現在來分析下。
散列表其實就是哈希表。只是對當前表的名字
首先我們在objc的源碼中搜索一下rootRetain,找到如下代碼
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
oldisa = LoadExclusive(&isa.bits);
if (variant == RRVariant::FastOrMsgSend) {
// These checks are only meaningful for objc_retain()
// They are here so that we avoid a re-load of the isa.
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
if (slowpath(!oldisa.nonpointer)) {
// a Class is a Class forever, so we can perform this check once
// outside of the CAS loop
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
do {
transcribeToSideTable = false;
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// 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;
}
- sidetable_tryRetain*我們看下這個函數的源碼,所示
bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
// NO SPINLOCK HERE
// _objc_rootTryRetain() is called exclusively by _objc_loadWeak(),
// which already acquired the lock on our behalf.
// fixme can't do this efficiently with os_lock_handoff_s
// if (table.slock == 0) {
// _objc_fatal("Do not call -_tryRetain.");
// }
bool result = true;
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);
auto &refcnt = it.first->second;
if (it.second) {
// there was no entry
} else if (refcnt & SIDE_TABLE_DEALLOCATING) {
result = false;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt += SIDE_TABLE_RC_ONE;
}
return result;
}
這里調用了SideTable& table = SideTables()[this];這個SideTables函數,散列表是有多張的(真機8張表,模擬器 6張表)基本結構如下
template<>
void SideTable::unlockTwo<DontHaveOld, DoHaveNew>
(SideTable *, SideTable *lock2)
{
lock2->unlock();
}
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
// anonymous namespace
};
void SideTableLockAll() {
SideTables().lockAll();
}
void SideTableUnlockAll() {
SideTables().unlockAll();
}
void SideTableForceResetAll() {
SideTables().forceResetAll();
}
void SideTableDefineLockOrder() {
SideTables().defineLockOrder();
}
void SideTableLocksPrecedeLock(const void *newlock) {
SideTables().precedeLock(newlock);
}
void SideTableLocksSucceedLock(const void *oldlock) {
SideTables().succeedLock(oldlock);
}
void SideTableLocksPrecedeLocks(StripedMap<spinlock_t>& newlocks) {
int i = 0;
const void *newlock;
while ((newlock = newlocks.getLock(i++))) {
SideTables().precedeLock(newlock);
}
}
void SideTableLocksSucceedLocks(StripedMap<spinlock_t>& oldlocks) {
int i = 0;
const void *oldlock;
while ((oldlock = oldlocks.getLock(i++))) {
SideTables().succeedLock(oldlock);
}
}
為什么散列表是多張表,而不是一張表?
我們往下分析,我們看下SideTable是什么,找到其源碼。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
spinlock_t是把鎖
RefcountMap是引用表計數表,我們上篇文章提到的extra_rc就存在這里
weak_table_t是弱引用計數表,比如被__weak修飾的
如果在系統中共用一張表的話,就會大大影響性能(每次需要開鎖,解鎖),多張表的可以即時回收內存(一張表的數據都置空,這張表就可以回收)
sidetable_retain這個函數源碼如下
id
objc_object::sidetable_retain(bool locked)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
if (!locked) table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
這里獲取對象所在的表,并根據這張表獲取到對象所在的存儲空間,對其進行++操作。
sidetable_release源碼
uintptr_t
objc_object::sidetable_release(bool locked, bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
if (!locked) table.lock();
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
2 引用計數分析
NSObject *objc = [[NSObject alloc] init];
__weak typeof (id) weakObjc = objc;
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
我們經常會有這樣的寫法,通過__weak修飾變量,這個weakObjc會被加入弱引用表中。
我們在源碼中搜下retainCount找到以下源碼
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
if (bits.nonpointer) {
uintptr_t rc = bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
在這里拿到isa的bits,判斷如果是nonpointer,這里不會執行
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
因為extra_rc還未存滿,所以不會進入散列表,CFGetRetainCount這個函數打印1是因為在isa初始化的默認賦值1。
我們調整下代碼,如下
NSObject *objc = [[NSObject alloc] init];
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
__weak typeof (id) weakObjc = objc;
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(weakObjc)),objc);
這時候運行項目會出現什么情況,我們試下,如圖
打印結果1,1,2。
這是為什么?
我們加入弱引用表是不影響引用計數的,為什么打印weakObjc是2呢?我們要想知道這些,我們就得分析弱引用表。
3 弱引用表
我們要分析__weak的原理是什么,就要探索一下,在我們剛才的代碼上,先運行項目,如圖
這里發現進到源碼中了,在objc_initWeak這個函數中,因為我們是通過源碼編譯分析的,如果不是通過源碼分析,也可以通過匯編一步一步的分析流程。
我們在源碼中搜索objc_initWeak這個函數,如下
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
這也說明了,只要使__weak修飾,就會進入到objc_initWeak這個函數,也就是說__weak這個標示符與objc_initWeak有綁定關系, 我們可以在llvm中找到相應的綁定關系,如圖
這里可以看出__weak與OCL_Weak的對應關系,OCL_Weak就是objc_initWeak,如圖
GetFunctionClass會返回要調用的函數,如果是__weak類型就會調用objc_initWeak這個函數。
objc_initWeak調用storeWeak傳的是(objc_object)newObj,而objc_destroyWeak*釋放函數這里傳的是nil,
storeWeak<DontHaveOld, DoHaveNew, DontCrashIfDeallocating>
(location, (objc_object*)newObj);
location是weakSelf指針地址,(objc_object)newObj就是要綁定的對象,我們接著看下storeWeak*的源碼,如下
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!newObj->isTaggedPointerOrNil()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// This must be called without the locks held, as it can invoke
// arbitrary code. In particular, even if _setWeaklyReferenced
// is not implemented, resolveInstanceMethod: may be, and may
// call back into the weak reference machinery.
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
這段代碼
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
判斷在弱引用表查找,有沒有這個對象,因為第一次進來,肯定沒有,所以oldTable為nil。
接著
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
這里判斷能不能找到原來的對象,第一次進來,肯定是可進來的,我們調試下,如圖
然后再執行
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
如果是舊的,移除。
如果是新的就注冊進來:
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
如果新的對象進來,執行這里的代碼。
再接著
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!newObj->isTaggedPointerOrNil()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
weak_register_no_lock調用這個函數,
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (referent->isTaggedPointerOrNil()) return referent_id;
// ensure that the referenced object is viable
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
// Use lookUpImpOrForward so we can avoid the assert in
// class_getInstanceMethod, since we intentionally make this
// callout with the lock held.
auto allowsWeakReference = (BOOL(*)(objc_object *, SEL))
lookUpImpOrForwardTryCache((id)referent, @selector(allowsWeakReference),
referent->getIsa());
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
}
if (deallocating) {
if (deallocatingOptions == CrashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
}
// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
在這里
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
在這里添加,在weak_entry_for_referent如果找到referent(修飾的對象)存儲,直接調用append_referrer往里面添加。
如果沒有找到,weak_entry_t new_entry(referent, referrer);(對象的指針地址,對象)創建新的實體。
weak_grow_maybe擴容表的處理。
weak_entry_insert把實體添加到弱引用表中。
剛才我們有介紹了referent存放的對象的指針地址,那么實際是什么?我們來調試下,我們先看張圖
這時可以看出referent就是objc,所以referent就是我們的對象
但是referrer地址不同,我們來分析下weak_entry_t結構。
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
referent存了很多referrer。
如果是weak修飾的屬性, referent還是我們的對象,referrers存放我們的屬性。
weak大致流程
- weak是存在弱引用表中(散列表sideTable)
- 通過sideTable查找到weakTable弱引用表
- 創建一個weak_entry_t
- 把referent(對象的指針地址,如weakSelf)加入到weak_entry_t的數組inline_referrers
- 把weak_table擴容一下
-
把new_entry加入到weak_table中
函數流程 - objc_initWeak
- storeWeak
- weak_unregister_no_lock
- weak_register_no_lock ----->weak_entry_for_referent ----->weak_entry_for_referent -----> new_referrers[i]=entry->inline_referrer[i]
- weak_entry_t->inline_referrer[i]
- weak_grow_maybe
- weak_entry_insert
4 關于弱引用的引用計數
NSObject *objc = [[NSObject alloc] init];
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
__weak typeof (id) weakObjc = objc;
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(weakObjc)),objc);
我們看下執行結果,如圖
這里
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
輸出為1
這里
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)),objc);
輸出也是1
這里
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(weakObjc)),objc);
輸出為2。
這是為什么?
NSLog(@"%d----%@",(long)CFGetRetainCount((__bridge CFTypeRef)(weakObjc)),objc);
這里訪問的是weakObjc。
objc指向這塊內存,reatinCount=1,但是weakObjc是弱引用,不持有這個對象,為什么會是2呢,因為弱引用表把我們的這個對象復制了一份,加入引用計數表中,copy了這份對象跟復制的那份是指向同一塊內存,我們獲取weakObjc的引用計數,是獲取copy那份的引用計數
我們斷點,運行項目,如圖
調用objc_loadWeak函數,接著,我們在源碼搜下這個函數,如下
id
objc_loadWeak(id *location)
{
if (!*location) return nil;
return objc_autorelease(objc_loadWeakRetained(location));
}
這里調用了objc_loadWeakRetained這個函數,我們搜下objc_loadWeakRetained這個函數
id
objc_loadWeakRetained(id *location)
{
id obj;
id result;
Class cls;
SideTable *table;
retry:
// fixme std::atomic this load
obj = *location;
if (obj->isTaggedPointerOrNil()) return obj;
table = &SideTables()[obj];
table->lock();
if (*location != obj) {
table->unlock();
goto retry;
}
result = obj;
cls = obj->ISA();
if (! cls->hasCustomRR()) {
// Fast case. We know +initialize is complete because
// default-RR can never be set before then.
ASSERT(cls->isInitialized());
if (! obj->rootTryRetain()) {
result = nil;
}
}
else {
// Slow case. We must check for +initialize and call it outside
// the lock if necessary in order to avoid deadlocks.
// Use lookUpImpOrForward so we can avoid the assert in
// class_getInstanceMethod, since we intentionally make this
// callout with the lock held.
if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
lookUpImpOrForwardTryCache(obj, @selector(retainWeakReference), cls);
if ((IMP)tryRetain == _objc_msgForward) {
result = nil;
}
else if (! (*tryRetain)(obj, @selector(retainWeakReference))) {
result = nil;
}
}
else {
table->unlock();
class_initialize(cls, obj);
goto retry;
}
}
table->unlock();
return result;
}
我們在這個函數,再斷點進去,看下
如圖
這里的location就是weakSelf,這里就是獲取weakSelf所指向的內存空間。
經過調試發現,是在
if (! obj->rootTryRetain()) {
result = nil;
}
函數調用rootRetain這個函數,這里會執+1操作,如圖
這里就有一個疑問了,如果一直執行rootRetain會一直+1操作,但是發現不是,weakObjc的引用計數一直2 ,為什么?
因為id obj;是臨時變量,出了函數作用域就不存在了,因為會發送release消息。
總結
本篇文章, 我們分析散列表,計用計數,弱引用計數。本篇文章有遺漏的地方,煩請批評指正。