筆記-更深層次的瞭解iOS內存管理

在研究Hash表的過程當中,想看iOS當中有哪些場景應用,最爲你們所知的應該就是weak關鍵字的底層原理,利用網上的資料深究了一下,同時更進一步瞭解到了iOS內存管理方面的知識,因此但願本身可以保留這份記憶,就記錄一下。ios

Hash

筆記-數據結構之 Hash(OC的粗略實現)算法

Hash或者說散列表,它是一種基礎數據結構,這裏爲何會說到它,由於我感受理解了Hash對weak關鍵字底層的理解有很大的幫助。編程

Hash表是一種特殊的數據結構,它同數組、鏈表以及二叉樹等相比有很明顯的區別,可是它又是在數組和鏈表的基礎上演化而來。數組

Hash表的本質是一個數組,數組中每個元素稱爲一個箱子,箱子中存放元素。
存儲過程以下:安全

  • 根據key計算出它的哈希值h。
  • 假設箱子的個數爲n,那麼這個鍵值對應該放在第(h % n)個箱子中。
  • 若是該箱子中已經有了鍵值對,就使用方法解決衝突(這裏值說分離連接法解決衝突,還有一個方法是開放定址法)。

Hash表採用一個映射函數f:key->address將關鍵字映射到該記錄在表中存儲位置,從而想要查找該記錄時,能夠直接根據關鍵字和映射關係計算出該記錄在表中的存儲位置,一般狀況下,這種映射關係稱做Hash函數,而經過Hash函數和關鍵字計算出來的存儲位置(這裏的存儲位置只是表中的存儲位置,並非實際的物理地址)稱做Hash地址。bash

先看一個列子: 假如聯繫人信息採用Hash表存儲,當想要找到「lisi」的信息時,直接根據「lisi」和Hash函數計算出Hash地址便可。 由於咱們是用數組大小對哈希值進行取模,有可能不一樣的鍵值產生的索引值相同,這就是所謂的衝突。 markdown

顯然這裏「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向當前的AutoreleasePoolPageadd進一個哨兵對象,值爲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是所指對象的地址,Valueweak指針的地址(這個地址的值是所指對象的地址)數組。

weak的實現原理能夠歸納成三步:

  • 初始化時,runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。
  • 添加引用時,objc_initWeak函數會調用objc_storeWeak()函數,objc_storeWeak()的做用是更新指針指向,建立對應的弱引用表。
  • 釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取全部weak指針地址的數組,而後遍歷這個數組把其中的數據設爲nil,最後把這個entryweak表中刪除,最後清理對象的記錄。

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_tweak_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_tweak全局表):採用Hash表的方式把全部weak引用的對象,存儲全部引用weak對象。
  • weak_entry_tweak_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,最後把這個entryweak表中刪除,最後清理對象的記錄。

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

相關文章
相關標籤/搜索