iOS 進(jìn)階—— iOS內(nèi)存管理
1 似乎每個(gè)人在學(xué)習(xí) iOS 過程中都考慮過的問題
- alloc retain release delloc 做了什么?
- autoreleasepool 是怎樣實(shí)現(xiàn)的?
- __unsafe_unretained 是什么?
- Block 是怎樣實(shí)現(xiàn)的
- 什么時(shí)候會(huì)引起循環(huán)引用,什么時(shí)候不會(huì)引起循環(huán)引用?
所以我將在本篇博文中詳細(xì)的從 ARC 解釋到 iOS 的內(nèi)存管理,以及 Block 相關(guān)的原理、源碼。
2 從 ARC 說起
說 iOS 的內(nèi)存管理,就不得不從 ARC(Automatic Reference Counting / 自動(dòng)引用計(jì)數(shù)) 說起, ARC 是 WWDC2011 和 iOS5 引入的變化。ARC 是 LLVM 3.0 編譯器的特性,用來自動(dòng)管理內(nèi)存。
與 Java 中 GC 不同,ARC 是編譯器特性,而不是基于運(yùn)行時(shí)的,所以 ARC 其實(shí)是在編譯階段自動(dòng)幫開發(fā)者插入了管理內(nèi)存的代碼,而不是實(shí)時(shí)監(jiān)控與回收內(nèi)存。
ARC 的內(nèi)存管理規(guī)則可以簡(jiǎn)述為:
- 每個(gè)對(duì)象都有一個(gè)『被引用計(jì)數(shù)』
- 對(duì)象被持有,『被引用計(jì)數(shù)』+1
- 對(duì)象被放棄持有,『被引用計(jì)數(shù)』-1
- 『引用計(jì)數(shù)』=0,釋放對(duì)象
3 你需要知道
- 包含 NSObject 類的 Foundation 框架并沒有公開
- Core Foundation 框架源代碼,以及通過 NSObject 進(jìn)行內(nèi)存管理的部分源代碼是公開的。
- GNUstep 是 Foundation 框架的互換框架
GNUstep 也是 GNU 計(jì)劃之一。將 Cocoa Objective-C 軟件庫(kù)以自由軟件方式重新實(shí)現(xiàn)
某種意義上,GNUstep 和 Foundation 框架的實(shí)現(xiàn)是相似的
通過 GNUstep 的源碼來分析 Foundation 的內(nèi)存管理
4 alloc retain release dealloc 的實(shí)現(xiàn)
4.1 GNU – alloc
查看 GNUStep 中的 alloc 函數(shù)。
GNUstep/modules/core/base/Source/NSObject.m alloc:
- + (id) alloc
- {
- return [self allocWithZone: NSDefaultMallocZone()];
- }
- + (id) allocWithZone: (NSZone*)z
- {
- return NSAllocateObject (self, 0, z);
- }
GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:
- struct obj_layout {
- NSUInteger retained;
- };
- NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
- {
- int size = 計(jì)算容納對(duì)象所需內(nèi)存大小;
- id new = NSZoneCalloc(zone, 1, size);
- memset (new, 0, size);
- new = (id)&((obj)new)[1];
- }
NSAllocateObject 函數(shù)通過調(diào)用 NSZoneCalloc 函數(shù)來分配存放對(duì)象所需的空間,之后將該內(nèi)存空間置為 nil,***返回作為對(duì)象而使用的指針。
我們將上面的代碼做簡(jiǎn)化整理:
GNUstep/modules/core/base/Source/NSObject.m alloc 簡(jiǎn)化版本:
- struct obj_layout {
- NSUInteger retained;
- };
- + (id) alloc
- {
- int size = sizeof(struct obj_layout) + 對(duì)象大小;
- struct obj_layout *p = (struct obj_layout *)calloc(1, size);
- return (id)(p+1)
- return [self allocWithZone: NSDefaultMallocZone()];
- }
alloc 類方法用 struct obj_layout 中的 retained 整數(shù)來保存引用計(jì)數(shù),并將其寫入對(duì)象的內(nèi)存頭部,該對(duì)象內(nèi)存塊全部置為 0 后返回。
一個(gè)對(duì)象的表示便如下圖:
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;
- }
GNUstep/modules/core/base/Source/NSObject.m retain:
- - (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++;
- }
以上代碼中, NSIncrementExtraRefCount 方法首先寫入了當(dāng) retained 變量超出***值時(shí)發(fā)生異常的代碼(因?yàn)?retained 是 NSUInteger 變量),然后進(jìn)行 retain ++ 代碼。
4.3 GNU – release
和 retain 相應(yīng)的,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;
- }
4.4 GNU – dealloc
dealloc 將會(huì)對(duì)對(duì)象進(jìn)行釋放。
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);
- }
4.***pple 實(shí)現(xiàn)
在 Xcode 中 設(shè)置 Debug -> Debug Workflow -> Always Show Disassenbly 打開。這樣在打斷點(diǎn)后,可以看到更詳細(xì)的方法調(diào)用。
通過在 NSObject 類的 alloc 等方法上設(shè)置斷點(diǎn)追蹤可以看到幾個(gè)方法內(nèi)部分別調(diào)用了:
retainCount
- __CFdoExternRefOperation
- CFBasicHashGetCountOfKey
retain
- __CFdoExternRefOperation
- CFBasicHashAddValue
release
- __CFdoExternRefOperation
- CFBasicHashRemoveValue
可以看到他們都調(diào)用了一個(gè)共同的 __CFdoExternRefOperation 方法。
該方法從前綴可以看到是包含在 Core Foundation,在 CFRuntime.c 中可以找到,做簡(jiǎn)化后列出源碼:
CFRuntime.c __CFDoExternRefOperation:
- int __CFDoExternRefOperation(uintptr_t op, id obj) {
- CFBasicHashRef table = 取得對(duì)象的散列表(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;
- }
- }
所以 __CFDoExternRefOperation 是針對(duì)不同的操作,進(jìn)行具體的方法調(diào)用,如果 op 是 OPERATION_retain,就去掉用具體實(shí)現(xiàn) retain 的方法。
從 BasicHash 這樣的方法名可以看出,其實(shí)引用計(jì)數(shù)表就是散列表。
key 為 hash(對(duì)象的地址) value 為 引用計(jì)數(shù)。
下圖是 Apple 和 GNU 的實(shí)現(xiàn)對(duì)比:
5 autorelease 和 autorelaesepool
在蘋果對(duì)于 NSAutoreleasePool 的文檔中表示:
每個(gè)線程(包括主線程),都維護(hù)了一個(gè)管理 NSAutoreleasePool 的棧。當(dāng)創(chuàng)先新的 Pool 時(shí),他們會(huì)被添加到棧頂。當(dāng) Pool 被銷毀時(shí),他們會(huì)被從棧中移除。
autorelease 的對(duì)象會(huì)被添加到當(dāng)前線程的棧頂?shù)?Pool 中。當(dāng) Pool 被銷毀,其中的對(duì)象也會(huì)被釋放。
當(dāng)線程結(jié)束時(shí),所有的 Pool 被銷毀釋放。
對(duì) NSAutoreleasePool 類方法和 autorelease 方法打斷點(diǎn),查看其運(yùn)行過程,可以看到調(diào)用了以下函數(shù):
- 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)
[NSAutoreleasePool showPools] 可以看到當(dāng)前線程所有 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 中可以查看到 AutoreleasePoolPage:
- objc4/NSObject.mm AutoreleasePoolPage
- class AutoreleasePoolPage
- {
- static inline void *push()
- {
- 生成或者持有 NSAutoreleasePool 類對(duì)象
- }
- static inline void pop(void *token)
- {
- 廢棄 NSAutoreleasePool 類對(duì)象
- releaseAll();
- }
- static inline id autorelease(id obj)
- {
- 相當(dāng)于 NSAutoreleasePool 類的 addObject 類方法
- AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 實(shí)例;
- }
- id *add(id obj)
- {
- 將對(duì)象追加到內(nèi)部數(shù)組
- }
- void releaseAll()
- {
- 調(diào)用內(nèi)部數(shù)組中對(duì)象的 release 方法
- }
- };
- void *
- objc_autoreleasePoolPush(void)
- {
- if (UseGC) return nil;
- return AutoreleasePoolPage::push();
- }
- void
- objc_autoreleasePoolPop(void *ctxt)
- {
- if (UseGC) return;
- AutoreleasePoolPage::pop(ctxt);
- }
AutoreleasePoolPage 以雙向鏈表的形式組合而成(分別對(duì)應(yīng)結(jié)構(gòu)中的 parent 指針和 child 指針)。
thread 指針指向當(dāng)前線程。
每個(gè) AutoreleasePoolPage 對(duì)象會(huì)開辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁的大小),除了上面的實(shí)例變量所占空間,剩下的空間全部用來儲(chǔ)存autorelease對(duì)象的地址。
next 指針指向下一個(gè) add 進(jìn)來的 autorelease 的對(duì)象即將存放的位置。
一個(gè) Page 的空間被占滿時(shí),會(huì)新建一個(gè) AutoreleasePoolPage 對(duì)象,連接鏈表。
6 __unsafe_unretained
有時(shí)候我們除了 __weak 和 __strong 之外也會(huì)用到 __unsafe_unretained 這個(gè)修飾符,那么我們對(duì) __unsafe_unretained 了解多少?
__unsafe_unretained 是不安全的所有權(quán)修飾符,盡管 ARC 的內(nèi)存管理是編譯器的工作,但附有 __unsafe_unretained 修飾符的變量不屬于編譯器的內(nèi)存管理對(duì)象。賦值時(shí)即不獲得強(qiáng)引用也不獲得弱引用。
來運(yùn)行一段代碼:
- id __unsafe_unretained obj1 = nil;
- {
- id __strong obj0 = [[NSObject alloc] init];
- obj1 = obj0;
- NSLog(@"A: %@", obj1);
- }
- NSLog(@"B: %@", obj1);
運(yùn)行結(jié)果:
- 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
對(duì)代碼進(jìn)行詳細(xì)分析:
- id __unsafe_unretained obj1 = nil;
- {
- // 自己生成并持有對(duì)象
- id __strong obj0 = [[NSObject alloc] init];
- // 因?yàn)?nbsp;obj0 變量為強(qiáng)引用,
- // 所以自己持有對(duì)象
- obj1 = obj0;
- // 雖然 obj0 變量賦值給 obj1
- // 但是 obj1 變量既不持有對(duì)象的強(qiáng)引用,也不持有對(duì)象的弱引用
- NSLog(@"A: %@", obj1);
- // 輸出 obj1 變量所表示的對(duì)象
- }
- NSLog(@"B: %@", obj1);
- // 輸出 obj1 變量所表示的對(duì)象
- // obj1 變量表示的對(duì)象已經(jīng)被廢棄
- // 所以此時(shí)獲得的是懸垂指針
- // 錯(cuò)誤訪問
所以,***的 NSLog 只是碰巧正常運(yùn)行,如果錯(cuò)誤訪問,會(huì)造成 crash
在使用 __unsafe_unretained 修飾符時(shí),賦值給附有 __strong 修飾符變量時(shí),要確保對(duì)象確實(shí)存在