第五章 內(nèi)存管理
內(nèi)存管理對一門語言來說異常的重要,掌握一門語言的內(nèi)存管理是很必要的。
29. 理解引用計數(shù)
OC采用引用計數(shù)進行內(nèi)存管理,就是說,每個對象有一個計數(shù)器,如果想讓對象不被內(nèi)存管理回收,就要保證對象的引用計數(shù)大于等于1,如果引用計數(shù)變?yōu)?則表示這個對象不再被需要,其占用的內(nèi)存就可以回收了。
經(jīng)過不斷的發(fā)展,現(xiàn)在OC的引用計數(shù)已經(jīng)不用手動管理,而是由編譯器幫助我們執(zhí)行(自動引用計數(shù)ARC),不過還是要了解引用計數(shù),在某些情況下對于寫代碼還是很有用的。
下面只簡單介紹一下關(guān)于引用計數(shù)的一些東西,不做過深的解釋,首先對象在創(chuàng)建出來的時候引用計數(shù)為1,OC中直接令引用計數(shù)增加用retain方法,領(lǐng)引用計數(shù)減少用release和autorelease,autorelease調(diào)用后不會立即將引用計數(shù)減1,而是將對象放入“自動釋放池”,稍后在做處理。在非ARC下,可以通過retainCount方法查看引用計數(shù)是幾,但是這種方法是不推薦的,因為這個方法返回的數(shù)字可能和實際數(shù)字不符,后面介紹為什么。前面介紹的這幾個方法在ARC下使用會報錯的,如果想體驗一下手動管理可以通過下圖步驟修改編譯環(huán)境

在一個應(yīng)用程序中會同時存在許多對象,這些對象之間也是可以存在持有關(guān)系的,并且可以多個對象同時持有一個對象,這就導(dǎo)致一個對象會因為持有者的變化而導(dǎo)致引用計數(shù)不斷變化,當引用計數(shù)變?yōu)?,這個對象就可以摧毀了。大家可能會有一個疑問,對象之間是持有關(guān)系存在的,那么最頂端的對象是誰,在iOS中這個對象是UIApplication對象,這個對象是應(yīng)用程序啟動時創(chuàng)建的單例。另外如果不是手動將對象的內(nèi)存置為空的話,引用計數(shù)變?yōu)?的對象也不一定被摧毀,只不過這個對象占用的內(nèi)存被放回“可用內(nèi)存池”,當這部分內(nèi)存沒有被覆寫的時候,這個對象仍然有效,這時候可能會出現(xiàn)我們意想不到的bug,所以一般情況下如果確定不用一個對象的話,要對這個對象指針進行處理(將其置為空)。
下面介紹幾個和內(nèi)存管理息息相關(guān)的知識:
1.屬性存取方法中的內(nèi)存管理
一般情況下實現(xiàn)一個對象對一個對象的持有都是通過訪問屬性來實現(xiàn)的,這時候會用到相關(guān)屬性的設(shè)置方法和獲取方法,這時候?qū)傩缘膬?nèi)存管理語義的關(guān)鍵字就顯得格外重要,例如strong關(guān)系,就是強引用,如果一個屬性被strong引用,則其設(shè)置方法可能會是下面這樣:
- (void)setFoo:(id)foo{
[foo retain];
[_foo release];
_foo = foo;
}
該方法會保留新值釋放舊值,當然在ARC下是不允許這樣寫的,我們要理解屬性內(nèi)存管理語義的重要性。
2.自動釋放池
自動釋放池的最大用處就是能延長對象生命期,尤其是在方法返回對象時候更應(yīng)該用他,通過autorelease的調(diào)用,對象不會被立即釋放,而是等到下次“事件循環(huán)”才執(zhí)行引用計數(shù)遞減工作,在這期間就會留給我們足夠長的時間對對象進行相關(guān)的操作,相比較于release不會那么強硬,但是各有各的好處。另外自動釋放池也會有降低內(nèi)存峰值的作用,后面介紹。
3.保留環(huán)
就是循環(huán)引用,就是兩個對象互相持有(也可能多個對象之間成環(huán)式的持有),這就導(dǎo)致所有對象的引用計數(shù)為1,最后誰也不能釋放,對于不同情況下產(chǎn)生的保留環(huán)會有不同的處理方法,后面會介紹一種“弱引用”方法解決,也可以從外界命令環(huán)中的一個對象不再對另外一個對象進行持有。
30. 以ARC簡化引用計數(shù)
ARC的出現(xiàn)時程序員的福音,因為再也不用為考慮引用計數(shù)發(fā)愁了,所有這些應(yīng)該增加或減少引用計數(shù)的地方都由編譯器的“靜態(tài)分析器”幫我們解決,所以這也是為什么我們不能再ARC下調(diào)用reatin、release、autorelease、dealloc方法的原因,因為手動調(diào)用這些方法會干擾編譯器的判斷。另外,編譯器的引用計數(shù)不是通過普通的消息派發(fā)機制,而是通過更底層的方法,這樣更能提高代碼的效率。我們要知道一點,ARC下還是有引用計數(shù)機制,只不過這個工作被編譯器做了。
使用ARC時要遵循方法命名規(guī)則,因為編譯器在分析法代碼的時候會根據(jù)方法名分析代碼,例如,若方法名以alloc、new、copy、mutableCopy開頭,則其返回的對象歸調(diào)用者所有,那么這部分代碼就要負責(zé)釋放方法所返回的對象。若方法名不以這四個詞開頭,則返回對象不歸調(diào)用者所有,這種個情況下返回兌現(xiàn)會制自動釋放,不過現(xiàn)在這些工作都由ARC幫我們做了。ARC還會對操作約減,將retain和release相互抵消,這樣都會優(yōu)化代碼,節(jié)省內(nèi)存。另外,ARC還有運行期組件,這些操作都會大大的優(yōu)化我們的程序,具體的不過多介紹,至于這些優(yōu)化的詳細內(nèi)部實現(xiàn),只有編譯器的作者知道。
ARC也會處理局部變量和實例變量的內(nèi)存管理,ARC會以一種安全的方式來設(shè)置一個變量,他總是遵循先保留新值,再釋放舊值,最后設(shè)置實例變量。在應(yīng)用程序中,可以用下面的修飾符來改變局部變量與實例變量的語義:
__strong: 默認語義,保留此值
__unsafe_unretained: 不保留此值(這么做不安全,因為再次使用的時候?qū)ο罂赡芤呀?jīng)被回收)
__weak: 不保留此值,但是變量可以安全使用,在某些情況下這個修飾符很有用
__autoreleasing: 把對象“按引用傳遞”給方法時,此修飾符會讓此值在方法返回時自動釋放
block會自動保留其捕獲的全部對象,如果這些對象中有一個對象又保留了block本身,那么就會導(dǎo)致保留環(huán),這時候就可以用__weak修飾局部變量來打破這種保留環(huán),避免循環(huán)引用。
ARC可以自動的幫助我們清理實例變量,并且ARC的清理會比我們手動在dealloc方法中release更高效,ARC會用C++對象的析構(gòu)函數(shù),不過有一些非OC對象在調(diào)用的時候就要手動清理,這些框架都有對應(yīng)的清理方法,例如CoreFoundation框架中的對象或是由malloc()分配在對中的內(nèi)存,這時候需要我們在dealloc中調(diào)用對應(yīng)的方法手動釋放。如下
- (void)dealloc{
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
}
另外,由于ARC的自動引用計數(shù)機制,內(nèi)存管理方法是不可以覆寫的,我們要相信ARC會給我們更好的代碼優(yōu)化。
31. 在dealloc方法中只釋放引用并解除監(jiān)聽
dealloc是一個對象生命周期中執(zhí)行的最后一個方法,這個方法執(zhí)行后,對象將不復(fù)存在,所以,在這個方法中我們就要考慮清除所有關(guān)于這個對象的痕跡,前面已經(jīng)提到過,ARC下編輯器會自動在這個方法中添加適當?shù)姆椒?,解除對象的引用,對于不屬于OC的對象,也應(yīng)該在這個方法中釋放,另外dealloc方法還有一個重要的用處就是把原來配置的觀測行為都清理掉,最典型的就是移除通知的觀察者:
- (void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
我們要謹記,不能再別的方法中調(diào)用dealloc方法,調(diào)用后后續(xù)的代碼都將失效,另外我們也要遵循一個規(guī)則,就是在dealloc中不要調(diào)用其他的和釋放無關(guān)的方法。
有時候,一些開銷較大或系統(tǒng)稀缺支援的釋放需要我們自行設(shè)定釋放方法在合適的時候調(diào)用,例如文件描述符、套接字、大塊內(nèi)存等,這些東西都需要我們自行編寫釋放方法,至于有的時候可能在程序運行到一半的時候就退出了,這個時候還沒有來的及走dealloc方法,大家可以不用關(guān)心這個問題,程序退出,程序中的對象也會銷毀,另外我們可以通過UIApplicationDelegate中的applicationWillTerminate方法,在程序退出之前做我們想要做的事情。
32. 編寫“異常安全代碼”時留意內(nèi)存管理問題
OC和C++都支持異常,并且兩門語言的異常是互通的,也就是說從一門語言里拋出的異常能用另外一門語言編輯的異常處理程序來捕獲。OC中,異常的拋出應(yīng)該是在極其錯誤的情況下,前面的錯誤模型已經(jīng)介紹,但是有時候第三方庫中也會用到異常,這時候就需要我們進行處理。
通常我們使用事務(wù)來處理一段異常,在非ARC情況下可以做如下處理:
EOCSomeClass *object;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
} @catch (NSException *exception) {
NSLog(@"有一個異常要處理");
} @finally {
[object release];
}
在非ARC的情況下我們可以通過@finally塊,把最后的引用釋放,但是在ARC下怎么處理呢?下面是ARC下相同功能的代碼:
EOCSomeClass *object;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
} @catch (NSException *exception) {
NSLog(@"有一個異常要處理");
}
這時候不能寫release方法,所以@finally塊可以去掉,但是拋出異常的時候引用沒有被釋放啊,我們可能以為編輯器會給我們做處理,其實不是,默認情況下編輯器是不會給我們做處理的,我們需要通過-fobjc-arc-exceptions這個編譯標志來開啟這個功能,這時候編譯器會幫我們生成安全處理異常的附加代碼(附加代碼的代碼量還是很大的),這個功能默認不開啟有兩個原因,第一個就是前面說的附加碼代碼量很大,另外一個原因就是系統(tǒng)任務(wù)如果拋出異常就是很嚴重的錯誤,可以直接終止程序了,所以系統(tǒng)默認是不開啟的。另外說一點,如果編譯器發(fā)現(xiàn)我們編寫的是Objective-C++代碼,會默認開啟這個狀態(tài),因為C++中會頻繁使用異常。
33. 以弱引用避免保留環(huán)
保留環(huán)就是我們常說的循環(huán)引用,簡單的保留環(huán)是兩個對象之間互相引用,復(fù)雜的保留環(huán)可能是三個或者更多對象之間的呈鏈式的循環(huán)引用,前面已經(jīng)介紹了,可以在聲明名屬性的時候使用assign、unsafe_unretained、weak三個修飾符解決保留環(huán)的問題,下面介紹一下這三個修飾符的不同點。
assign: 通常只用于整形類型(int、float、結(jié)構(gòu)體等)
unsafe_unretained: 通常用于對象
weak:通常用于對象
舉個例子介紹unsafe_unretained和weak區(qū)別,現(xiàn)系統(tǒng)持有兩個對象,對象A和對象B,并且對象A和對象B互相持有,當系統(tǒng)銷毀對象A之后,如果是用weak修飾,那么對象B指向?qū)ο驛的引用就不存在了,這時候?qū)ο驜指向nil,如果是用unsafe_unretained修飾,在系統(tǒng)銷毀對象A之后,對象B指向?qū)ο驛的引用依然存在,由此可以看出unsafe_unretained是很不安全的,所以可以發(fā)現(xiàn),基本我們看不到unsafe_unretained修飾的屬性,一般情況下都是使用weak修飾。
34. 以“自動釋放池塊”降低內(nèi)存峰值
OC中的自動釋放池是引用計數(shù)機制中的一項特性,前面已經(jīng)介紹,自動釋放池中對象的釋放是等到一次循環(huán)結(jié)束,而不是像release一樣馬上釋放,這樣就留下充足的時間處理這些對象,創(chuàng)建自動釋放池用@autoreleasepool,不過在一般情況下我們無需擔(dān)心自動釋放池的創(chuàng)建問題,因為系統(tǒng)會在他認為需要的地方自動創(chuàng)建一些自動釋放池,例如主線程或者GCD機制中的線程。下面介紹幾個和自動釋放池有關(guān)的知識點,大家會發(fā)現(xiàn),我們創(chuàng)建好一個工程后,main函數(shù)總是這樣寫的:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
其實從技術(shù)角度看,這個自動釋放池可有可無,因為等到要執(zhí)行自動釋放操作的時候已經(jīng)是程序的結(jié)尾了,那么為什么還要加呢,如果不加的話UIApplicationMain函數(shù)所自動釋放的那些對象,就沒有自動釋放池容納了,所以,這個釋放池可以理解成最外圍捕捉全部自動釋放對象所用的池。
再看下面的代碼:
@autoreleasepool {
// 做一些操作
@autoreleasepool {
// 做一些操作
}
}
從上面可以發(fā)現(xiàn)自動釋放池可以嵌套使用,這樣做最大的好處是可以不用等到最外面的釋放池執(zhí)行釋放操作,內(nèi)部的釋放池可以先執(zhí)行釋放操作,自動釋放池的范圍的由大括號決定的,這樣做可以降低內(nèi)存峰值,再看下面一個例子:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
// 執(zhí)行創(chuàng)建對象操作
}
}
從上面可以看出這個制動釋放池的作用就是不用等到循環(huán)結(jié)束再執(zhí)行釋放操作,如果等到循環(huán)結(jié)束的時候再執(zhí)行釋放操作的話,內(nèi)存中創(chuàng)建的對象就太多了,這種情況在我們從數(shù)據(jù)庫中提取數(shù)據(jù)的時候會經(jīng)常發(fā)生,對于數(shù)據(jù)庫中的數(shù)據(jù)我們是不知道多少的,如果我們一下查詢出過多的數(shù)據(jù),這個時候如果要把數(shù)據(jù)轉(zhuǎn)換成對象就會出現(xiàn)上面的問題,這個時候自動釋放池就派上用場了。
是否進行自動釋放池優(yōu)化,完全是根據(jù)應(yīng)用本身決定的,在ARC出現(xiàn)之前,有一種老式的制動釋放池寫法,就是NSAutoreleasePool對象,這里不再介紹,感興趣可以自行查閱,總的來說這個對象更加重量級,而現(xiàn)在的@autoreleasePool更加輕便好用。
35. 用“僵尸對象”調(diào)試內(nèi)存管理問題
如果向被回收的對象發(fā)送消息有時會造成崩潰,但是這種崩潰又不是一定的,為什么會出現(xiàn)這種情況呢,前面已經(jīng)介紹,被回收的對象占用的內(nèi)存沒有清空,只不過這一部分內(nèi)存放入可用內(nèi)存區(qū)域,如果還沒有被覆寫,那么對象仍然是存在的,這就是為什么有時候崩潰有時候不崩潰的原因,想要排查很困難,這時候就輪到僵尸對象上場了,當開啟僵尸模式后,運行期系統(tǒng)不會把回收的對象放到可用內(nèi)存區(qū)域,而是將回收對象變成僵尸對象,這樣所有的信息都被僵尸對象接收,系統(tǒng)的設(shè)定是僵尸對象在收到消息后,會拋出異常,并在拋出的信息中準確的描述發(fā)送過來的信息,并且描述了是哪個對象變成了現(xiàn)在的僵尸對象,這就是大致的僵尸模式的工作模式,下面介紹一下如何開啟僵尸模式

在僵尸模式拋出的異常信息中就有相關(guān)的對象的信息,例如有一個類EOCClass,在僵尸模式中拋出異常的時候我們會看到_NSZombie_EOCClass,從拋出的異常信息就可以追查出問題所在,有助于解決問題。
ARC模式下,由于內(nèi)存不用手動管理,會很少出現(xiàn)僵尸對象,但容易產(chǎn)生上述問題的場景主要有兩個:一是方法內(nèi)的局部對象,在其他方法使用; 二是異步過程的回調(diào),比如網(wǎng)絡(luò)操作。
36. 不要使用retainCount
前面已經(jīng)多次強調(diào)過這個方法的不可靠性,這里再強調(diào)一下,首先,在ARC下,只要調(diào)用這個方法編譯器就會報錯,在非ARC模式下,調(diào)用此方法也有很多風(fēng)險,這里只說一點,retainCount返回的是某個時間點上的絕對保留計數(shù),這一時間點無法反應(yīng)生命周期全貌,并且OC中還有自動釋放池機制,所以無論在哪種模式下都不應(yīng)該使用這個方法。
總的來說,ARC的出現(xiàn)大大的簡化了內(nèi)存管理,但是ARC不代表不會出現(xiàn)內(nèi)存泄漏等問題,在寫代碼時還是要很細心,另外書中有許多例子沒有介紹,例如僵尸模式下系統(tǒng)是如何把一個對象變成僵尸對象的等,感興趣的同學(xué)可以自行查閱。