iOS 進階—— iOS記憶體管理
阿新 • • 發佈:2019-01-30
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 其實是在編譯階段自動幫開發者插入了管理記憶體的程式碼,而不是實時監控與回收記憶體。
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:
-
+ (id) alloc
-
{
-
return [self allocWithZone: NSDefaultMallocZone()];
-
}
-
+ (id) allocWithZone: (NSZone*)z
-
{
-
return NSAllocateObject (self, 0, z);
- }
-
struct obj_layout {
-
NSUInteger retained;
-
};
-
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
-
{
-
int size = 計算容納物件所需記憶體大小;
-
id new = NSZoneCalloc(zone, 1, size);
-
memset (new, 0, size);
-
new = (id)&((obj)new)[1];
- }
我們將上面的程式碼做簡化整理:
GNUstep/modules/core/base/Source/NSObject.m alloc 簡化版本:
-
struct obj_layout {
-
NSUInteger retained;
-
};
-
+ (id) alloc
-
{
-
int size = sizeof(struct obj_layout) + 物件大小;
-
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
-
return (id)(p+1)
-
return [self allocWithZone: NSDefaultMallocZone()];
- }
一個物件的表示便如下圖:
4.2 GNU – retain
GNUstep/modules/core/base/Source/NSObject.m retainCount:
-
- (NSUInteger) retainCount
-
{
-
return NSExtraRefCount(self) + 1;
-
}
-
inline NSUInteger
-
NSExtraRefCount(id anObject)
-
{
-
return ((obj_layout)anObject)[-1].retained;
- }
-
- (id) retain
-
{
-
NSIncrementExtraRefCount(self);
-
return self;
-
}
-
inline void
-
NSIncrementExtraRefCount(id anObject)
-
{
-
if (((obj)anObject)[-1].retained == UINT_MAX - 1)
-
[NSException raise: NSInternalInconsistencyException
-
format: @"NSIncrementExtraRefCount() asked to increment too far”];
-
((obj_layout)anObject)[-1].retained++;
- }
4.3 GNU – release
和 retain 相應的,release 方法做的就是 retain --。
GNUstep/modules/core/base/Source/NSObject.m release
-
- (oneway void) release
-
{
-
if (NSDecrementExtraRefCountWasZero(self))
-
{
-
[self dealloc];
-
}
-
}
-
BOOL
-
NSDecrementExtraRefCountWasZero(id anObject)
-
{
-
if (((obj)anObject)[-1].retained == 0)
-
{
-
return YES;
-
}
-
((obj)anObject)[-1].retained--;
-
return NO;
- }
dealloc 將會對物件進行釋放。
GNUstep/modules/core/base/Source/NSObject.m dealloc:
-
- (void) dealloc
-
{
-
NSDeallocateObject (self);
-
}
-
inline void
-
NSDeallocateObject(id anObject)
-
{
-
obj_layout o = &((obj_layout)anObject)[-1];
-
free(o);
- }
在 Xcode 中 設定 Debug -> Debug Workflow -> Always Show Disassenbly 開啟。這樣在打斷點後,可以看到更詳細的方法呼叫。
通過在 NSObject 類的 alloc 等方法上設定斷點追蹤可以看到幾個方法內部分別呼叫了:
retainCount
-
__CFdoExternRefOperation
- CFBasicHashGetCountOfKey
-
__CFdoExternRefOperation
- CFBasicHashAddValue
-
__CFdoExternRefOperation
- CFBasicHashRemoveValue
該方法從字首可以看到是包含在 Core Foundation,在 CFRuntime.c 中可以找到,做簡化後列出原始碼:
CFRuntime.c __CFDoExternRefOperation:
-
int __CFDoExternRefOperation(uintptr_t op, id obj) {
-
CFBasicHashRef table = 取得物件的散列表(obj);
-
int count;
-
switch (op) {
-
case OPERATION_retainCount:
-
count = CFBasicHashGetCountOfKey(table, obj);
-
return count;
-
break;
-
case OPERATION_retain:
-
count = CFBasicHashAddValue(table, obj);
-
return obj;
-
case OPERATION_release:
-
count = CFBasicHashRemoveValue(table, obj);
-
return 0 == count;
-
}
- }
從 BasicHash 這樣的方法名可以看出,其實引用計數表就是散列表。
key 為 hash(物件的地址) value 為 引用計數。
下圖是 Apple 和 GNU 的實現對比:
5 autorelease 和 autorelaesepool
在蘋果對於 NSAutoreleasePool 的文件中表示:
每個執行緒(包括主執行緒),都維護了一個管理 NSAutoreleasePool 的棧。當創先新的 Pool 時,他們會被新增到棧頂。當 Pool 被銷燬時,他們會被從棧中移除。
autorelease 的物件會被新增到當前執行緒的棧頂的 Pool 中。當 Pool 被銷燬,其中的物件也會被釋放。
當執行緒結束時,所有的 Pool 被銷燬釋放。
對 NSAutoreleasePool 類方法和 autorelease 方法打斷點,檢視其執行過程,可以看到呼叫了以下函式:
-
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
-
// 等同於 objc_autoreleasePoolPush
-
id obj = [[NSObject alloc] init];
-
[obj autorelease];
-
// 等同於 objc_autorelease(obj)
-
[NSAutoreleasePool showPools];
-
// 檢視 NSAutoreleasePool 狀況
-
[pool drain];
- // 等同於 objc_autoreleasePoolPop(pool)
-
objc[21536]: ##############
-
objc[21536]: AUTORELEASE POOLS for thread 0x10011e3c0
-
objc[21536]: 2 releases pending.
-
objc[21536]: [0x101802000] ................ PAGE (hot) (cold)
-
objc[21536]: [0x101802038] ################ POOL 0x101802038
-
objc[21536]: [0x101802040] 0x1003062e0 NSObject
-
objc[21536]: ##############
- Program ended with exit code: 0
-
objc4/NSObject.mm AutoreleasePoolPage
-
class AutoreleasePoolPage
-
{
-
static inline void *push()
-
{
-
生成或者持有 NSAutoreleasePool 類物件
-
}
-
static inline void pop(void *token)
-
{
-
廢棄 NSAutoreleasePool 類物件
-
releaseAll();
-
}
-
static inline id autorelease(id obj)
-
{
-
相當於 NSAutoreleasePool 類的 addObject 類方法
-
AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 例項;
-
}
-
id *add(id obj)
-
{
-
將物件追加到內部陣列
-
}
-
void releaseAll()
-
{
-
呼叫內部陣列中物件的 release 方法
-
}
-
};
-
void *
-
objc_autoreleasePoolPush(void)
-
{
-
if (UseGC) return nil;
-
return AutoreleasePoolPage::push();
-
}
-
void
-
objc_autoreleasePoolPop(void *ctxt)
-
{
-
if (UseGC) return;
-
AutoreleasePoolPage::pop(ctxt);
- }
thread 指標指向當前執行緒。
每個 AutoreleasePoolPage 物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址。
next 指標指向下一個 add 進來的 autorelease 的物件即將存放的位置。
一個 Page 的空間被佔滿時,會新建一個 AutoreleasePoolPage 物件,連線連結串列。
6 __unsafe_unretained
有時候我們除了 __weak 和 __strong 之外也會用到 __unsafe_unretained 這個修飾符,那麼我們對 __unsafe_unretained 瞭解多少?
__unsafe_unretained 是不安全的所有權修飾符,儘管 ARC 的記憶體管理是編譯器的工作,但附有 __unsafe_unretained 修飾符的變數不屬於編譯器的記憶體管理物件。賦值時即不獲得強引用也不獲得弱引用。
來執行一段程式碼:
-
id __unsafe_unretained obj1 = nil;
-
{
-
id __strong obj0 = [[NSObject alloc] init];
-
obj1 = obj0;
-
NSLog(@"A: %@", obj1);
-
}
- NSLog(@"B: %@", obj1);
-
2017-01-12 19:24:47.245220 __unsafe_unretained[55726:4408416] A:
-
2017-01-12 19:24:47.246670 __unsafe_unretained[55726:4408416] B:
- Program ended with exit code: 0
-
id __unsafe_unretained obj1 = nil;
-
{
-
// 自己生成並持有物件
-
id __strong obj0 = [[NSObject alloc] init];
-
// 因為 obj0 變數為強引用,
-
// 所以自己持有物件
-
obj1 = obj0;
-
// 雖然 obj0 變數賦值給 obj1
-
// 但是 obj1 變數既不持有物件的強引用,也不持有物件的弱引用
-
NSLog(@"A: %@", obj1);
-
// 輸出 obj1 變數所表示的物件
-
}
-
NSLog(@"B: %@", obj1);
-
// 輸出 obj1 變數所表示的物件
-
// obj1 變量表示的物件已經被廢棄
-
// 所以此時獲得的是懸垂指標
- // 錯誤訪問
在使用 __unsafe_unretained 修飾符時,賦值給附有 __strong 修飾符變數時,要確保物件確實存在
轉自解放號社群:http://bbs.jointforce.com/topic/26109