《Objective-C高級(jí)編程:iOS與OS X多線程和內(nèi)存管理》是iOS開發(fā)中一本經(jīng)典書籍吃型,書中有關(guān)ARC、Block、GCD的梳理是iOS開發(fā)進(jìn)階路上必不可少的知識(shí)儲(chǔ)備。筆者讀完此書后為了加強(qiáng)理解,特以筆記記之邑时。本文為開篇,圍繞ARC談起Objective-C中的內(nèi)存管理特姐。
鑒于本書翻譯自日文原版且翻譯偏向書面晶丘,筆者希望采用通俗的語言記錄,文章結(jié)構(gòu)略有調(diào)整唐含。
本文首發(fā)于Rachal's blog浅浮。
內(nèi)存管理
ARC(自動(dòng)引用計(jì)數(shù))是iOS5、macOS10.7引入的內(nèi)存管理技術(shù)捷枯,為了循序漸進(jìn)的方式了解這項(xiàng)技術(shù)滚秩,本書先從ARC無效的環(huán)境說起递惋,也就是常指的MRC(手動(dòng)引用計(jì)數(shù))環(huán)境坑匠。
本書開篇沒有直接提及引用計(jì)數(shù)的概念,而是以辦公室開燈關(guān)燈的例子引出內(nèi)存管理的思考方式惊来。作者認(rèn)為理解內(nèi)存管理時(shí)把注意力落在“生成”攀痊、“持有”桐腌、“釋放”等管理操作上更為客觀。
內(nèi)存管理的思考方式
- 自己生成的對(duì)象蚕苇,自己所持有哩掺。
- 非自己生成的對(duì)象,自己也能持有涩笤。
- 不再需要自己持有的對(duì)象時(shí)釋放嚼吞。
- 非自己持有的對(duì)象無法釋放。
這里的“自己”理解為編程人員自身蹬碧。與“生成”舱禽、“持有”、“釋放”操作并列的還有“廢棄”恩沽,分別對(duì)應(yīng)以下方法:
對(duì)象操作 | Objective-C方法 |
---|---|
生成并持有對(duì)象 |
alloc /new /copy /mutableCopy 等方法 |
持有對(duì)象 |
retain 方法 |
釋放對(duì)象 |
release 方法 |
廢棄對(duì)象 |
dealloc 方法 |
- 注意:以上方法包含在Cocoa框架中而非Objective-C語言中誊稚。
自己生成的對(duì)象,自己所持有
以下面名稱開頭的方法生成的對(duì)象為自己持有:
alloc
new
copy
mutableCopy
id obj1 = [[NSObject alloc] init];// 自己生成并持有
id obj2 = [NSObject new];// 自己生成并持有
另外罗心,根據(jù)以上原則里伯,下列方法也意味著自己生成并持有對(duì)象:
allocMyObject
newThatObject
copyThis
mutableCopyYourObject
id obj = [MyObject allocMyObject];
// 內(nèi)部實(shí)現(xiàn)
+ (MyObject *)allocMyObject {
MyObject *obj = [[MyObject alloc] init];
return obj;
}
非自己生成的對(duì)象,自己也能持有
alloc
/new
/copy
/mutableCopy
以外方法取得對(duì)象渤闷,非自己生成疾瓮,自己不持有對(duì)象§可以通過retain
方法為自己所持有狼电。
id obj = [NSMutableArray array];// 取得對(duì)象蜒灰,但自己不持有
[obj retain];// 自己持有對(duì)象
不再需要自己持有的對(duì)象時(shí)釋放
自己持有的對(duì)象不再需要時(shí),持有者有義務(wù)將其釋放肩碟。釋放使用release
方法强窖。
id obj = [[NSObject alloc] init];// 自己生成并持有對(duì)象
[obj release];// 釋放對(duì)象
用retain
方法持有對(duì)象,一旦不再需要削祈,務(wù)必要用release
方法釋放翅溺。
id obj = [NSMutableArray array];// 取得對(duì)象,但自己不持有
[obj retain];// 持有非自己生成對(duì)象
[obj release];// 釋放對(duì)象
類似[NSMutableArray array]
方法取得的對(duì)象存在岩瘦,但自己不持有對(duì)象未巫,內(nèi)部如何實(shí)現(xiàn)窿撬?以object
這個(gè)方法名為例:
- (id)object {
id obj = [[NSObject alloc] init];// 自己持有
[obj autorelease];// 適當(dāng)時(shí)機(jī)自動(dòng)釋放
return obj;// 取得對(duì)象存在启昧,但自己不持有
}
autorelease
提供這樣的功能,使對(duì)象在超出指定的生存范圍時(shí)能夠自動(dòng)并正確地釋放劈伴。
使用NSMutableArray
類的array
類方法等可以取得誰都不持有的對(duì)象密末,這些方法是通過autorelease
實(shí)現(xiàn)的。
非自己持有的對(duì)象無法釋放
用alloc
/new
/copy
/mutableCopy
方法生成并持有的對(duì)象跛璧,或用retain
方法持有的對(duì)象严里,在不需要時(shí)要將其釋放。倘若在應(yīng)用程序中釋放了非自己持有的對(duì)象會(huì)造成崩潰追城。
id obj = [[NSObject alloc] init];// 自己生成并持有對(duì)象
[obj release];// 釋放對(duì)象
[obj release];// 重復(fù)釋放對(duì)象刹碾,崩潰
id obj1 = [obj0 object];// 取得對(duì)象,但自己不持有
[obj1 release];// 釋放非自己持有的對(duì)象座柱,崩潰
alloc/retain/release/dealloc及其實(shí)現(xiàn)
Cocoa是macOS的系統(tǒng)框架迷帜,在iOS上被稱為Cocoa Touch。Cocoa框架雖然沒有公開色洞,但是可以通過Cocoa框架的互換框架GNUstep來推測(cè)蘋果的實(shí)現(xiàn)戏锹。
alloc
調(diào)用allocWithZone
,那么這里的參數(shù)類型NSZone
是什么火诸?
它是為了防止內(nèi)存碎片化而引入的結(jié)構(gòu)锦针。對(duì)內(nèi)存分配的區(qū)域本身進(jìn)行多重化的管理,根據(jù)使用對(duì)象的目的置蜀、對(duì)象的大小分配內(nèi)存奈搜,從而提高內(nèi)存管理的效率。
現(xiàn)在運(yùn)行時(shí)系統(tǒng)中的內(nèi)存管理已經(jīng)極具效率盯荤,使用區(qū)域來管理內(nèi)存反而會(huì)引起內(nèi)存使用效率低下以及源代碼復(fù)雜等問題馋吗。
GNUstep的實(shí)現(xiàn)
GNUstep源碼里alloc
類方法用obj_layout
結(jié)構(gòu)體中的整數(shù)變量retained
來保存引用計(jì)數(shù)retainCount
,并將其寫入對(duì)象內(nèi)存頭部廷雅。
執(zhí)行alloc
后對(duì)象的實(shí)例方法retainCount
獲得數(shù)值是1耗美,retain
使變量retained
值+1京髓,release
使變量retained
值-1。release
使tetained
變量大于0時(shí)-1商架,等于0時(shí)調(diào)用dealloc
實(shí)例方法堰怨,廢棄對(duì)象。
具體總結(jié)如下:
- 在Objective-C的對(duì)象中存有引用計(jì)數(shù)這一整數(shù)值蛇摸。
- 調(diào)用
alloc
或是retain
方法后备图,引用計(jì)數(shù)值+1。 - 調(diào)用
release
后赶袄,引用計(jì)數(shù)值-1揽涮。 - 引用計(jì)數(shù)值為0時(shí),調(diào)用
dealloc
方法廢棄對(duì)象饿肺。
蘋果的實(shí)現(xiàn)
alloc
過程設(shè)置斷點(diǎn)追蹤調(diào)用的方法和函數(shù):
+alloc
+allocWithZone:
class_createInstance
calloc//分配內(nèi)存塊
蘋果對(duì)alloc
的實(shí)現(xiàn)與GNUstep并無多大差異蒋困。
retainCount
/retain
/release
調(diào)用的方法和函數(shù)分別如下:
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CEBasicHashRemoveValue返回0時(shí),-release 調(diào)用dealloc)
可以從__CFDoExternRefOperation
函數(shù)以及一些CFBasicHash
開頭的函數(shù)名看出敬辣,蘋果的實(shí)現(xiàn)大概就是采用散列表(又稱哈希表)來管理引用計(jì)數(shù)雪标。
在引用計(jì)數(shù)表中,key
為內(nèi)存塊地址溉跃,value
為對(duì)應(yīng)的引用計(jì)數(shù)村刨,蘋果這樣實(shí)現(xiàn)的優(yōu)勢(shì)在于:
- 為對(duì)象分配內(nèi)存塊時(shí)無需考慮內(nèi)存塊頭部。
- 對(duì)象占用內(nèi)存塊損壞時(shí)撰茎,可以根據(jù)引用計(jì)數(shù)表來確認(rèn)內(nèi)存塊的位置嵌牺。
- 檢測(cè)內(nèi)存泄露時(shí),根據(jù)引用計(jì)數(shù)表中的記錄檢查對(duì)象的持有者是否存在龄糊。
autorelease及其實(shí)現(xiàn)
autorelease
會(huì)像C語言的自動(dòng)變量那樣對(duì)待對(duì)象實(shí)例逆粹。當(dāng)超出其作用域時(shí),對(duì)象實(shí)例的release
實(shí)例方法被調(diào)用绎签。
autorelease
具體使用方法如下:
- 1.生成并持有
NSAutoreleasePool
對(duì)象枯饿; - 2.調(diào)用已分配對(duì)象的
autorelease
實(shí)例方法; - 3.廢棄
NSAutoreleasePool
對(duì)象诡必。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj release];
[pool drain];
Cocoa框架中程序主循環(huán)的NSRunLoop
對(duì)NSAutoreleasePool
對(duì)象進(jìn)行生成奢方、持有和廢棄處理。在大量產(chǎn)生autorelease
對(duì)象時(shí)爸舒,若不廢棄NSAutoreleasePool
對(duì)象蟋字,那么生成的對(duì)象就不能被廢棄,會(huì)產(chǎn)生內(nèi)存不足現(xiàn)象扭勉。
GNUstep的實(shí)現(xiàn)
autorelease
實(shí)例方法的本質(zhì)就是調(diào)用NSAutoreleasePool
對(duì)象的addObject
類方法鹊奖。
[obj autorelease];
源碼:
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
GNUstep在實(shí)現(xiàn)NSAutoreleasePool
時(shí)使用連接列表,可以理解為數(shù)組涂炎。若調(diào)用NSObject
類的autorelease
方法忠聚,該對(duì)象就會(huì)被追加到正在使用的NSAutoreleasePool
對(duì)象的數(shù)組中设哗。drain
實(shí)例方法廢棄正在使用的NSAutoreleasePool
對(duì)象,會(huì)對(duì)數(shù)組中的所有對(duì)象調(diào)用release
方法两蟀。
蘋果的實(shí)現(xiàn)
autoreleasepool以數(shù)組的形式實(shí)現(xiàn)网梢,主要通過以下3個(gè)函數(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)存管理及實(shí)現(xiàn)。
ARC
ARC概述
ARC(Auto Reference Counting)是iOS5赂毯、macOS10.7(OS X Lion)引入的內(nèi)存管理技術(shù)战虏。
ARC的出現(xiàn)解決了原來需要手動(dòng)鍵入
retain
或release
操作的問題。這在降低程序崩潰党涕、內(nèi)存風(fēng)險(xiǎn)的同時(shí)烦感,很大程度上減少了開發(fā)程序的工作量。
內(nèi)存管理的思考方式
“引用計(jì)數(shù)式內(nèi)存管理”的本質(zhì)在ARC中并沒有改變膛堤,ARC只是自動(dòng)地幫我們處理“引用計(jì)數(shù)”的相關(guān)部分手趣。
- 自己生成的對(duì)象,自己所持有骑祟。
- 非自己生成的對(duì)象回懦,自己也能持有。
- 不再需要自己持有的對(duì)象時(shí)釋放次企。
- 非自己持有的對(duì)象無法釋放。
所有權(quán)修飾符
ARC環(huán)境下其類型必須附加所有權(quán)修飾符(有省略的情況)潜圃,所有權(quán)修飾符有以下4種:
- __strong
- __weak
- __unsafe_unretained
- __autorelease
書中此處提到id
類型做一下記錄:
Objective-C中為了處理對(duì)象缸棵,可將變量定義為
id
類型,id
類型用于隱藏對(duì)象類型的類名部分谭期,相當(dāng)于C語言中常用到的void *
堵第。
__strong修飾符
id
和對(duì)象類型默認(rèn)使用__strong
修飾,由于是默認(rèn)情況隧出,可省略不寫踏志。
__strong
表示對(duì)對(duì)象的強(qiáng)引用。持有強(qiáng)引用的變量在超出其作用域時(shí)被廢棄胀瞪。
__strong
同__weak
针余、__autoreleasing
一樣,可以保證被修飾的變量在初始化時(shí)為nil
凄诞。
id obj = [[NSObject alloc] init];
//等同于
//id __strong obj = [[NSObject alloc] init];
__weak修飾符
循環(huán)引用容易引起內(nèi)存泄漏圆雁。所謂內(nèi)存泄漏就是應(yīng)當(dāng)廢棄的對(duì)象在超出其生存周期后繼續(xù)存在。使用
__weak
修飾符可以避免循環(huán)引用帆谍。
__weak
表示弱引用伪朽,弱引用不能持有對(duì)象實(shí)例。
id __weak obj = [[NSObject alloc] init];//編譯器會(huì)警告
__weak
修飾符還有另一個(gè)優(yōu)點(diǎn)汛蝙。在持有對(duì)象的弱引用時(shí)烈涮,若對(duì)象被廢棄朴肺,則此弱引用將失效且處于nil
被賦值的狀態(tài)。
通過檢查__weak
修飾的變量是否為nil
可以判斷被賦值的對(duì)象是否已廢棄坚洽。
__weak
只能用于iOS5和macOS10.7以上版本宇挫,在iOS4和macOS10.6及以前用__unsafe_unretained
代替。
__autoreleasing修飾符
ARC下指定@autoreleasepool
塊來替代NSAutoreleasePool
類生成酪术、持有及廢棄這一范圍器瘪。_autoreleasing
修飾變量等價(jià)于對(duì)象調(diào)用autorelease
方法,即可將對(duì)象注冊(cè)到autoreleasepool中绘雁。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
- 提問:前文提到
__weak
修飾的變量必須注冊(cè)到autoreleasepool中橡疼,為什么? - 答:因?yàn)?code>__weak修飾的變量只能持有對(duì)象的弱引用庐舟,在訪問對(duì)象的過程中欣除,該對(duì)象可能被廢棄。如果把要訪問的對(duì)象注冊(cè)到autoreleasepool中挪略,那么在
@autoreleasepool
塊結(jié)束之前能確保該對(duì)象存在历帚。
_autoreleasing
同__strong
一樣,顯式使用罕見杠娱。
ARC的規(guī)則
ARC環(huán)境下編譯源代碼遵循一定規(guī)則:
- 不能使用
retain
/release
/retainCount
/autorelease
- 不能使用
NSAllocateObject
/NSDeallocateObject
ARC有效時(shí)挽牢,以上方法會(huì)導(dǎo)致編譯器報(bào)錯(cuò)。
- 必須遵守內(nèi)存管理的方法命名規(guī)則
對(duì)象的生成摊求、持有的方法必須遵循命名規(guī)則:alloc
/new
/copy
/mutableCopy
。以init
開頭的方法更嚴(yán)格:必須是實(shí)例方法且必須返回對(duì)象睹栖,返回對(duì)象的類型必須是id
類型或該方法聲明類的對(duì)象類型野来。
- 不要顯式調(diào)用
dealloc
dealloc
方法無需顯式調(diào)用曼氛,但C語言庫需要在dealloc
中free
搪锣,以及刪除已注冊(cè)的通知觀察者。
- 使用
@autorelease
塊代替NSAutoreleasePool
ARC有效時(shí)狗超,使用@autoreleasepool
塊代替NSAutoreleasePool
努咐。
- 不能使用區(qū)域(
NSZone
)
不管ARC是否有效渗稍,區(qū)域在現(xiàn)在運(yùn)行時(shí)系統(tǒng)中已單純地被忽略竿屹。
- 對(duì)象型變量不能作為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有效時(shí)需要通過__bridge
來顯式轉(zhuǎn)換:
/* ARC有效 */
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;
id o = (__bridge id)p;
屬性和數(shù)組
- 聲明屬性所用的關(guān)鍵詞與所有權(quán)修飾符的對(duì)應(yīng)關(guān)系:
聲明屬性的關(guān)鍵詞 | 所有權(quán)修飾符 |
---|---|
assign | __unsafe_unretained |
copy | __strong |
retain | __strong |
strong | __strong |
unsafe_unretained | __unsafe_unretained |
weak | __weak |
- 動(dòng)態(tài)數(shù)組中操作
__strong
修飾的變量與靜態(tài)數(shù)組有很大差異,需要自己釋放所有元素召嘶。靜態(tài)數(shù)組中哮缺,編譯器能夠根據(jù)變量的作用域自動(dòng)插入釋放賦值對(duì)象的代碼碟绑,而在動(dòng)態(tài)數(shù)組中,編譯器不能確定數(shù)組的生存周期诵冒,所以無從處理。
ARC的實(shí)現(xiàn)
__strong的實(shí)現(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ù)用于持有對(duì)象,注冊(cè)到autoreleasepool中并返回铁蹈。與之對(duì)應(yīng)的函數(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é)作,可以不將對(duì)象注冊(cè)到autoreleasepool中而直接傳遞沿腰,以達(dá)到最優(yōu)化程序運(yùn)行。
__weak的實(shí)現(xiàn)
使用
__weak
修飾的變量颂龙,就是使用注冊(cè)到autoreleasepool中的對(duì)象。
{
id __weak obj1 = obj;
}
/* 編譯器模擬代碼 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
objc_destroyWeak(obj1);
__weak
同引用計(jì)數(shù)一樣通過散列表(哈希表)實(shí)現(xiàn)措嵌,大致流程如下:
- 1.
objc_initWeak(&obj1, obj)
函數(shù)初始化__weak
修飾的變量,通過執(zhí)行objc_storeWeak(&obj1, obj)
函數(shù)铅匹,以第一個(gè)參數(shù)(變量的地址)作為key
押赊,把第二個(gè)參數(shù)(賦值對(duì)象)作為value
存入哈希表。 - 2.由于弱引用不能持有對(duì)象流礁,函數(shù)
objc_loadWeakRetained(&obj1)
取出所引用的對(duì)象并retain
。 - 3.
objc_autorelease(tmp)
函數(shù)將對(duì)象注冊(cè)到autoreleasepool
中。 - 4.
objc_destroyWeak(&obj1)
函數(shù)釋放__weak
修飾的變量萌抵,通過過程執(zhí)行objc_store(&obj1, 0)
函數(shù),在weak表中查到變量地址并刪除讨永。廢棄對(duì)象調(diào)用objc_clear_deallocating
函數(shù)揭糕,這個(gè)過程會(huì)將weak表記錄中__weak
修飾的變量地址賦值為nil
。
如果大量使用
__weak
修飾的變量锻霎,則會(huì)消耗相應(yīng)的CPU資源著角。良策是只在需要避免循環(huán)引用時(shí)使用__weak
修飾符。
__autoreleasing的實(shí)現(xiàn)
_autoreleasing
修飾變量旋恼,等同于ARC無效時(shí)對(duì)象調(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篇的學(xué)習(xí)內(nèi)容。