在研究Hash表的過程中,想看iOS當中有哪些場景應用,最為大家所知的應該就是weak關鍵字的底層原理,利用網上的資料深究了一下,同時更進一步瞭解到了iOS記憶體管理方面的知識,所以希望自己能夠保留這份記憶,就記錄一下。
Hash
Hash或者說雜湊表,它是一種基礎資料結構,這裡為什麼會說到它,因為我感覺理解了Hash對weak關鍵字底層的理解有很大的幫助。
Hash表是一種特殊的資料結構,它同陣列、連結串列以及二叉樹等相比有很明顯的區別,但是它又是在陣列和連結串列的基礎上演化而來。
Hash表的本質是一個陣列,陣列中每一個元素稱為一個箱子,箱子中存放元素。
儲存過程如下:
- 根據key計算出它的雜湊值h。
- 假設箱子的個數為n,那麼這個鍵值對應該放在第(h % n)個箱子中。
- 如果該箱子中已經有了鍵值對,就使用方法解決衝突(這裡值說分離連結法解決衝突,還有一個方法是開放定址法)。
Hash表採用一個對映函式f:key->address將關鍵字對映到該記錄在表中儲存位置,從而想要查詢該記錄時,可以直接根據關鍵字和對映關係計算出該記錄在表中的儲存位置,通常情況下,這種對映關係稱作Hash函式,而通過Hash函式和關鍵字計算出來的儲存位置(這裡的儲存位置只是表中的儲存位置,並不是實際的實體地址)稱作Hash地址。
先看一個列子: 假如聯絡人資訊採用Hash表儲存,當想要找到“lisi”的資訊時,直接根據“lisi”和Hash函式計算出Hash地址即可。 因為我們是用陣列大小對雜湊值進行取模,有可能不同的鍵值產生的索引值相同,這就是所謂的衝突。
顯然這裡“sizhang”元素和“zhangsi”元素產生了衝突,解決該衝突的方法就是改變資料結構,將陣列內的元素改變為一個連結串列,這樣就能容下足夠多的元素。在使用分離連結法解決雜湊衝突時,每個箱子其實是一個連結串列,將屬於同一個箱子裡的元素儲存在一張線性表中,而每張表的表頭的序號即為計算得到的Hash地址,如下圖最左邊是陣列結構,陣列內的元素為連結串列結構。
這裡的Hash表我們只做簡單的瞭解,想要詳細瞭解的請參考:
筆記-資料結構之 Hash(OC的粗略實現)
深入理解雜湊表
雜湊演算法詳解
記憶體管理的思考
ARC的核心思想:
- 自己生成的物件,自己持有
- 非自己生成的物件,自己也可以持有
- 自己持有的物件不需要時,需要對其進行釋放
- 非自己持有的物件無法釋放
其實不論ARC還是MRC都遵循該方式,只是在ARC模式下這些工作被編譯器做了
引用計數
retain、release、etainCount
蘋果的實現:(這部分內容是根據 《Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理》 來的)
- retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
複製程式碼
- retain
__CFDoExternRefOperation
CFBasicHashAddValue
複製程式碼
- release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue返回0時,-release呼叫dealloc)
複製程式碼
各個方法都通過同一個呼叫來__CFDoExternRefOperation
函式,呼叫來一系列名稱相似的函式。如這些函式名的字首“CF”所示,它們包含於Core Foundation
框架原始碼中,即是CFRuntime.c
的__CFDoExternRefOperation
函式。
__CFDoExternRefOperation
函式按retainCount/retain/release
操作進行分發,呼叫不同的函式,NSObject類的retainCount/retain/release
例項方法也許如下面程式碼所示:
- (NSUInteger)retainCount {
return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount,self);
}
- (id)retain {
return (id)__CFDoExternRefOperation(OPERATION_retain,self);
}
- (void)release {
return __CFDoExternRefOperation(OPERATION_release,self);
}
複製程式碼
int __CFDoExternRefOperation(uintptr_r op,id obj) {
CFBasicHashRef table = 取得物件對應的雜湊表(obj);
int count;
switch(op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table,obj);
return count;
case OPERATION_retain:
CFBasicHashAddValue(table,obj);
return obj;
case OPERATION_release:
count = CFBasicHashRemoveValue(table,obj):
return 0 == count;
}
}
複製程式碼
從上面程式碼可以看出,蘋果大概就是採用雜湊表(引用計數表)來管理引用計數,當我們在呼叫retain、retainCount、release
時,先呼叫_CFDoExternRefOperation()
從而獲取到引用計數表的記憶體地址以及本物件的記憶體地址,然後根據物件的記憶體地址在表中查詢獲取到引用計數值。
若是retain
則加1,若是retainCount
就直接返回值,若是release
則減1。(在CFBasechashRemoveValue
中將引用計數減少到0時會呼叫dealloc
廢棄物件)
Autorelease
作用: autorelease
作用是將物件放入自動釋放池中,當自從釋放池銷燬時對自動釋放池中的物件都進行一次release操作。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
複製程式碼
原理: ARC下,使用@autoreleasepool{}
來使用一個AutoreleasePool
,隨後編譯器會改成下面的樣子:
void *context = objc_autoreleasePoolPush();
// 執行的程式碼
objc_autoreleasePoolPop(context);
複製程式碼
而這兩個函式都是對AutoreleasePoolPage
的簡單的封裝,所以自動釋放機制的核心就在於這個類。
AutoreleasePoolPage
是一個C++實現的類
AutoreleasePool
並沒有單獨的結構,而是由若干個AutoreleasePoolPage
以雙連結串列的形式組合而成(分別對應結構中的parent
指標和child
指標)AutoreleasePool
是按執行緒一一對應的(結構中的thread
指標指向當前執行緒)AutoreleasePoolPage
每個物件開闢一個虛擬記憶體一頁的大小,除了上面例項變數所佔空間,剩下的空間全部用來儲存autorelease
物件的地址- 上面的
id *next
指標作為遊標指向棧頂最新add進來的autorelease
物件的下一個位置 - 一個
AutoreleasePoolPage
的空間被佔滿時,會新建一個AutoreleasePoolPage
物件,連線連結串列,後來的autorelease
物件在新的page加入
所以,若當前執行緒中只有一個AutoreleasePoolPage
物件,並記錄了很多autorelease
物件地址時記憶體如下:
autorelease
物件就要滿了(也就是next
指標馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page物件,與這一頁連結串列連結完成後,新page的next
指標被初始化在棧底(begin
的位置),然後繼續向棧頂新增新物件。
所以,向一個物件傳送- autorelease
訊息,就是將這個物件加入到當前AutoreleasePoolPage
的棧頂next
指標指向的位置
每當執行一個objc_autoreleasePoolPush
呼叫時,runtime
向當前的AutoreleasePoolPage
中add
進一個哨兵物件
,值為0(也就是nil
),那麼page就變成了下面的樣子:
objc_autoreleasePoolPush
的返回值正式這個哨兵物件的地址,被objc_autoreleasePoolPop(哨兵物件)
作為入參,
- 根據傳入的哨兵物件地址找到哨兵物件所處的page
- 在當前page中,將晚於哨兵物件插入的所有
autorelease
物件都傳送一次- release
訊息,並向回移動next
指標到正確位置 - 從最新加入的物件一直向前清理,可以向前跨越若干個page,知道哨兵所在的page
剛才的objc_autoreleasePoolPop
執行後,最終變成了下面樣子:
關鍵字
__strong
__strong
表示強引用,指向並持有該物件。該物件只要引用計數不為0,就不會被銷燬。如果在宣告引用時,不加修飾符,那麼引用將預設為強引用。
- 物件通過
alloc、new、copy、mutableCopy
來分配記憶體的
id __strong obj = [[NSObject alloc] init];
複製程式碼
編譯器會轉換成下面程式碼:
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
// ...
objc_release(obj);
複製程式碼
當使用alloc、new、copy、mutableCopy
進行物件記憶體分配時,強指標直接指向一個引用計數為1的物件
- 物件不是自身生成,但是自身持有
id __strong obj = [NSMutableArray array];
複製程式碼
在這種情況下,obj
也指向一個引用計數為1的物件記憶體。編譯器會轉換成下面程式碼:
id obj = objc_msgSend(NSMutableArray, @selector(array));
//替代我們呼叫retain方法,是obj持有該物件
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj);
複製程式碼
從而使得obj指向了一個引用計數為1的物件,不過,
objc_retainAutoreleaseReturnValue
有一個成對的函式objc_autoreleaseReturnValue
,這兩個函式可以用於最優化程式的執行,程式碼如下:
+ (id)array {
return [[NSMutableArray alloc] init];
}
複製程式碼
編譯器轉換如下:
+ (id)array {
id obj = objc_msgSend(NSMutableArray,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 代替我們呼叫autorelease方法
return objc_autoreleaseReturnValue(obj);
}
複製程式碼
其實autorelease
這個開銷不小,runtime
機制解決了這個問題。
優化
Thread Local Storage(TLS)
執行緒區域性儲存,目的很簡單,將一塊記憶體作為某個執行緒專有的儲存,以key-value
的形式進行讀寫,比如在非arm架構下,使用pthread
提供的方法實現:
void *pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t, const void *);
複製程式碼
在返回值身上呼叫objc_autoreleaseReturnValue
方法時,runtime
將這個返回值object
儲存在TLS
中,然後直接返回這個object
(不呼叫autorelease
),同時,在外部接收這個返回值的objc_retainAutoreleaseReturnValue
裡,發現TLS
中正好存在這個物件,那麼直接返回這個object
(不呼叫retain
)。
於是乎,呼叫方和被呼叫利用TLS
做中轉,很有默契的免去了對返回值的記憶體管理。
關係圖如下:
__weak
__weak
表示弱引用,弱引用不會影響物件的釋放,而當物件被釋放時,所有指向它的弱引用都會自動被置為nil
,這樣可以防止野指標。
id __weak obj = [[NSObject alloc] init];
複製程式碼
根據我們的瞭解,可以知道obj
物件在生成之後立馬就會被釋放,主要原因是因為__weak
修飾的指標沒有引起物件內部的引用計數發生變化。
__weak
的幾個使用場景:
- 在Delegate關係中防止迴圈引用
- 在Block中防止迴圈引用
- 用來修飾指向有Interface Builder建立的控制元件
weak實現原理的概括:
Runtime
維護了一個weak
表,用於儲存指向某個物件的所有weak
指標。weak
表其實是一個Hash(雜湊)表(這就是為什麼在本文開始我要簡單介紹一下Hash表的原因),Key
是所指物件的地址,Value
是weak
指標的地址(這個地址的值是所指物件的地址)陣列。
weak
的實現原理可以概括成三步:
- 初始化時,
runtime
會呼叫objc_initWeak
函式,初始化一個新的weak
指標指向物件的地址。 - 新增引用時,
objc_initWeak
函式會呼叫objc_storeWeak()
函式,objc_storeWeak()
的作用是更新指標指向,建立對應的弱引用表。 - 釋放時,呼叫
clearDeallocating
函式。clearDeallocating
函式首先根據物件地址獲取所有weak
指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil
,最後把這個entry
從weak
表中刪除,最後清理物件的記錄。
weak表
weak
表是一個弱引用表,實現為一個weak_table
結構體
struct weak_table_t {
weak_entry_t *weak_entries; // 儲存來所有指向指定物件的weak指標 weak_entries的物件
size_t num_entries; // weak物件的儲存空間
uintptr_t mask; // 參與判斷引用計數輔助量
uintptr_t max_hash_displacement;// hash key 最大偏移值
};
複製程式碼
這是一個全域性弱引用Hash表。使用不定型別物件的地址作為key
,用weak_entry_t
型別結構體物件作為value
,其中的weak_entries
成員,從字面意思上看,即為弱引用表的入口。
weak
全域性表中的儲存weak
定義的物件的表結構weak_entry_t
,weak_entry_t
是儲存在弱引用表中的一個內部結構體,它負責維護和儲存指向一個物件的所有弱引用Hash表。定義如下:
typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
DisguisedPtr<objc_object> referent; //範型
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
}
};
複製程式碼
即:
weak_table_t
(weak
全域性表):採用Hash表的方式把所有weak
引用的物件,儲存所有引用weak
物件。weak_entry_t
(weak_table_t
表中Hash表的value
值,weak
物件體):用於記錄Hash表中weak
物件。objc_objct
(weak_entry_t
物件中的範型物件,用於標記物件weak
物件):用於標示weak
引用物件。
下面詳細看下weak
底層實現原理:
id __weak obj = [[NSObject alloc] init];
複製程式碼
編譯器轉換後程式碼如下:
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp,@selector(init));
objc_initWeak(&obj,tmp);
objc_release(tmp);
objc_destroyWeak(&obj);
複製程式碼
對於objc_initWeak()
的實現:
id objc_initWeak(id *location, id newObj) {
// 檢視物件例項是否有效,無效物件直接導致指標釋放
if (!newObj) {
*location = nil;
return nil;
}
// 儲存weak物件
return storeWeak(location, newObj);
}
複製程式碼
儲存weak
物件的方法:
/**
* This function stores a new value into a __weak variable. It would
* be used anywhere a __weak variable is the target of an assignment.
*
* @param location The address of the weak pointer itself
* @param newObj The new object this weak ptr should now point to
*
* @return \e newObj
*/
id
objc_storeWeak(id *location, id newObj)
{
// 更新弱引用指標的指向
id oldObj;
SideTable *oldTable;
SideTable *newTable;
spinlock_t *lock1;
#if SIDE_TABLE_STRIPE > 1
spinlock_t *lock2;
#endif
// 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:
// 更改指標,獲得以oldObj為索引所儲存的值地址
oldObj = *location;
oldTable = SideTable::tableForPointer(oldObj);
// 更改新值指標,獲得以newObj為索引所儲存的值地址
newTable = SideTable::tableForPointer(newObj);
// 加鎖操作,防止多執行緒中競爭衝突
lock1 = &newTable->slock;
#if SIDE_TABLE_STRIPE > 1
lock2 = &oldTable->slock;
if (lock1 > lock2) {
spinlock_t *temp = lock1;
lock1 = lock2;
lock2 = temp;
}
if (lock1 != lock2) spinlock_lock(lock2);
#endif
spinlock_lock(lock1);
if (*location != oldObj) {
spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
if (lock1 != lock2) spinlock_unlock(lock2);
#endif
goto retry;
}
// 舊物件解除註冊操作
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
// 新物件新增註冊操作
newObj = weak_register_no_lock(&newTable->weak_table, newObj, location);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
// 弱引用位初始化操作
// 引用計數那張雜湊表的weak引用物件的引用計數中標識為weak的引用
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
// 前面不要設定location物件,這裡需要更改指標指向
*location = newObj;
spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
if (lock1 != lock2) spinlock_unlock(lock2);
#endif
return newObj;
}
複製程式碼
這裡同樣引用一個比較直觀的初始化弱引用物件流程圖:
總之根據以上對weak進行的儲存過程,可以通過下面流程圖幫助理解:
weak釋放為nil的過程
釋放物件基本流程如下:
- 呼叫
objc_release
- 因為物件的引用計數為0,所以執行
dealloc
- 在
dealloc
中,呼叫來_objc_rootDealloc
函式 - 在
_objc_rootDealloc
中,呼叫來object_dispose
函式 - 呼叫
objc_destructInstance
- 最後呼叫
objc_clear_deallocating
clearDeallocating
函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil
,最後把這個entry
從weak
表中刪除,最後清理物件的記錄。
void objc_clear_deallocating(id obj) {
assert(obj);
assert(!UseGC);
if (obj->isTaggedPointer()) return;
obj->clearDeallocating();
}
//執行 clearDeallocating方法
inline void objc_object::clearDeallocating() {
sidetable_clearDeallocating();
}
// 執行sidetable_clearDeallocating,找到weak表中的value值
void objc_object::sidetable_clearDeallocating() {
SideTable *table = SideTable::tableForPointer(this);
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
spinlock_lock(&table->slock);
RefcountMap::iterator it = table->refcnts.find(this);
if (it != table->refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table->weak_table, (id)this);
}
table->refcnts.erase(it);
}
spinlock_unlock(&table->slock);
}
複製程式碼
最終通過呼叫weak_clear_no_lock
方法,將weak
指標置空,函式實現如下:
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
// XXX should not happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
複製程式碼
objc_clear_deallocating
函式的操作如下:
- 從
weak
表中獲取廢棄物件的地址為鍵值的記錄 - 將包含在記錄中的所有附有
weak
修飾符變數的地址,置為nil
- 將
weak
表中該記錄刪除 - 從引用計數表中刪除廢棄物件的地址為鍵值的記錄
說了這麼多,還是為了說明一開始說的那句話:
Runtime
維護了一個weak表,用於儲存指向某個物件的所有weak指標。weak表其實是一個Hash(雜湊)表,Key是所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件的地址)陣列。
__unsafe_unretained
__unsafe_unretained
作用需要和weak對比,它不會引起物件的內部引用計數的變化,但是,當其指向的物件被銷燬是__unsafe_unretained
修飾的指標不會置為nil。是不安全的所有權修飾符,它不納入ARC的記憶體管理。
__autoreleasing
將物件賦值給附有__autoreleasing
修飾符的變數等同於MRC時呼叫物件的autorelease
方法。
@autoeleasepool {
// 如果看了上面__strong的原理,就知道實際上物件已經註冊到自動釋放池裡面了
id __autoreleasing obj = [[NSObject alloc] init];
}
複製程式碼
編譯器轉換如下程式碼:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
複製程式碼
編譯器轉換上述程式碼如下:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
複製程式碼
上面兩種方式,雖然第二種持有物件的方法從alloc
方法變為了objc_retainAutoreleasedReturnValue
函式,都是通過objc_autorelease
,註冊到autoreleasePool
中。
篇幅太長了,很多底層上面的東西,網上都有相關的資料,以前看不是很懂,現在回過頭來細細研讀,感覺還是能理解的,所以參考了網路上的資料整理出來了,增加自己的印象,也希望我的理解能夠幫助到小夥伴們,如有錯誤,希望指出,共同進步,謝謝
參考資料:
《Objective-C高階程式設計 iOS於OS X多執行緒和記憶體管理》
iOS 底層解析weak的實現原理(包含weak物件的初始化,引用,釋放的分析
黑幕後的Autorelease