1. 程式人生 > >iOS 進階—— iOS記憶體管理

iOS 進階—— iOS記憶體管理

1 似乎每個人在學習 iOS 過程中都考慮過的問題

    alloc retain release delloc 做了什麼?
    autoreleasepool 是怎樣實現的?
    __unsafe_unretained 是什麼?
    Block 是怎樣實現的
    什麼時候會引起迴圈引用,什麼時候不會引起迴圈引用?

所以我將在本篇博文中詳細的從 ARC 解釋到 iOS 的記憶體管理,以及 Block 相關的原理、原始碼。

2 從 ARC 說起

說 iOS 的記憶體管理,就不得不從 ARC(Automatic Reference Counting / 自動引用計數) 說起, ARC 是 WWDC2011 和 iOS5 引入的變化。ARC 是 LLVM 3.0 編譯器的特性,用來自動管理記憶體。

與 Java 中 GC 不同,ARC 是編譯器特性,而不是基於執行時的,所以 ARC 其實是在編譯階段自動幫開發者插入了管理記憶體的程式碼,而不是實時監控與回收記憶體。 
1.jpg
 
ARC 的記憶體管理規則可以簡述為:

    每個物件都有一個『被引用計數』
    物件被持有,『被引用計數』+1
    物件被放棄持有,『被引用計數』-1
    『引用計數』=0,釋放物件

3 你需要知道

    包含 NSObject 類的 Foundation 框架並沒有公開
    Core Foundation 框架原始碼,以及通過 NSObject 進行記憶體管理的部分原始碼是公開的。
    GNUstep 是 Foundation 框架的互換框架

GNUstep 也是 GNU 計劃之一。將 Cocoa Objective-C 軟體庫以自由軟體方式重新實現

某種意義上,GNUstep 和 Foundation 框架的實現是相似的

通過 GNUstep 的原始碼來分析 Foundation 的記憶體管理

4 alloc retain release dealloc 的實現


4.1 GNU – alloc

檢視 GNUStep 中的 alloc 函式。

GNUstep/modules/core/base/Source/NSObject.m alloc:
  1.     + (id) alloc 
  2.     { 
  3.     return [self allocWithZone: NSDefaultMallocZone()]; 
  4.     }  
  5.     + (id) allocWithZone: (NSZone*)z 
  6.     { 
  7.     return NSAllocateObject (self, 0, z); 
  8.     }  
複製程式碼 GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:
  1.     struct obj_layout { 
  2.     NSUInteger retained; 
  3.     }; 
  4.     NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) 
  5.     { 
  6.     int size = 計算容納物件所需記憶體大小; 
  7.     id new = NSZoneCalloc(zone, 1, size); 
  8.     memset (new, 0, size); 
  9.     new = (id)&((obj)new)[1]; 
  10.     }  
複製程式碼 NSAllocateObject 函式通過呼叫 NSZoneCalloc 函式來分配存放物件所需的空間,之後將該記憶體空間置為 nil,最後返回作為物件而使用的指標。

我們將上面的程式碼做簡化整理:

GNUstep/modules/core/base/Source/NSObject.m alloc 簡化版本:
  1.     struct obj_layout { 
  2.     NSUInteger retained; 
  3.     }; 
  4.     + (id) alloc 
  5.     { 
  6.     int size = sizeof(struct obj_layout) + 物件大小; 
  7.     struct obj_layout *p = (struct obj_layout *)calloc(1, size); 
  8.     return (id)(p+1) 
  9.     return [self allocWithZone: NSDefaultMallocZone()]; 
  10.     }  
複製程式碼 alloc 類方法用 struct obj_layout 中的 retained 整數來儲存引用計數,並將其寫入物件的記憶體頭部,該物件記憶體塊全部置為 0 後返回。

一個物件的表示便如下圖:
2.png 
4.2 GNU – retain

GNUstep/modules/core/base/Source/NSObject.m retainCount:
  1.     - (NSUInteger) retainCount 
  2.     { 
  3.     return NSExtraRefCount(self) + 1; 
  4.     } 
  5.     inline NSUInteger 
  6.     NSExtraRefCount(id anObject) 
  7.     { 
  8.     return ((obj_layout)anObject)[-1].retained; 
  9.     }  
複製程式碼 GNUstep/modules/core/base/Source/NSObject.m retain:
  1.     - (id) retain 
  2.     { 
  3.     NSIncrementExtraRefCount(self); 
  4.     return self; 
  5.     } 
  6.     inline void 
  7.     NSIncrementExtraRefCount(id anObject) 
  8.     { 
  9.     if (((obj)anObject)[-1].retained == UINT_MAX - 1) 
  10.     [NSException raise: NSInternalInconsistencyException 
  11.     format: @"NSIncrementExtraRefCount() asked to increment too far”]; 
  12.     ((obj_layout)anObject)[-1].retained++; 
  13.     }  
複製程式碼 以上程式碼中, NSIncrementExtraRefCount 方法首先寫入了當 retained 變數超出最大值時發生異常的程式碼(因為 retained 是 NSUInteger 變數),然後進行 retain ++ 程式碼。

4.3 GNU – release

和 retain 相應的,release 方法做的就是 retain --。

GNUstep/modules/core/base/Source/NSObject.m release
  1.     - (oneway void) release 
  2.     { 
  3.     if (NSDecrementExtraRefCountWasZero(self)) 
  4.     { 
  5.     [self dealloc]; 
  6.     } 
  7.     } 
  8.     BOOL 
  9.     NSDecrementExtraRefCountWasZero(id anObject) 
  10.     { 
  11.     if (((obj)anObject)[-1].retained == 0) 
  12.     { 
  13.     return YES; 
  14.     } 
  15.     ((obj)anObject)[-1].retained--; 
  16.     return NO; 
  17.     }  
複製程式碼 4.4 GNU – dealloc

dealloc 將會對物件進行釋放。

GNUstep/modules/core/base/Source/NSObject.m dealloc:
  1.     - (void) dealloc 
  2.     { 
  3.     NSDeallocateObject (self); 
  4.     } 
  5.     inline void 
  6.     NSDeallocateObject(id anObject) 
  7.     { 
  8.     obj_layout o = &((obj_layout)anObject)[-1]; 
  9.     free(o); 
  10.     }  
複製程式碼 4.5 Apple 實現

在 Xcode 中 設定 Debug -> Debug Workflow -> Always Show Disassenbly 開啟。這樣在打斷點後,可以看到更詳細的方法呼叫。

通過在 NSObject 類的 alloc 等方法上設定斷點追蹤可以看到幾個方法內部分別呼叫了:

retainCount
  1.     __CFdoExternRefOperation 
  2.     CFBasicHashGetCountOfKey  
複製程式碼 retain
  1.     __CFdoExternRefOperation 
  2.     CFBasicHashAddValue  
複製程式碼 release
  1.     __CFdoExternRefOperation 
  2.     CFBasicHashRemoveValue  
複製程式碼 可以看到他們都呼叫了一個共同的 __CFdoExternRefOperation 方法。

該方法從字首可以看到是包含在 Core Foundation,在 CFRuntime.c 中可以找到,做簡化後列出原始碼:

CFRuntime.c __CFDoExternRefOperation:
  1.     int __CFDoExternRefOperation(uintptr_t op, id obj) { 
  2.     CFBasicHashRef table = 取得物件的散列表(obj); 
  3.     int count; 
  4.     switch (op) { 
  5.     case OPERATION_retainCount: 
  6.     count = CFBasicHashGetCountOfKey(table, obj); 
  7.     return count; 
  8.     break; 
  9.     case OPERATION_retain: 
  10.     count = CFBasicHashAddValue(table, obj); 
  11.     return obj; 
  12.     case OPERATION_release: 
  13.     count = CFBasicHashRemoveValue(table, obj); 
  14.     return 0 == count; 
  15.     } 
  16.     }  
複製程式碼 所以 __CFDoExternRefOperation 是針對不同的操作,進行具體的方法呼叫,如果 op 是 OPERATION_retain,就去掉用具體實現 retain 的方法。

從 BasicHash 這樣的方法名可以看出,其實引用計數表就是散列表。

key 為 hash(物件的地址) value 為 引用計數。

下圖是 Apple 和 GNU 的實現對比:
3.jpg 
5 autorelease 和 autorelaesepool

在蘋果對於 NSAutoreleasePool 的文件中表示:

每個執行緒(包括主執行緒),都維護了一個管理 NSAutoreleasePool 的棧。當創先新的 Pool 時,他們會被新增到棧頂。當 Pool 被銷燬時,他們會被從棧中移除。

autorelease 的物件會被新增到當前執行緒的棧頂的 Pool 中。當 Pool 被銷燬,其中的物件也會被釋放。

當執行緒結束時,所有的 Pool 被銷燬釋放。

對 NSAutoreleasePool 類方法和 autorelease 方法打斷點,檢視其執行過程,可以看到呼叫了以下函式:
  1.     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
  2.     // 等同於 objc_autoreleasePoolPush 
  3.     id obj = [[NSObject alloc] init]; 
  4.     [obj autorelease]; 
  5.     // 等同於 objc_autorelease(obj) 
  6.     [NSAutoreleasePool showPools]; 
  7.     // 檢視 NSAutoreleasePool 狀況 
  8.     [pool drain]; 
  9.     // 等同於 objc_autoreleasePoolPop(pool)  
複製程式碼 [NSAutoreleasePool showPools] 可以看到當前執行緒所有 pool 的情況:
  1.     objc[21536]: ############## 
  2.     objc[21536]: AUTORELEASE POOLS for thread 0x10011e3c0 
  3.     objc[21536]: 2 releases pending. 
  4.     objc[21536]: [0x101802000] ................ PAGE (hot) (cold) 
  5.     objc[21536]: [0x101802038] ################ POOL 0x101802038 
  6.     objc[21536]: [0x101802040] 0x1003062e0 NSObject 
  7.     objc[21536]: ############## 
  8.     Program ended with exit code: 0  
複製程式碼 在 objc4 中可以檢視到 AutoreleasePoolPage:
  1.     objc4/NSObject.mm AutoreleasePoolPage 
  2.     class AutoreleasePoolPage 
  3.     { 
  4.     static inline void *push() 
  5.     { 
  6.     生成或者持有 NSAutoreleasePool 類物件 
  7.     } 
  8.     static inline void pop(void *token) 
  9.     { 
  10.     廢棄 NSAutoreleasePool 類物件 
  11.     releaseAll(); 
  12.     } 
  13.     static inline id autorelease(id obj) 
  14.     { 
  15.     相當於 NSAutoreleasePool 類的 addObject 類方法 
  16.     AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 例項; 
  17.     } 
  18.     id *add(id obj) 
  19.     { 
  20.     將物件追加到內部陣列 
  21.     } 
  22.     void releaseAll() 
  23.     { 
  24.     呼叫內部陣列中物件的 release 方法 
  25.     } 
  26.     }; 
  27.     void * 
  28.     objc_autoreleasePoolPush(void) 
  29.     { 
  30.     if (UseGC) return nil; 
  31.     return AutoreleasePoolPage::push(); 
  32.     } 
  33.     void 
  34.     objc_autoreleasePoolPop(void *ctxt) 
  35.     { 
  36.     if (UseGC) return; 
  37.     AutoreleasePoolPage::pop(ctxt); 
  38.     }  
複製程式碼 AutoreleasePoolPage 以雙向連結串列的形式組合而成(分別對應結構中的 parent 指標和 child 指標)。

thread 指標指向當前執行緒。

每個 AutoreleasePoolPage 物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址。

next 指標指向下一個 add 進來的 autorelease 的物件即將存放的位置。

一個 Page 的空間被佔滿時,會新建一個 AutoreleasePoolPage 物件,連線連結串列。
5.jpg 
6 __unsafe_unretained

有時候我們除了 __weak 和 __strong 之外也會用到 __unsafe_unretained 這個修飾符,那麼我們對 __unsafe_unretained 瞭解多少?

__unsafe_unretained 是不安全的所有權修飾符,儘管 ARC 的記憶體管理是編譯器的工作,但附有 __unsafe_unretained 修飾符的變數不屬於編譯器的記憶體管理物件。賦值時即不獲得強引用也不獲得弱引用。

來執行一段程式碼:
  1.     id __unsafe_unretained obj1 = nil; 
  2.     { 
  3.     id __strong obj0 = [[NSObject alloc] init];  
  4.     obj1 = obj0;  
  5.     NSLog(@"A: %@", obj1); 
  6.     }  
  7.     NSLog(@"B: %@", obj1);  
複製程式碼 執行結果:
  1.     2017-01-12 19:24:47.245220 __unsafe_unretained[55726:4408416] A: 
  2.     2017-01-12 19:24:47.246670 __unsafe_unretained[55726:4408416] B: 
  3.     Program ended with exit code: 0  
複製程式碼 對程式碼進行詳細分析:
  1.     id __unsafe_unretained obj1 = nil; 
  2.     { 
  3.     // 自己生成並持有物件 
  4.     id __strong obj0 = [[NSObject alloc] init]; 
  5.     // 因為 obj0 變數為強引用, 
  6.     // 所以自己持有物件 
  7.     obj1 = obj0; 
  8.     // 雖然 obj0 變數賦值給 obj1 
  9.     // 但是 obj1 變數既不持有物件的強引用,也不持有物件的弱引用 
  10.     NSLog(@"A: %@", obj1); 
  11.     // 輸出 obj1 變數所表示的物件 
  12.     } 
  13.     NSLog(@"B: %@", obj1); 
  14.     // 輸出 obj1 變數所表示的物件 
  15.     // obj1 變量表示的物件已經被廢棄 
  16.     // 所以此時獲得的是懸垂指標 
  17.     // 錯誤訪問  
複製程式碼 所以,最後的 NSLog 只是碰巧正常執行,如果錯誤訪問,會造成 crash

在使用 __unsafe_unretained 修飾符時,賦值給附有 __strong 修飾符變數時,要確保物件確實存在

轉自解放號社群:http://bbs.jointforce.com/topic/26109