筆記-更深層次的瞭解iOS記憶體管理

佐籩發表於2019-01-24

在研究Hash表的過程中,想看iOS當中有哪些場景應用,最為大家所知的應該就是weak關鍵字的底層原理,利用網上的資料深究了一下,同時更進一步瞭解到了iOS記憶體管理方面的知識,所以希望自己能夠保留這份記憶,就記錄一下。

Hash

筆記-資料結構之 Hash(OC的粗略實現)

Hash或者說雜湊表,它是一種基礎資料結構,這裡為什麼會說到它,因為我感覺理解了Hash對weak關鍵字底層的理解有很大的幫助。

Hash表是一種特殊的資料結構,它同陣列、連結串列以及二叉樹等相比有很明顯的區別,但是它又是在陣列和連結串列的基礎上演化而來。

Hash表的本質是一個陣列,陣列中每一個元素稱為一個箱子,箱子中存放元素。
儲存過程如下:

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

Hash表採用一個對映函式f:key->address將關鍵字對映到該記錄在表中儲存位置,從而想要查詢該記錄時,可以直接根據關鍵字和對映關係計算出該記錄在表中的儲存位置,通常情況下,這種對映關係稱作Hash函式,而通過Hash函式和關鍵字計算出來的儲存位置(這裡的儲存位置只是表中的儲存位置,並不是實際的實體地址)稱作Hash地址。

先看一個列子: 假如聯絡人資訊採用Hash表儲存,當想要找到“lisi”的資訊時,直接根據“lisi”和Hash函式計算出Hash地址即可。 因為我們是用陣列大小對雜湊值進行取模,有可能不同的鍵值產生的索引值相同,這就是所謂的衝突。

筆記-更深層次的瞭解iOS記憶體管理
顯然這裡“sizhang”元素和“zhangsi”元素產生了衝突,解決該衝突的方法就是改變資料結構,將陣列內的元素改變為一個連結串列,這樣就能容下足夠多的元素。

在使用分離連結法解決雜湊衝突時,每個箱子其實是一個連結串列,將屬於同一個箱子裡的元素儲存在一張線性表中,而每張表的表頭的序號即為計算得到的Hash地址,如下圖最左邊是陣列結構,陣列內的元素為連結串列結構。

筆記-更深層次的瞭解iOS記憶體管理

這裡的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++實現的類

筆記-更深層次的瞭解iOS記憶體管理

  • AutoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙連結串列的形式組合而成(分別對應結構中的parent指標和child指標)
  • AutoreleasePool是按執行緒一一對應的(結構中的thread指標指向當前執行緒)
  • AutoreleasePoolPage每個物件開闢一個虛擬記憶體一頁的大小,除了上面例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址
  • 上面的id *next指標作為遊標指向棧頂最新add進來的autorelease物件的下一個位置
  • 一個AutoreleasePoolPage的空間被佔滿時,會新建一個AutoreleasePoolPage物件,連線連結串列,後來的autorelease物件在新的page加入

所以,若當前執行緒中只有一個AutoreleasePoolPage物件,並記錄了很多autorelease物件地址時記憶體如下:

筆記-更深層次的瞭解iOS記憶體管理
圖中的情況,這一頁再加入一個autorelease物件就要滿了(也就是next指標馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page物件,與這一頁連結串列連結完成後,新page的next指標被初始化在棧底(begin的位置),然後繼續向棧頂新增新物件。

所以,向一個物件傳送- autorelease訊息,就是將這個物件加入到當前AutoreleasePoolPage的棧頂next指標指向的位置

每當執行一個objc_autoreleasePoolPush呼叫時,runtime向當前的AutoreleasePoolPageadd進一個哨兵物件,值為0(也就是nil),那麼page就變成了下面的樣子:

筆記-更深層次的瞭解iOS記憶體管理
objc_autoreleasePoolPush的返回值正式這個哨兵物件的地址,被objc_autoreleasePoolPop(哨兵物件)作為入參,

  • 根據傳入的哨兵物件地址找到哨兵物件所處的page
  • 在當前page中,將晚於哨兵物件插入的所有autorelease物件都傳送一次- release訊息,並向回移動next指標到正確位置
  • 從最新加入的物件一直向前清理,可以向前跨越若干個page,知道哨兵所在的page

剛才的objc_autoreleasePoolPop執行後,最終變成了下面樣子:

筆記-更深層次的瞭解iOS記憶體管理

關鍵字

__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做中轉,很有默契的免去了對返回值的記憶體管理。
關係圖如下:

筆記-更深層次的瞭解iOS記憶體管理

__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;
}
複製程式碼

這裡同樣引用一個比較直觀的初始化弱引用物件流程圖:

筆記-更深層次的瞭解iOS記憶體管理

總之根據以上對weak進行的儲存過程,可以通過下面流程圖幫助理解:

筆記-更深層次的瞭解iOS記憶體管理

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

相關文章