iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇1/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇2/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇3/4
iOS開發(fā)讀書筆記:Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法 - 篇4/4
- 第五章 內(nèi)存管理
- 第29條:理解引用計(jì)數(shù)
- 第30條:以ARC簡化引用計(jì)數(shù)
- 第31條:在dealloc方法中只是否引用并解除監(jiān)聽
- 第32條:編寫“異常安全代碼”時(shí)留意內(nèi)存管理問題
- 第33條:以弱引用避免保留環(huán)
- 第34條:以“自動(dòng)釋放池塊”降低內(nèi)存峰值
- 第35條:用“僵尸對象”調(diào)試內(nèi)存管理問題
- 第36條:不要使用retainCount
第五章 內(nèi)存管理
ARC幾乎把所有內(nèi)存管理事宜都交由編譯器來決定,開發(fā)者只需專注于業(yè)務(wù)邏輯晾蜘。ARC實(shí)際上也是一種引用計(jì)數(shù)機(jī)制。
第29條:理解引用計(jì)數(shù)
Objective-C語言使用引用計(jì)數(shù)來管理內(nèi)存,也就是說,每個(gè)對象都有個(gè)可以遞增或遞減的計(jì)數(shù)器吗跋。
從Mac OS X 10.8開始桌粉,“垃圾收集器”(garbage collector)已經(jīng)正式廢棄了,以O(shè)bjective-C代碼編寫Mac OS X程序時(shí)不應(yīng)再使用它祭钉,而iOS則從未支持過垃圾收集器。
引用計(jì)數(shù)工作原理
對象創(chuàng)建出來時(shí)己沛,其保留計(jì)數(shù)至少為1慌核。若想令其繼續(xù)存活,則調(diào)用retain方法申尼。要是某部分代碼不再使用此對象垮卓,不想令其繼續(xù)存活,那就調(diào)用 release或autorelease方法师幕。最終當(dāng)保留計(jì)數(shù)歸零時(shí)粟按,對象就回收了(deallocated)诬滩,NSObject協(xié)議聲明了下面三個(gè)方法用于操作計(jì)數(shù)器,以遞增或遞減其值:
- retain遞增保留計(jì)數(shù)灭将。
- release遞減保留計(jì)數(shù)疼鸟。
- autorelease待稍后清理“自動(dòng)釋放池”(autorelease pool)時(shí),再遞減保留計(jì)數(shù)庙曙。(本條后面幾頁與本書第34條將會(huì)詳細(xì)講解“自動(dòng)釋放池”空镜。)
注意:查看保留計(jì)數(shù)的方法叫做retainCount,此方法不太有用捌朴。
如果按“引用樹”回溯吴攒,那么最終會(huì)發(fā)現(xiàn)一個(gè)“根對象”(root object)。在Mac OS X應(yīng)用程序中砂蔽,此對象就是NSApplication對象舶斧;而在iOS應(yīng)用程序中,則是UIApplication對象察皇。兩者都是應(yīng)用程序啟動(dòng)時(shí)創(chuàng)建的單例茴厉。
為避免在不經(jīng)意間使用了無效對象,一般調(diào)用完release之后都會(huì)清空指針什荣。這就能保證不會(huì)出現(xiàn)可能指向無效對象的指針矾缓,這種指針通常稱為“懸掛指針’(dangling pointer)。
屬性存取方法中的內(nèi)存管理
其他對象也可以保留別的對象稻爬,這一般通過訪問“屬性”(參見第6條)來實(shí)現(xiàn)嗜闻,而訪問屬性時(shí),會(huì)用到相關(guān)實(shí)例變量的獲取方法及設(shè)置方法桅锄。若屬性為“strong關(guān)系”(strong relationship)琉雳,則設(shè)置的屬性值會(huì)保留。
-(void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
此方法將保留新值并釋放舊值友瘤,然后更新實(shí)例變量翠肘,令其指向新值。順序很重要辫秧。
自動(dòng)釋放池
調(diào)用release會(huì)立刻遞減對象的保留計(jì)數(shù)(而且還有可能令系統(tǒng)回收此對象)束倍,然而有時(shí)候可以不調(diào)用它,改為調(diào)用 autorelease,此方法會(huì)在稍后遞減計(jì)數(shù)盟戏,通常是在下一次“事件循環(huán)”(event loop)時(shí)遞減, 不過也可能執(zhí)行得更早些(參見第34條)绪妹。
此特性很有用,尤其是在方法中返回對象時(shí)更應(yīng)該用它柿究。在這種情況下邮旷,我們并不總是想令方法調(diào)用者手工保留其值。比方說蝇摸,有下面這個(gè)方法:
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat: @"I am this: %@", self];
return str;
}
此時(shí)返回的str對象其保留計(jì)數(shù)比期望值要多1 ( +1 retain count)婶肩,因?yàn)檎{(diào)用alloc會(huì)令保留計(jì)數(shù)加1糕簿,而又沒有與之對應(yīng)的釋放操作。保留計(jì)數(shù)多1狡孔,就意味著調(diào)用者要負(fù)責(zé)處理多出來的這一次保留操作。必須設(shè)法將其抵消蜂嗽。
但是苗膝,不能在方法內(nèi)釋放str,否則還沒等方法返回,系統(tǒng)就把該對象回收了植旧。這里應(yīng)該用autorelease辱揭,它會(huì)在稍后釋放對象,從而給調(diào)用者留下了足夠長的時(shí)間病附,使其可以在需要時(shí)先保留返回值问窃。
換句話說,此方法可以保證對象在跨越“方法調(diào)用邊界”(method call boundary)后一定存活讽挟。實(shí)際上拢肆,釋放操作會(huì)在清空最外層的自動(dòng)釋放池(參見第34條)時(shí)執(zhí)行蕾哟,除非你有自己的自動(dòng)釋放池,否則這個(gè)時(shí)機(jī)指的就是當(dāng)前線程的下一次事件循環(huán)听皿。改寫stringValue方法,使用autorelease來釋放對象:
- (NSString*)stringValue {
NSString *str = [[NSString alloc] initWithFormat: @"I am this: %@", self];
return [str autorelease];
}
修改之后宽档,stringValue方法把NSString對象返回給凋用者時(shí)尉姨,此對象必然存活。
由于返回的str對象將于稍后自動(dòng)釋放吗冤,所以多出來的那一次保留操作到時(shí)自然就會(huì)抵消又厉,無須再執(zhí)行內(nèi)存管理操作。
保留環(huán)
使用引用計(jì)數(shù)機(jī)制時(shí)椎瘟,經(jīng)常要注意的一個(gè)問題就是“保留環(huán)"(retain cycle),也就是呈環(huán)狀相互引用的多個(gè)對象覆致。這將導(dǎo)致內(nèi)存泄漏。
通常采用“弱引用”(weak reference,參見第33 條)來解決此問題肺蔚,或是從外界命令循環(huán)中的某個(gè)對象不再保留另外一個(gè)對象篷朵。這兩種辦法都能打破保留環(huán),從而避免內(nèi)存泄漏婆排。
要點(diǎn):
- 引用計(jì)數(shù)機(jī)制通過可以遞增遞減的計(jì)數(shù)器來管理內(nèi)存声旺。對象創(chuàng)建好之后,其保留計(jì)數(shù)至少為1段只。若保留計(jì)數(shù)為正腮猖,則對象繼續(xù)存活。當(dāng)保留計(jì)數(shù)降為0時(shí)赞枕,對象就被銷毀了澈缺。
- 在對象生命期中坪创,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會(huì)遞增及遞減保留計(jì)數(shù)姐赡。
第30條:以ARC簡化引用計(jì)數(shù)
引用計(jì)數(shù)這個(gè)概念相當(dāng)容易理解(參見第29條)莱预。需要執(zhí)行保留與釋放操作的地方也很容易就能看出來。所以Clang編譯器項(xiàng)目帶有一個(gè)“靜態(tài)分析器"(static analyzer)项滑,用于指明程序里引用計(jì)數(shù)出問題的地方依沮。從而分析出有內(nèi)存泄漏問題的對象。這正是“靜態(tài)分析器”要做的事枪狂。靜態(tài)分析器還有更為深入的用途危喉。既然可以査明內(nèi)存管理問題,那么應(yīng)該也可以根據(jù)需要州疾,預(yù)先加入適當(dāng)?shù)谋A艋蜥尫挪僮饕员苊膺@些問題辜限,對吧?自動(dòng)引用計(jì)數(shù)這一思路正是源于此严蓖。
使用ARC時(shí)一定要記住薄嫡,引用計(jì)數(shù)實(shí)際上還是要執(zhí)行的,只不過保留與釋放操作現(xiàn)在是由ARC自動(dòng)為你添加颗胡。
由于ARC會(huì)自動(dòng)執(zhí)行retain岂座、release、autorelease等操作杭措,所以直接在ARC下調(diào)用這些內(nèi)存管理方法(retain费什、release、autorelease手素、dealloc
)是非法的鸳址。
實(shí)際上,ARC在調(diào)用這些方法時(shí)泉懦,并不通過普通的Objective-C消息派發(fā)機(jī)制稿黍,而是直接調(diào)用其底層C語言版本。這樣做性能更好崩哩。比方說巡球,ARC會(huì)調(diào)用與retain等價(jià)的底層函數(shù)objc_ retain。這也是不能覆寫retain邓嘹、release或autorelease的緣由酣栈,因?yàn)檫@些方法從來不會(huì)被直接調(diào)用。
使用ARC時(shí)必須遵循的方法命名規(guī)則
將內(nèi)存管理語義在方法名中表示出來早已成為Objective-C的慣例汹押,而ARC則將之確立為硬性規(guī)定矿筝。這些規(guī)則簡單地體現(xiàn)在方法名上。若方法名以alloc棚贾、new窖维、copy榆综、mutableCopy
詞語開頭,則其返回的對象歸調(diào)用者所有铸史。
歸調(diào)用者所有的意思是:調(diào)用上述四種方法的那段代碼要負(fù)責(zé)釋放方法所返回的對象鼻疮。 也就是說,這些對象的保留計(jì)數(shù)是正值琳轿,而調(diào)用了這四種方法的那段代碼要將其中一次保留操作抵消掉判沟。
若方法名不以上述四個(gè)詞語開頭,則表示其所返回的對象并不歸調(diào)用者所有利赋。在這種情況下,返回的對象會(huì)自動(dòng)釋放猩系,所以其值在跨越方法調(diào)用邊界后依然有效媚送。要想使對象多存活一段時(shí)間,必須令調(diào)用者保留它才行寇甸。比如下面的代碼:
+ (EOCPerson *)newPerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}
+ (EOCPerson*)somePerson {
EOCPerson *person = [[EOCPerson alloc] init];
return [person autorelease];
}
ARC通過命名約定將內(nèi)存管理規(guī)則標(biāo)準(zhǔn)化塘偎,其他編程語言很少像Objective-C這樣強(qiáng)調(diào)命名。
ARC的好處:
好處1:自動(dòng)調(diào)用“保留”與“釋放”方法拿霉;
好處2:它可以執(zhí)行一些手工操作很難甚至無法完成的優(yōu)化吟秩。例如,在編譯期绽淘,ARC會(huì)把能夠互相抵消的retain涵防、 release、autorelease操作約簡沪铭。如果發(fā)現(xiàn)在同一個(gè)對象上執(zhí)行了多次“保留”與“釋放”操作, 那么ARC有時(shí)可以成對地移除這兩個(gè)操作壮池;
好處3:ARC也包含運(yùn)行期組件。此時(shí)所執(zhí)行的優(yōu)化很有意義杀怠,前面講到椰憋,某些方法在返回對象前,為其執(zhí)行了 autorelease 操作赔退,而調(diào)用方法的代碼可能需要將返冋的對象保留橙依,ARC可以在運(yùn)行期檢測到這一對多余的操作,為了優(yōu)化代碼硕旗,不直接調(diào)用對象的autorelease/retain
方法窗骑,而是改為調(diào)用objc autoreleaseRetumValue/retainAutoreleasedRetumValue
,要比調(diào)用autorelease和retain
更快漆枚。
將內(nèi)存管理交由編譯器和運(yùn)行期組件來做慧域,可以使代碼得到多種優(yōu)化。
變量的內(nèi)存管理語義
ARC也會(huì)處理局部變量與實(shí)例變量的內(nèi)存管理浪读。默認(rèn)情況下昔榴,每個(gè)變量都是指向?qū)ο蟮膹?qiáng)引用辛藻。ARC會(huì)用一種安全的方式來設(shè)置:先保留新值,再釋放舊值互订,最后設(shè)置實(shí)例變量吱肌。
在應(yīng)用程序中,可用下列修飾符來改變局部變量與實(shí)例變量的語義:
- strong:默認(rèn)語義仰禽,保留此值氮墨。
- __unsafe_unretained:不保留此值,這么做可能不安全吐葵,因?yàn)榈鹊皆俅问褂米兞繒r(shí), 其對象可能已經(jīng)回收了规揪。
- __weak:不保留此值,但是變量可以安全使用温峭,因?yàn)槿绻到y(tǒng)把這個(gè)對象回收了, 那么變量也會(huì)自動(dòng)清空猛铅。
- __autoreleasing :把對象“按引用傳遞”(pass by reference)給方法時(shí),使用這個(gè)特殊的修飾符凤藏。此值在方法返回時(shí)自動(dòng)釋放奸忽。
ARC如何清理實(shí)例變量
剛才說過,ARC也負(fù)責(zé)對實(shí)例變量進(jìn)行內(nèi)存管理揖庄。要管理其內(nèi)存栗菜,ARC就必須在“回收分配給對象的內(nèi)存’(deallocate) 時(shí)生成必要的淸理代碼(cleanup code)。凡是具備強(qiáng)引用的變量蹄梢,都必須釋放疙筹,ARC會(huì)在dealloc方法中插入這些代碼。
ARC會(huì)借用0bjective-C++的一項(xiàng)特性來生成清理例程(cleanup routine)禁炒‰缜福回收Objective-C++對象時(shí),待回收的對象會(huì)調(diào)用所有C++對象的析構(gòu)函數(shù)(destructor)齐苛。編譯器如果發(fā)現(xiàn)某個(gè)對象里含有C++對象翘盖,就會(huì)生成名為.cxx_destruct
的方法。而ARC則借助此特性凹蜂,自動(dòng)在該方法中生成清理內(nèi)存所需的代碼馍驯。(而在生成的代碼中會(huì)自動(dòng)調(diào)用超類的dealloc方法)
不過,如果有非Objective-C的對象玛痊,比如Core Foundation中的對象或是由malloc()分配在堆中的內(nèi)存汰瘫,那么仍然需要清理。ARC環(huán)境下擂煞,dealloc方法可以像這樣來寫:
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
}
因?yàn)锳RC會(huì)自動(dòng)生成回收對象時(shí)所執(zhí)行的代碼混弥,所以通常無須再編寫dealloc方法。
覆寫內(nèi)存管理方法
由于開發(fā)者不可調(diào)用及覆寫retain、release蝗拿、autorelease
方法晾捏,所以ARC能夠優(yōu)化這些方法,使之不經(jīng)過Objective-C 的消息派發(fā)機(jī)制(參見第11條)哀托。優(yōu)化后的操作惦辛,直接調(diào)用隱藏在運(yùn)行期程序庫中的C函數(shù)。
要點(diǎn):
- 有ARC之后仓手,程序員就無須擔(dān)心內(nèi)存管理問題了胖齐。使用ARC來編程,可省去類中的許多“樣板代碼”嗽冒。
- ARC管理對象生命期的辦法基本上就是:在合適的地方插入“保留”及“釋放”操作呀伙。 在ARC環(huán)境下,變量的內(nèi)存管理語義可以通過修飾符指明添坊,而原來則需要手工執(zhí)行“保留”及“釋放”操作剿另。
- 由方法所返回的對象,其內(nèi)存管理語義總是通過方法名來體現(xiàn)ARC將此確定為開發(fā)者必須遵守的規(guī)則帅腌。
- ARC只負(fù)責(zé)管理Objective-C對象的內(nèi)存驰弄。尤其要注意:Core Foundation對象不歸ARC管理麻汰,開發(fā)者必須適時(shí)調(diào)用CFRetain/CFRelease速客。
第31條:在dealloc方法中只釋放引用并解除監(jiān)聽
對象在經(jīng)歷其生命期后,最終會(huì)為系統(tǒng)所回收五鲫,這時(shí)就要執(zhí)行dealloc方法了溺职。在每個(gè)對象的生命期內(nèi),此方法僅執(zhí)行一次位喂,也就是當(dāng)保留計(jì)數(shù)降為0的時(shí)候浪耘。
那么,應(yīng)該在dealloc方法中做些什么呢塑崖?主要就是釋放對象所擁有的引用七冲,也就是把所有Objective-C對象都釋放掉,ARC會(huì)通過自動(dòng)生成的.cxx_destruct
方法(參見第30條)规婆,在dealloc中為你自動(dòng)添加這些釋放代碼澜躺。對象所擁有的其他非Objective-C對象也要釋放。 比如Core Foundation對象就必須手動(dòng)釋放抒蚜,因?yàn)樗鼈兪怯杉僀的API所生成的掘鄙。
在dealloc方法中,通常還要做一件事嗡髓,那就是把原來配置過的觀測行為(observation behavior)都清理掉操漠。
在清理方法而非dealloc方法中清理資源還有個(gè)原因,就是系統(tǒng)并不保證每個(gè)創(chuàng)建出來的對象的dealloc都會(huì)執(zhí)行饿这。極個(gè)別情況下浊伙,當(dāng)應(yīng)用程序終止時(shí)撞秋,仍有對象處于存活狀態(tài),這 些對象沒有收到dealloc消息吧黄。
編寫dealloc方法時(shí)還需注意部服,不要在里面隨便調(diào)用其他方法。如果在這里所調(diào)用的方法又要異步執(zhí)行某些任務(wù)拗慨,那么等到那些任務(wù)執(zhí)行完畢時(shí)廓八,系統(tǒng)已經(jīng)把當(dāng)前這個(gè)待回收的對象徹底摧毀了。
在dealloc里也不要調(diào)用屬性的存取方法赵抢,因?yàn)橛腥丝赡軙?huì)覆寫這些方法剧蹂,并于其中做一些無法在回收階段安全執(zhí)行的操作。此外烦却,屬性可能正處于“鍵值觀測”(Key-Value Observation, KV0)機(jī)制的監(jiān)控之下宠叼,該屬性的觀察者(observer)可能會(huì)在屬性值改變時(shí) “保留”或使用這個(gè)即將回收的對象。
要點(diǎn):
- 在dealloc方法里其爵,應(yīng)該做的事情就是釋放指向其他對象的引用冒冬,并取消原來訂閱的“鍵值觀測”〇CVO)或NSNotificationCenter等通知,不要做其他事情摩渺。
- 如果對象持有文件描述符等系統(tǒng)資源简烤,那么應(yīng)該專門編寫一個(gè)方法來釋放此種資源。這樣的類要和其使用者約定:用完資源后必須調(diào)用close方法摇幻。
- 執(zhí)行異步任務(wù)的方法不應(yīng)在dealloc里調(diào)用横侦;只能在正常狀態(tài)下執(zhí)行的那些方法也不應(yīng)在dealloc里調(diào)用,因?yàn)榇藭r(shí)對象已處于正在冋收的狀態(tài)了绰姻。
第32條:編寫“異常安全代碼”時(shí)留意內(nèi)存管理問題
許多時(shí)下流行的編程語言都提供了 “異常"(exception)這一特性枉侧。純C中沒有異常,而 C++與Objective-C都支持異常狂芋。
@try {
//...
}
@catch (. . . ) {
//...
)
@finally {
//...
}
Objective-C的錯(cuò)誤模型表明榨馁,異常只應(yīng)在發(fā)生嚴(yán)重錯(cuò)誤后拋出(參見第21條),雖說如此帜矾,不過有時(shí)仍然需要編寫代碼來捕獲并處理異常翼虫。比如編碼中用到了第三方程序庫而此程序庫所拋出的異常又不受你控制時(shí),就需要捕獲及處理異常了黍特。此外蛙讥,有些系統(tǒng)庫也會(huì)用到異常,這使我們想起從前那個(gè)頻繁使用異常的年代灭衷。比如次慢,在使用“鍵值觀測”(KVO)功能時(shí),若想注銷一個(gè)尚未注冊的“觀察者”,便會(huì)拋出異常迫像。
發(fā)生異常時(shí)應(yīng)該如何管理內(nèi)存是個(gè)值得研究的問題劈愚。在try塊中,如果先保留了某個(gè)對象闻妓,然后在釋放它之前又拋出了異常菌羽,那么,除非catch塊能處理此問題由缆,否則對象所占內(nèi)存就將泄漏注祖。
在ARC下使用try導(dǎo)致的內(nèi)存泄露:雖說默認(rèn)狀況下未開啟,但ARC依然能生成這種安全處理異常所用的附加代碼均唉。-fobjc- arc-exceptions
這個(gè)編譯器標(biāo)志用來開啟此功能是晨。其默認(rèn)不開啟的原因是:在Objective-C代碼中,只有當(dāng)應(yīng)用程序必須因異常狀況而終止時(shí)才應(yīng)拋出異常(參見第21條)舔箭。因此罩缴,如果應(yīng)用程序即將終止,那么是否還會(huì)發(fā)生內(nèi)存泄漏就已經(jīng)無關(guān)緊要了层扶。在應(yīng)用程序必須立即終止的情況下箫章,還去添加安全處理異常所用的附加代碼是沒有意義的。
有種情況編譯器會(huì)自動(dòng)把-fobjc-arc-exceptions
標(biāo)志打開镜会,就是處于Objective-C++模式時(shí)檬寂。因?yàn)镃++處理異常所用的代碼與ARC實(shí)現(xiàn)的附加代碼類似,所以令A(yù)RC加入自己的代碼以安全處理異常稚叹,其性能損失并不太大焰薄。此外拿诸,由于C++頻繁使用異常扒袖,所以O(shè)bjective-C++程序員很可能也會(huì)使用異常。
如果手工管理引用計(jì)數(shù)亩码,而且必須捕獲異常季率,那么要設(shè)法保證所編代碼能把對象正確清理干凈;若使用ARC且必須捕獲異常描沟,則需打開編譯器的-fobjc-arc-exceptions
標(biāo)志飒泻。但最重要的是:在發(fā)現(xiàn)大量異常捕獲操作時(shí),應(yīng)考慮重構(gòu)代碼吏廉,用第21條所講的NSError式錯(cuò)誤信息傳遞法來取代異常泞遗。
要點(diǎn):
- 捕獲異常時(shí),一定要注意將try塊內(nèi)所創(chuàng)立的對象清理干凈席覆。
- 在默認(rèn)情況下史辙,ARC不生成安全處理異常所需的清理代碼。開啟編譯器標(biāo)志后,可生成這種代碼聊倔,不過會(huì)導(dǎo)致應(yīng)用程序變大晦毙,而且會(huì)降低運(yùn)行效率。
第33條:以弱引用避免保留環(huán)
由于Objective-C內(nèi)存管理模型使用引用計(jì)數(shù)架構(gòu)耙蔑,所以這種情況通常會(huì)泄漏內(nèi)存见妒, 避免保留環(huán)的最佳方式就是弱引用。這種引用經(jīng)常用來表示“非擁有關(guān)系”(ruHiowning relationship)甸陌。
用__unsafe_unretained
修飾的屬性特質(zhì)须揣,其語義同assign
特質(zhì)等價(jià)(參見第6條)。然而钱豁,assign
通常只用于“整體類型”(int返敬、float、結(jié)構(gòu)體等)寥院,__unsafe_unretained
則多用于對象類型劲赠。 這個(gè)詞本身就表明其所修飾的屬性可能無法安全使用(unsafe)。
Objective-C中還有一項(xiàng)與ARC相伴的運(yùn)行期特性秸谢,可以令開發(fā)者安全使用弱引用: 這就是weak
屬性特質(zhì)凛澎,它與__unsafe_unretained
的作用完全相同。然而估蹄,只要系統(tǒng)把屬性回收塑煎,屬性值就會(huì)自動(dòng)設(shè)為nil。當(dāng)引用移除后臭蚁,__unsafe_unretained
屬性仍然指向那個(gè)已經(jīng)回收的實(shí)例最铁,而weak
屬性則指向nil。因此使用weak
而非__unsafe_unretained
引用可以令代碼更安全垮兑。
要點(diǎn):
- 將某些引用設(shè)為
weak
,可避免出現(xiàn)“保留環(huán)”冷尉。weak
引用可以自動(dòng)清空,也可以不自動(dòng)清空系枪。自動(dòng)清空(autonilling)是隨著ARC而引入的新特性雀哨,由運(yùn)行期系統(tǒng)來實(shí)現(xiàn)。在具備自動(dòng)清空功能的弱引用上私爷,可以隨意讀取其數(shù)據(jù)雾棺,因?yàn)檫@種引用不會(huì)指向已經(jīng)回收過的對象。
第34條:以“自動(dòng)釋放池塊”降低內(nèi)存峰值
Objective-C對象的生命期取決于其引用計(jì)數(shù)(參見第29條)衬浑。在Objective-C的引用計(jì)數(shù)架構(gòu)中捌浩,有一項(xiàng)特性叫做“自動(dòng)釋放池”(autoreleasepool)。釋放對象有兩種方式:一種是調(diào)用release方法工秩,使其保留計(jì)數(shù)立即遞減尸饺;另一種是調(diào)用autorelease方法宏榕,將其加入“自動(dòng)釋放池”中。自動(dòng)釋放池用于存放那些需要在稍后某個(gè)時(shí)刻釋放的對象侵佃。清空(drain)自動(dòng) 釋放池時(shí)麻昼,系統(tǒng)會(huì)向其中的對象發(fā)送release消息。
創(chuàng)建自動(dòng)釋放池所用語法如下:
@autoreleasepool {
//...
}
系統(tǒng)會(huì)自動(dòng)創(chuàng)建一些線程馋辈,比如說主線程或是“大中樞 派發(fā)’(Grand Center Dispatch抚芦,GCD)機(jī)制中的線程,這些線程默認(rèn)都有自動(dòng)釋放池迈螟,每次執(zhí)行“事件循環(huán)"(event loop)時(shí)叉抡,就會(huì)將其清空。因此答毫,不需要自己來創(chuàng)建“自動(dòng)釋放池塊”褥民。 通常只有一個(gè)地方需要?jiǎng)?chuàng)建自動(dòng)釋放池,那就是在main函數(shù)里洗搂,我們用自動(dòng)釋放池來包裹應(yīng)用程序的主入口點(diǎn)(main application entry point)消返。這個(gè)池可以理解成最外圍捕捉全部自動(dòng)釋放對象所用的池。
自動(dòng)釋放池可以嵌套耘拇,將自動(dòng)釋放池嵌套用的好處是撵颊,可以借此控制應(yīng)用程序的內(nèi)存峰值,使其不致過高惫叛。
考慮下面這段代碼:
for (int i = 0; i <100000; i++) {
[self doSomethingWithInt:i];
}
如果“doSomethingWithInt:”方法要?jiǎng)?chuàng)建臨時(shí)對象倡勇,那么這些對象很可能會(huì)放在自動(dòng)釋放池里。比方說嘉涌,它們可能是一些臨時(shí)字符串妻熊。但是,即便這些對象在調(diào)用完方法之后就不再使用了仑最,它們也依然處于存活狀態(tài)扔役,因?yàn)槟壳斑€在自動(dòng)釋放池里,等待系統(tǒng)稍后將其釋放并回收词身。然而厅目,自動(dòng)釋放池要等線程執(zhí)行下一次事件循環(huán)時(shí)才會(huì)清空番枚。這就意味著在執(zhí)行for循環(huán)時(shí)法严,會(huì)持續(xù)有新對象創(chuàng)建出來,并加入自動(dòng)釋放池中葫笼。所有這種對象都要等for循環(huán)執(zhí)行完才會(huì)釋放深啤。這樣一來,在執(zhí)行for循環(huán)時(shí)路星,應(yīng)用程序所占內(nèi)存量就會(huì)持續(xù)上漲溯街,而等到所有臨時(shí)對象都釋放后诱桂,內(nèi)存用量又會(huì)突然下降。
增加一個(gè)自動(dòng)釋放池即可解決此問題呈昔。如果把循環(huán)內(nèi)的代碼包裹在“自動(dòng)釋放池塊”中挥等,那么在循環(huán)中自動(dòng)釋放的對象就會(huì)放在這個(gè)池,而不是線程的主池里面堤尾。
for (int i = 0; i <100000; i++) {
@autoreleasepool {
[self doSomethingWithlnt:i];
}
}
加上這個(gè)自動(dòng)釋放池之后肝劲,應(yīng)用程序在執(zhí)行循環(huán)時(shí)的內(nèi)存峰值就會(huì)降低,不再像原來那么高了郭宝。
內(nèi)存峰值(high-memory waterline)是指應(yīng)用程序在某個(gè)特定時(shí)段內(nèi)的最大內(nèi)存用量 (highest memory footprint)辞槐。新增的自動(dòng)釋放池塊可以減少這個(gè)峰值,因?yàn)橄到y(tǒng)會(huì)在塊的末尾把某些對象回收掉粘室。而剛才提到的那種臨時(shí)對象榄檬,就在回收之列。
自動(dòng)釋放池機(jī)制就像“椣瓮常”(stack) —樣鹿榜。系統(tǒng)創(chuàng)建好自動(dòng)釋放池之后,就將其推入棧中, 而清空自動(dòng)釋放池锦爵,則相當(dāng)于將其從棧中彈出犬缨。在對象上執(zhí)行自動(dòng)釋放操作,就等于將其放入棧頂?shù)哪莻€(gè)池里棉浸。
是否應(yīng)該用池來優(yōu)化效率怀薛,完全取決于具體的應(yīng)用程序。首先得監(jiān)控內(nèi)存用量迷郑,判斷其中有沒有需要解決的問題枝恋,如果沒完成這一步,那就別急著優(yōu)化嗡害。盡管自動(dòng)釋放池塊的開銷不太大焚碌,但畢竟還是有的,所以盡量不要建立額外的自動(dòng)釋放池霸妹。
如果在ARC出現(xiàn)之前就寫過Objective-C程序十电,那么可能還記得有種老式寫法,就是使用NSAutoreleasePool對象叹螟。這個(gè)特殊的對象與普通對象不同鹃骂,它專門用來表示自動(dòng)釋放池,就像新語法中的自動(dòng)釋放池塊一樣罢绽。但是這種寫法并不會(huì)在每次執(zhí)行for循環(huán)時(shí)都清空池畏线,此對象更為“重量級(jí)"(heavyweight),通常用來創(chuàng)建那種偶爾需要清空的池良价。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool drain];
現(xiàn)在不需要再這樣寫代碼了寝殴。采用隨著ARC所引入的新語法蒿叠,可以創(chuàng)建出更為“輕量級(jí)”(light weight)的自動(dòng)釋放池。原來所寫的代碼可能會(huì)每執(zhí)行n次循環(huán)清空一次自動(dòng)釋放池蚣常,現(xiàn)在可以改用自動(dòng)釋放池塊把for循環(huán)中的語句包起來市咽,這樣的話,每次執(zhí)行循環(huán)時(shí)都會(huì)建立并清空自動(dòng)釋放池抵蚊。
@autordeasepool語法還有個(gè)好處:每個(gè)自動(dòng)釋放池均有其范圍魂务,可以避免無意間誤用了那些在清空池后已為系統(tǒng)所回收的對象。
要點(diǎn):
- 自動(dòng)釋放池排布在棧中泌射,對象收到autorelease消息后粘姜,系統(tǒng)將其放入最頂端的池里。
- 合理運(yùn)用自動(dòng)釋放池熔酷,可降低應(yīng)用程序的內(nèi)存峰值孤紧。
- @autoreleasepool這種新式寫法能創(chuàng)建出更為輕便的自動(dòng)釋放池。
第35條:用“僵尸對象”調(diào)試內(nèi)存管理問題
調(diào)試內(nèi)存管理問題很令人頭疼拒秘。向業(yè)已回收的對象發(fā)送消息是不安全的号显。 這么做有時(shí)崩潰,有時(shí)不崩潰躺酒。崩潰與否押蚤,完全取決于對象所占內(nèi)存有沒有為其他內(nèi)容所覆寫。
所幸Cocoa提供了 “僵尸對象”(Zombie Object)這個(gè)非常方便的功能羹应。啟用這項(xiàng)調(diào)試功能之后揽碘,運(yùn)行期系統(tǒng)會(huì)把所有已經(jīng)回收的實(shí)例轉(zhuǎn)化成特殊的“僵尸對象”,而不會(huì)真正回收它們园匹。僵尸對象收到消息后雳刺,會(huì)拋出異常,其中準(zhǔn)確說明了發(fā)送過來的消息裸违,并描述了回收之前的那個(gè)對象掖桦。僵尸對象是調(diào)試內(nèi)存管理問題的最佳方式。將NSZombieEnabled環(huán)境變量設(shè)為YES供汛,即可開啟此功能枪汪。
僵尸對象的工作原理:系統(tǒng)在即將回收對象時(shí),如果發(fā)現(xiàn)通過環(huán)境變量啟用了僵尸對象功能怔昨,那么還將執(zhí)行一個(gè)附加步驟雀久,這一步就是把即將要變成僵尸對象所屬的類,由EOCClass變?yōu)開NSZombie_EOCClass朱监。但是岸啡,這個(gè)新類是從哪里來的呢?_NSZombie_EOCClass實(shí)際上是在運(yùn)行期生成的赫编,當(dāng)首次碰到EOCClass類的對象要變成僵尸對象時(shí)巡蘸,就會(huì)創(chuàng)建這么一個(gè)類。 創(chuàng)建過程中用到了運(yùn)行期程序庫里的函數(shù)擂送。
系統(tǒng)根據(jù)需要?jiǎng)?chuàng)建出僵尸類之后悦荒,僵尸類又把待回收的對象轉(zhuǎn)化成僵尸對象。這個(gè)過程其實(shí)就是NSObject的dealloc
方法所做的事嘹吨。運(yùn)行期系統(tǒng)如果發(fā)現(xiàn)NSZombieEnabled
環(huán)境變量已設(shè)置搬味,那么就把dealloc
方法“調(diào)配”(swizzle,參見第13條) 成一個(gè)會(huì)執(zhí)行上述代碼的版本。執(zhí)行到程序末尾時(shí)蟀拷,對象所屬的類已經(jīng)變?yōu)?code>_NSZombie_ OriginalClass了碰纬,其中OriginalClass
指的是原類名。
僵尸類的作用會(huì)在消息轉(zhuǎn)發(fā)例程(參見第12條)中體現(xiàn)出來问芬。_NSZombie_類
(以及所有從該類拷貝出來的類)并未實(shí)現(xiàn)任何方法悦析。此類沒有超類,因此和NSObject—樣此衅,也是個(gè) “根類”强戴,該類只有一個(gè)實(shí)例變量,叫做isa挡鞍,所有Objective-C的根類都必須有此變量骑歹。由于這個(gè)輕量級(jí)的類沒有實(shí)現(xiàn)任何方法,所以發(fā)給它的全部消息都要經(jīng)過“完整的消息轉(zhuǎn)發(fā)機(jī)制” (full forwarding mechanism,參見第 12 條)墨微。
在完整的消息轉(zhuǎn)發(fā)機(jī)制中道媚,forwarding是核心,調(diào)試程序時(shí)翘县,大家可能在椝ニ觯回溯消息里看見過這個(gè)函數(shù)。它首先要做的事情就包括檢査接收消息的對象所屬的類名炼蹦。若名稱前綴為NSZombie羡宙,則表明消息接收者是僵尸對象,需要特殊處理掐隐。此時(shí)會(huì)打印一條消息狗热,其中指明了僵尸對象所收到的消息及原來所屬的類,然后應(yīng)用程序就終止了虑省。在僵尸類名中嵌入原始類名的好處匿刮,這時(shí)就可以看出來了。只要把_NSZombie_
從僵尸類名的開頭拿掉探颈,剩下的就是原始類名熟丸。
要點(diǎn):
- 系統(tǒng)在回收對象時(shí),可以不將其真的回收伪节,而是把它轉(zhuǎn)化為僵尸對象光羞。通過環(huán)境變量NSZombieEnabled可開啟此功能绩鸣。
- 系統(tǒng)會(huì)修改對象的isa指針,令其指向特殊的僵尸類纱兑,從而使該對象變?yōu)榻┦瑢ο蟆?儸尸類能夠響應(yīng)所有的選擇子呀闻,響應(yīng)方式為:打印一條包含消息內(nèi)容及其接收者的消息,然后終止應(yīng)用程序潜慎。
第36條:不要使用retainCount
此方法之所以無用捡多,其首要原因在于:它所返回的保留計(jì)數(shù)只是某個(gè)給定時(shí)間點(diǎn)上的值。 該方法并未考慮到系統(tǒng)會(huì)稍后把自動(dòng)釋放池清空(參見第34條)铐炫,因而不會(huì)將后續(xù)的釋放操作從返回值里減去垒手,這樣的話,此值就未必能真實(shí)反映實(shí)際的保留計(jì)數(shù)了倒信。
我們可能還是想看一看保留計(jì)數(shù)的具體值科贬,然而看過之后你就會(huì)覺得奇怪了,它的值為何那么大呢堤结?比方說唆迁,有下面這段代碼:
NSString *string = @"Some string";
NSLog (@"string retainCount = %lu", [string retainCount]);
NSNumber *numberl = @1;
NSLog (@"numberl retainCount = %lu", [numberl retainCount]);
NSNumber *numberF = @3.141f;
NSLog (@"numberF retainCount = %lu", [numberF retainCount]);
在64位MacOSX10.8.2系統(tǒng)中,用Clang4.1編譯后竞穷,這段代碼輸出的消息如下:
string retainCount = 18446744073709551615
numberl retainCount = 9223372036854775807
numberF retainCount = 1
第一個(gè)對象的保留計(jì)數(shù)是2(64)-1唐责,第二個(gè)對象的保留計(jì)數(shù)是2(63)-1。由于二者皆為“單例對象"(singleton object)瘾带,所以其保留計(jì)數(shù)都很大鼠哥。系統(tǒng)會(huì)盡可能把NSString實(shí)現(xiàn)成單例對象。 如果字符串像本例所舉的這樣看政,是個(gè)編譯期常量(compile-time constant)朴恳,那么就可以這樣來實(shí)現(xiàn)了。在這種情況下允蚣,編譯器會(huì)把NSString對象所表示的數(shù)據(jù)放到應(yīng)用程序的二進(jìn)制文件里于颖,這樣的話,運(yùn)行程序時(shí)就可以直接用了嚷兔,無須再創(chuàng)建NSString對象森渐。NSNumber也類似,它使用了一種叫做“標(biāo)簽指針”(tagged pointer)的概念來標(biāo)注特定類型的數(shù)值冒晰。這種做法不使用NSNumber
對象同衣,而是把與數(shù)值有關(guān)的全部消息都放在指針值里面。運(yùn)行期系統(tǒng)會(huì)在消息派發(fā)(參見第11條)期間檢測到這種標(biāo)簽指針壶运,并對它執(zhí)行相應(yīng)操作耐齐,使其行為看上去和真正的NSNumbei *
對象一樣。這種優(yōu)化只在某些場合使用,比如范例中的浮點(diǎn)數(shù)對象就沒有優(yōu)化埠况,所以其保留計(jì)數(shù)就是1耸携。
要點(diǎn):
- 對象的保留計(jì)數(shù)看似有用,實(shí)則不然询枚,因?yàn)槿魏谓o定時(shí)間點(diǎn)上的“絕對保留計(jì)數(shù)” (absolute retain count)都無法反映對象生命期的全貌违帆。
- 引入ARC之后浙巫,retainCount方法就正式廢止了金蜀,在ARC下調(diào)用該方法會(huì)導(dǎo)致編譯器報(bào)錯(cuò)。