《Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理》是iOS開發(fā)中一本經(jīng)典書籍,書中有關(guān)ARC、Block、GCD的梳理是iOS開發(fā)進階路上必不可少的知識儲備。筆者讀完此書后為了加強理解,特以筆記記之。本文為開篇,圍繞ARC談起Objective-C中的內(nèi)存管理。

鑒于本書翻譯自日文原版且翻譯偏向書面,筆者希望采用通俗的語言記錄,文章結(jié)構(gòu)略有調(diào)整。
本文首發(fā)于Rachal's blog。
內(nèi)存管理
ARC(自動引用計數(shù))是iOS5、macOS10.7引入的內(nèi)存管理技術(shù),為了循序漸進的方式了解這項技術(shù),本書先從ARC無效的環(huán)境說起,也就是常指的MRC(手動引用計數(shù))環(huán)境。
本書開篇沒有直接提及引用計數(shù)的概念,而是以辦公室開燈關(guān)燈的例子引出內(nèi)存管理的思考方式。作者認為理解內(nèi)存管理時把注意力落在“生成”、“持有”、“釋放”等管理操作上更為客觀。
內(nèi)存管理的思考方式
- 自己生成的對象,自己所持有。
- 非自己生成的對象,自己也能持有。
- 不再需要自己持有的對象時釋放。
- 非自己持有的對象無法釋放。
這里的“自己”理解為編程人員自身。與“生成”、“持有”、“釋放”操作并列的還有“廢棄”,分別對應以下方法:
| 對象操作 | Objective-C方法 |
|---|---|
| 生成并持有對象 |
alloc/new/copy/mutableCopy等方法 |
| 持有對象 |
retain方法 |
| 釋放對象 |
release方法 |
| 廢棄對象 |
dealloc方法 |
- 注意:以上方法包含在Cocoa框架中而非Objective-C語言中。
自己生成的對象,自己所持有
以下面名稱開頭的方法生成的對象為自己持有:
allocnewcopymutableCopy
id obj1 = [[NSObject alloc] init];// 自己生成并持有
id obj2 = [NSObject new];// 自己生成并持有
另外,根據(jù)以上原則,下列方法也意味著自己生成并持有對象:
allocMyObjectnewThatObjectcopyThismutableCopyYourObject
id obj = [MyObject allocMyObject];
// 內(nèi)部實現(xiàn)
+ (MyObject *)allocMyObject {
MyObject *obj = [[MyObject alloc] init];
return obj;
}
非自己生成的對象,自己也能持有
alloc/new/copy/mutableCopy以外方法取得對象,非自己生成,自己不持有對象。可以通過retain方法為自己所持有。
id obj = [NSMutableArray array];// 取得對象,但自己不持有
[obj retain];// 自己持有對象
不再需要自己持有的對象時釋放
自己持有的對象不再需要時,持有者有義務將其釋放。釋放使用release方法。
id obj = [[NSObject alloc] init];// 自己生成并持有對象
[obj release];// 釋放對象
用retain方法持有對象,一旦不再需要,務必要用release方法釋放。
id obj = [NSMutableArray array];// 取得對象,但自己不持有
[obj retain];// 持有非自己生成對象
[obj release];// 釋放對象
類似[NSMutableArray array]方法取得的對象存在,但自己不持有對象,內(nèi)部如何實現(xiàn)?以object這個方法名為例:
- (id)object {
id obj = [[NSObject alloc] init];// 自己持有
[obj autorelease];// 適當時機自動釋放
return obj;// 取得對象存在,但自己不持有
}
autorelease提供這樣的功能,使對象在超出指定的生存范圍時能夠自動并正確地釋放。
使用NSMutableArray類的array類方法等可以取得誰都不持有的對象,這些方法是通過autorelease實現(xiàn)的。
非自己持有的對象無法釋放
用alloc/new/copy/mutableCopy方法生成并持有的對象,或用retain方法持有的對象,在不需要時要將其釋放。倘若在應用程序中釋放了非自己持有的對象會造成崩潰。
id obj = [[NSObject alloc] init];// 自己生成并持有對象
[obj release];// 釋放對象
[obj release];// 重復釋放對象,崩潰
id obj1 = [obj0 object];// 取得對象,但自己不持有
[obj1 release];// 釋放非自己持有的對象,崩潰
alloc/retain/release/dealloc及其實現(xiàn)
Cocoa是macOS的系統(tǒng)框架,在iOS上被稱為Cocoa Touch。Cocoa框架雖然沒有公開,但是可以通過Cocoa框架的互換框架GNUstep來推測蘋果的實現(xiàn)。
alloc調(diào)用allocWithZone,那么這里的參數(shù)類型NSZone是什么?
它是為了防止內(nèi)存碎片化而引入的結(jié)構(gòu)。對內(nèi)存分配的區(qū)域本身進行多重化的管理,根據(jù)使用對象的目的、對象的大小分配內(nèi)存,從而提高內(nèi)存管理的效率。
現(xiàn)在運行時系統(tǒng)中的內(nèi)存管理已經(jīng)極具效率,使用區(qū)域來管理內(nèi)存反而會引起內(nèi)存使用效率低下以及源代碼復雜等問題。
GNUstep的實現(xiàn)
GNUstep源碼里alloc類方法用obj_layout結(jié)構(gòu)體中的整數(shù)變量retained來保存引用計數(shù)retainCount,并將其寫入對象內(nèi)存頭部。
執(zhí)行alloc后對象的實例方法retainCount獲得數(shù)值是1,retain使變量retained值+1,release使變量retained值-1。release使tetained變量大于0時-1,等于0時調(diào)用dealloc實例方法,廢棄對象。
具體總結(jié)如下:
- 在Objective-C的對象中存有引用計數(shù)這一整數(shù)值。
- 調(diào)用
alloc或是retain方法后,引用計數(shù)值+1。 - 調(diào)用
release后,引用計數(shù)值-1。 - 引用計數(shù)值為0時,調(diào)用
dealloc方法廢棄對象。
蘋果的實現(xiàn)
alloc過程設置斷點追蹤調(diào)用的方法和函數(shù):
+alloc
+allocWithZone:
class_createInstance
calloc//分配內(nèi)存塊
蘋果對alloc的實現(xiàn)與GNUstep并無多大差異。
retainCount/retain/release調(diào)用的方法和函數(shù)分別如下:
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CEBasicHashRemoveValue返回0時,-release 調(diào)用dealloc)
可以從__CFDoExternRefOperation函數(shù)以及一些CFBasicHash開頭的函數(shù)名看出,蘋果的實現(xiàn)大概就是采用散列表(又稱哈希表)來管理引用計數(shù)。
在引用計數(shù)表中,key為內(nèi)存塊地址,value為對應的引用計數(shù),蘋果這樣實現(xiàn)的優(yōu)勢在于:
- 為對象分配內(nèi)存塊時無需考慮內(nèi)存塊頭部。
- 對象占用內(nèi)存塊損壞時,可以根據(jù)引用計數(shù)表來確認內(nèi)存塊的位置。
- 檢測內(nèi)存泄露時,根據(jù)引用計數(shù)表中的記錄檢查對象的持有者是否存在。
autorelease及其實現(xiàn)
autorelease會像C語言的自動變量那樣對待對象實例。當超出其作用域時,對象實例的release實例方法被調(diào)用。
autorelease具體使用方法如下:
- 1.生成并持有
NSAutoreleasePool對象; - 2.調(diào)用已分配對象的
autorelease實例方法; - 3.廢棄
NSAutoreleasePool對象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj release];
[pool drain];
Cocoa框架中程序主循環(huán)的NSRunLoop對NSAutoreleasePool對象進行生成、持有和廢棄處理。在大量產(chǎn)生autorelease對象時,若不廢棄NSAutoreleasePool對象,那么生成的對象就不能被廢棄,會產(chǎn)生內(nèi)存不足現(xiàn)象。
GNUstep的實現(xiàn)
autorelease實例方法的本質(zhì)就是調(diào)用NSAutoreleasePool對象的addObject類方法。
[obj autorelease];
源碼:
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
GNUstep在實現(xiàn)NSAutoreleasePool時使用連接列表,可以理解為數(shù)組。若調(diào)用NSObject類的autorelease方法,該對象就會被追加到正在使用的NSAutoreleasePool對象的數(shù)組中。drain實例方法廢棄正在使用的NSAutoreleasePool對象,會對數(shù)組中的所有對象調(diào)用release方法。
蘋果的實現(xiàn)
autoreleasepool以數(shù)組的形式實現(xiàn),主要通過以下3個函數(shù):
obj_autoreleasePoolPush()obj_autorelease(obj)obj_autoreleasePoolPop(pool)
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/* 等同于objc_autoreleasePoolPush */
id obj = [[NSObject alloc] init];
[obj autorelease];
/* 等同于 objc_autorelease(obj) */
[pool drain];
/* 等同于 objc_autoreleasePoolPop(pool) */
以上是MRC環(huán)境下的內(nèi)存管理及實現(xiàn)。
ARC
ARC概述
ARC(Auto Reference Counting)是iOS5、macOS10.7(OS X Lion)引入的內(nèi)存管理技術(shù)。
ARC的出現(xiàn)解決了原來需要手動鍵入
retain或release操作的問題。這在降低程序崩潰、內(nèi)存風險的同時,很大程度上減少了開發(fā)程序的工作量。
內(nèi)存管理的思考方式
“引用計數(shù)式內(nèi)存管理”的本質(zhì)在ARC中并沒有改變,ARC只是自動地幫我們處理“引用計數(shù)”的相關(guān)部分。
- 自己生成的對象,自己所持有。
- 非自己生成的對象,自己也能持有。
- 不再需要自己持有的對象時釋放。
- 非自己持有的對象無法釋放。
所有權(quán)修飾符
ARC環(huán)境下其類型必須附加所有權(quán)修飾符(有省略的情況),所有權(quán)修飾符有以下4種:
- __strong
- __weak
- __unsafe_unretained
- __autorelease
書中此處提到id類型做一下記錄:
Objective-C中為了處理對象,可將變量定義為
id類型,id類型用于隱藏對象類型的類名部分,相當于C語言中常用到的void *。
__strong修飾符
id和對象類型默認使用__strong修飾,由于是默認情況,可省略不寫。
__strong表示對對象的強引用。持有強引用的變量在超出其作用域時被廢棄。
__strong同__weak、__autoreleasing一樣,可以保證被修飾的變量在初始化時為nil。
id obj = [[NSObject alloc] init];
//等同于
//id __strong obj = [[NSObject alloc] init];
__weak修飾符
循環(huán)引用容易引起內(nèi)存泄漏。所謂內(nèi)存泄漏就是應當廢棄的對象在超出其生存周期后繼續(xù)存在。使用
__weak修飾符可以避免循環(huán)引用。
__weak表示弱引用,弱引用不能持有對象實例。
id __weak obj = [[NSObject alloc] init];//編譯器會警告
__weak修飾符還有另一個優(yōu)點。在持有對象的弱引用時,若對象被廢棄,則此弱引用將失效且處于nil被賦值的狀態(tài)。
通過檢查__weak修飾的變量是否為nil可以判斷被賦值的對象是否已廢棄。
__weak只能用于iOS5和macOS10.7以上版本,在iOS4和macOS10.6及以前用__unsafe_unretained代替。
__autoreleasing修飾符
ARC下指定@autoreleasepool塊來替代NSAutoreleasePool類生成、持有及廢棄這一范圍。_autoreleasing修飾變量等價于對象調(diào)用autorelease方法,即可將對象注冊到autoreleasepool中。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
- 提問:前文提到
__weak修飾的變量必須注冊到autoreleasepool中,為什么? - 答:因為
__weak修飾的變量只能持有對象的弱引用,在訪問對象的過程中,該對象可能被廢棄。如果把要訪問的對象注冊到autoreleasepool中,那么在@autoreleasepool塊結(jié)束之前能確保該對象存在。
_autoreleasing同__strong一樣,顯式使用罕見。
ARC的規(guī)則
ARC環(huán)境下編譯源代碼遵循一定規(guī)則:
- 不能使用
retain/release/retainCount/autorelease - 不能使用
NSAllocateObject/NSDeallocateObject
ARC有效時,以上方法會導致編譯器報錯。
- 必須遵守內(nèi)存管理的方法命名規(guī)則
對象的生成、持有的方法必須遵循命名規(guī)則:alloc/new/copy/mutableCopy。以init開頭的方法更嚴格:必須是實例方法且必須返回對象,返回對象的類型必須是id類型或該方法聲明類的對象類型。
- 不要顯式調(diào)用
dealloc
dealloc方法無需顯式調(diào)用,但C語言庫需要在dealloc中free,以及刪除已注冊的通知觀察者。
- 使用
@autorelease塊代替NSAutoreleasePool
ARC有效時,使用@autoreleasepool塊代替NSAutoreleasePool。
- 不能使用區(qū)域(
NSZone)
不管ARC是否有效,區(qū)域在現(xiàn)在運行時系統(tǒng)中已單純地被忽略。
- 對象型變量不能作為C語言結(jié)構(gòu)體(
struct/union)的成員
C語言的規(guī)約上沒有方法來管理結(jié)構(gòu)體成員變量的生存周期。
- 顯式轉(zhuǎn)換“
id”和“void *”
/* ARC無效 */
id obj = [[NSObject alloc] init];
void *p = obj
ARC有效時需要通過__bridge來顯式轉(zhuǎn)換:
/* ARC有效 */
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;
id o = (__bridge id)p;
屬性和數(shù)組
- 聲明屬性所用的關(guān)鍵詞與所有權(quán)修飾符的對應關(guān)系:
| 聲明屬性的關(guān)鍵詞 | 所有權(quán)修飾符 |
|---|---|
| assign | __unsafe_unretained |
| copy | __strong |
| retain | __strong |
| strong | __strong |
| unsafe_unretained | __unsafe_unretained |
| weak | __weak |
- 動態(tài)數(shù)組中操作
__strong修飾的變量與靜態(tài)數(shù)組有很大差異,需要自己釋放所有元素。靜態(tài)數(shù)組中,編譯器能夠根據(jù)變量的作用域自動插入釋放賦值對象的代碼,而在動態(tài)數(shù)組中,編譯器不能確定數(shù)組的生存周期,所以無從處理。
ARC的實現(xiàn)
__strong的實現(xiàn)
- 自己生成并持有
{
id __strong obj = [[NSObject alloc] init];
}
/* 編譯器的模擬代碼 */
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
- 非自己生成持有
id __strong obj = [NSMutableArray array];
/* 編譯器的模擬代碼 */
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
objc_retainAutoreleasedReturnValue函數(shù)用于持有對象,注冊到autoreleasepool中并返回。與之對應的函數(shù)是objc_autoreleaseReturnValue。
+ (id)array {
return [[NSMutableArray alloc] init];
}
/* 編譯器的模擬代碼 */
+ (id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
通過
objc_retainAutoreleasedReturnValue函數(shù)和objc_autoreleaseReturnValue函數(shù)的協(xié)作,可以不將對象注冊到autoreleasepool中而直接傳遞,以達到最優(yōu)化程序運行。
__weak的實現(xiàn)
使用
__weak修飾的變量,就是使用注冊到autoreleasepool中的對象。
{
id __weak obj1 = obj;
}
/* 編譯器模擬代碼 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
objc_destroyWeak(obj1);
__weak同引用計數(shù)一樣通過散列表(哈希表)實現(xiàn),大致流程如下:
- 1.
objc_initWeak(&obj1, obj)函數(shù)初始化__weak修飾的變量,通過執(zhí)行objc_storeWeak(&obj1, obj)函數(shù),以第一個參數(shù)(變量的地址)作為key,把第二個參數(shù)(賦值對象)作為value存入哈希表。 - 2.由于弱引用不能持有對象,函數(shù)
objc_loadWeakRetained(&obj1)取出所引用的對象并retain。 - 3.
objc_autorelease(tmp)函數(shù)將對象注冊到autoreleasepool中。 - 4.
objc_destroyWeak(&obj1)函數(shù)釋放__weak修飾的變量,通過過程執(zhí)行objc_store(&obj1, 0)函數(shù),在weak表中查到變量地址并刪除。廢棄對象調(diào)用objc_clear_deallocating函數(shù),這個過程會將weak表記錄中__weak修飾的變量地址賦值為nil。
如果大量使用
__weak修飾的變量,則會消耗相應的CPU資源。良策是只在需要避免循環(huán)引用時使用__weak修飾符。
__autoreleasing的實現(xiàn)
_autoreleasing修飾變量,等同于ARC無效時對象調(diào)用autorelease方法。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
/* 編譯器的模擬代碼 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelese(obj);
objc_autoreleasePoolPop();
以上為ARC篇的學習內(nèi)容。