在研究Hash表的過程當中,想看iOS當中有哪些場景應用,最爲你們所知的應該就是weak關鍵字的底層原理,利用網上的資料深究了一下,同時更進一步瞭解到了iOS內存管理方面的知識,因此但願本身可以保留這份記憶,就記錄一下。ios
Hash或者說散列表,它是一種基礎數據結構,這裏爲何會說到它,由於我感受理解了Hash對weak關鍵字底層的理解有很大的幫助。編程
Hash表是一種特殊的數據結構,它同數組、鏈表以及二叉樹等相比有很明顯的區別,可是它又是在數組和鏈表的基礎上演化而來。數組
Hash表的本質是一個數組,數組中每個元素稱爲一個箱子,箱子中存放元素。
存儲過程以下:安全
Hash表採用一個映射函數f:key->address將關鍵字映射到該記錄在表中存儲位置,從而想要查找該記錄時,能夠直接根據關鍵字和映射關係計算出該記錄在表中的存儲位置,一般狀況下,這種映射關係稱做Hash函數,而經過Hash函數和關鍵字計算出來的存儲位置(這裏的存儲位置只是表中的存儲位置,並非實際的物理地址)稱做Hash地址。bash
先看一個列子: 假如聯繫人信息採用Hash表存儲,當想要找到「lisi」的信息時,直接根據「lisi」和Hash函數計算出Hash地址便可。 由於咱們是用數組大小對哈希值進行取模,有可能不一樣的鍵值產生的索引值相同,這就是所謂的衝突。 markdown
顯然這裏「sizhang」元素和「zhangsi」元素產生了衝突,解決該衝突的方法就是改變數據結構,將數組內的元素改變爲一個鏈表,這樣就能容下足夠多的元素。在使用分離連接法解決哈希衝突時,每一個箱子實際上是一個鏈表,將屬於同一個箱子裏的元素存儲在一張線性表中,而每張表的表頭的序號即爲計算獲得的Hash地址,以下圖最左邊是數組結構,數組內的元素爲鏈表結構。 網絡
這裏的Hash表咱們只作簡單的瞭解,想要詳細瞭解的請參考:
筆記-數據結構之 Hash(OC的粗略實現)
深刻理解哈希表
哈希算法詳解數據結構
其實不論ARC仍是MRC都遵循該方式,只是在ARC模式下這些工做被編譯器作了多線程
蘋果的實現:(這部份內容是根據 《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
做用是將對象放入自動釋放池中,當自從釋放池銷燬時對自動釋放池中的對象都進行一次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(哨兵對象)
做爲入參,
autorelease
對象都發送一次- release
消息,並向回移動next
指針到正確位置剛纔的objc_autoreleasePoolPop
執行後,最終變成了下面樣子:
__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
表示弱引用,弱引用不會影響對象的釋放,而當對象被釋放時,全部指向它的弱引用都會自動被置爲nil
,這樣能夠防止野指針。
id __weak obj = [[NSObject alloc] init];
複製代碼
根據咱們的瞭解,能夠知道obj
對象在生成以後立馬就會被釋放,主要緣由是由於__weak
修飾的指針沒有引發對象內部的引用計數發生變化。
__weak
的幾個使用場景:
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
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
做用須要和weak對比,它不會引發對象的內部引用計數的變化,可是,當其指向的對象被銷燬是__unsafe_unretained
修飾的指針不會置爲nil。是不安全的全部權修飾符,它不歸入ARC的內存管理。
將對象賦值給附有__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