Effective Objective-C 2.0 讀書筆記五

第五章 內(nèi)存管理

內(nèi)存管理對(duì)一門語言來說異常的重要,掌握一門語言的內(nèi)存管理是很必要的。

29. 理解引用計(jì)數(shù)

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



在一個(gè)應(yīng)用程序中會(huì)同時(shí)存在許多對(duì)象,這些對(duì)象之間也是可以存在持有關(guān)系的,并且可以多個(gè)對(duì)象同時(shí)持有一個(gè)對(duì)象,這就導(dǎo)致一個(gè)對(duì)象會(huì)因?yàn)槌钟姓叩淖兓鴮?dǎo)致引用計(jì)數(shù)不斷變化,當(dāng)引用計(jì)數(shù)變?yōu)?,這個(gè)對(duì)象就可以摧毀了。大家可能會(huì)有一個(gè)疑問,對(duì)象之間是持有關(guān)系存在的,那么最頂端的對(duì)象是誰,在iOS中這個(gè)對(duì)象是UIApplication對(duì)象,這個(gè)對(duì)象是應(yīng)用程序啟動(dòng)時(shí)創(chuàng)建的單例。另外如果不是手動(dòng)將對(duì)象的內(nèi)存置為空的話,引用計(jì)數(shù)變?yōu)?的對(duì)象也不一定被摧毀,只不過這個(gè)對(duì)象占用的內(nèi)存被放回“可用內(nèi)存池”,當(dāng)這部分內(nèi)存沒有被覆寫的時(shí)候,這個(gè)對(duì)象仍然有效,這時(shí)候可能會(huì)出現(xiàn)我們意想不到的bug,所以一般情況下如果確定不用一個(gè)對(duì)象的話,要對(duì)這個(gè)對(duì)象指針進(jìn)行處理(將其置為空)。
下面介紹幾個(gè)和內(nèi)存管理息息相關(guān)的知識(shí):
1.屬性存取方法中的內(nèi)存管理
一般情況下實(shí)現(xiàn)一個(gè)對(duì)象對(duì)一個(gè)對(duì)象的持有都是通過訪問屬性來實(shí)現(xiàn)的,這時(shí)候會(huì)用到相關(guān)屬性的設(shè)置方法和獲取方法,這時(shí)候?qū)傩缘膬?nèi)存管理語義的關(guān)鍵字就顯得格外重要,例如strong關(guān)系,就是強(qiáng)引用,如果一個(gè)屬性被strong引用,則其設(shè)置方法可能會(huì)是下面這樣:

- (void)setFoo:(id)foo{
    [foo retain];
    [_foo release];
    _foo = foo;
}

該方法會(huì)保留新值釋放舊值,當(dāng)然在ARC下是不允許這樣寫的,我們要理解屬性內(nèi)存管理語義的重要性。
2.自動(dòng)釋放池
自動(dòng)釋放池的最大用處就是能延長(zhǎng)對(duì)象生命期,尤其是在方法返回對(duì)象時(shí)候更應(yīng)該用他,通過autorelease的調(diào)用,對(duì)象不會(huì)被立即釋放,而是等到下次“事件循環(huán)”才執(zhí)行引用計(jì)數(shù)遞減工作,在這期間就會(huì)留給我們足夠長(zhǎng)的時(shí)間對(duì)對(duì)象進(jìn)行相關(guān)的操作,相比較于release不會(huì)那么強(qiáng)硬,但是各有各的好處。另外自動(dòng)釋放池也會(huì)有降低內(nèi)存峰值的作用,后面介紹。
3.保留環(huán)
就是循環(huán)引用,就是兩個(gè)對(duì)象互相持有(也可能多個(gè)對(duì)象之間成環(huán)式的持有),這就導(dǎo)致所有對(duì)象的引用計(jì)數(shù)為1,最后誰也不能釋放,對(duì)于不同情況下產(chǎn)生的保留環(huán)會(huì)有不同的處理方法,后面會(huì)介紹一種“弱引用”方法解決,也可以從外界命令環(huán)中的一個(gè)對(duì)象不再對(duì)另外一個(gè)對(duì)象進(jìn)行持有。

30. 以ARC簡(jiǎn)化引用計(jì)數(shù)

ARC的出現(xiàn)時(shí)程序員的福音,因?yàn)樵僖膊挥脼榭紤]引用計(jì)數(shù)發(fā)愁了,所有這些應(yīng)該增加或減少引用計(jì)數(shù)的地方都由編譯器的“靜態(tài)分析器”幫我們解決,所以這也是為什么我們不能再ARC下調(diào)用reatin、release、autorelease、dealloc方法的原因,因?yàn)槭謩?dòng)調(diào)用這些方法會(huì)干擾編譯器的判斷。另外,編譯器的引用計(jì)數(shù)不是通過普通的消息派發(fā)機(jī)制,而是通過更底層的方法,這樣更能提高代碼的效率。我們要知道一點(diǎn),ARC下還是有引用計(jì)數(shù)機(jī)制,只不過這個(gè)工作被編譯器做了。
使用ARC時(shí)要遵循方法命名規(guī)則,因?yàn)榫幾g器在分析法代碼的時(shí)候會(huì)根據(jù)方法名分析代碼,例如,若方法名以alloc、new、copy、mutableCopy開頭,則其返回的對(duì)象歸調(diào)用者所有,那么這部分代碼就要負(fù)責(zé)釋放方法所返回的對(duì)象。若方法名不以這四個(gè)詞開頭,則返回對(duì)象不歸調(diào)用者所有,這種個(gè)情況下返回兌現(xiàn)會(huì)制自動(dòng)釋放,不過現(xiàn)在這些工作都由ARC幫我們做了。ARC還會(huì)對(duì)操作約減,將retain和release相互抵消,這樣都會(huì)優(yōu)化代碼,節(jié)省內(nèi)存。另外,ARC還有運(yùn)行期組件,這些操作都會(huì)大大的優(yōu)化我們的程序,具體的不過多介紹,至于這些優(yōu)化的詳細(xì)內(nèi)部實(shí)現(xiàn),只有編譯器的作者知道。
ARC也會(huì)處理局部變量和實(shí)例變量的內(nèi)存管理,ARC會(huì)以一種安全的方式來設(shè)置一個(gè)變量,他總是遵循先保留新值,再釋放舊值,最后設(shè)置實(shí)例變量。在應(yīng)用程序中,可以用下面的修飾符來改變局部變量與實(shí)例變量的語義:
__strong: 默認(rèn)語義,保留此值
__unsafe_unretained: 不保留此值(這么做不安全,因?yàn)樵俅问褂玫臅r(shí)候?qū)ο罂赡芤呀?jīng)被回收)
__weak: 不保留此值,但是變量可以安全使用,在某些情況下這個(gè)修飾符很有用
__autoreleasing: 把對(duì)象“按引用傳遞”給方法時(shí),此修飾符會(huì)讓此值在方法返回時(shí)自動(dòng)釋放
block會(huì)自動(dòng)保留其捕獲的全部對(duì)象,如果這些對(duì)象中有一個(gè)對(duì)象又保留了block本身,那么就會(huì)導(dǎo)致保留環(huán),這時(shí)候就可以用__weak修飾局部變量來打破這種保留環(huán),避免循環(huán)引用。
ARC可以自動(dòng)的幫助我們清理實(shí)例變量,并且ARC的清理會(huì)比我們手動(dòng)在dealloc方法中release更高效,ARC會(huì)用C++對(duì)象的析構(gòu)函數(shù),不過有一些非OC對(duì)象在調(diào)用的時(shí)候就要手動(dòng)清理,這些框架都有對(duì)應(yīng)的清理方法,例如CoreFoundation框架中的對(duì)象或是由malloc()分配在對(duì)中的內(nèi)存,這時(shí)候需要我們?cè)赿ealloc中調(diào)用對(duì)應(yīng)的方法手動(dòng)釋放。如下

- (void)dealloc{
    CFRelease(_coreFoundationObject);
    free(_heapAllocatedMemoryBlob);
}

另外,由于ARC的自動(dòng)引用計(jì)數(shù)機(jī)制,內(nèi)存管理方法是不可以覆寫的,我們要相信ARC會(huì)給我們更好的代碼優(yōu)化。

31. 在dealloc方法中只釋放引用并解除監(jiān)聽

dealloc是一個(gè)對(duì)象生命周期中執(zhí)行的最后一個(gè)方法,這個(gè)方法執(zhí)行后,對(duì)象將不復(fù)存在,所以,在這個(gè)方法中我們就要考慮清除所有關(guān)于這個(gè)對(duì)象的痕跡,前面已經(jīng)提到過,ARC下編輯器會(huì)自動(dòng)在這個(gè)方法中添加適當(dāng)?shù)姆椒?,解除?duì)象的引用,對(duì)于不屬于OC的對(duì)象,也應(yīng)該在這個(gè)方法中釋放,另外dealloc方法還有一個(gè)重要的用處就是把原來配置的觀測(cè)行為都清理掉,最典型的就是移除通知的觀察者:

- (void)dealloc{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

我們要謹(jǐn)記,不能再別的方法中調(diào)用dealloc方法,調(diào)用后后續(xù)的代碼都將失效,另外我們也要遵循一個(gè)規(guī)則,就是在dealloc中不要調(diào)用其他的和釋放無關(guān)的方法。
有時(shí)候,一些開銷較大或系統(tǒng)稀缺支援的釋放需要我們自行設(shè)定釋放方法在合適的時(shí)候調(diào)用,例如文件描述符、套接字、大塊內(nèi)存等,這些東西都需要我們自行編寫釋放方法,至于有的時(shí)候可能在程序運(yùn)行到一半的時(shí)候就退出了,這個(gè)時(shí)候還沒有來的及走dealloc方法,大家可以不用關(guān)心這個(gè)問題,程序退出,程序中的對(duì)象也會(huì)銷毀,另外我們可以通過UIApplicationDelegate中的applicationWillTerminate方法,在程序退出之前做我們想要做的事情。

32. 編寫“異常安全代碼”時(shí)留意內(nèi)存管理問題

OC和C++都支持異常,并且兩門語言的異常是互通的,也就是說從一門語言里拋出的異常能用另外一門語言編輯的異常處理程序來捕獲。OC中,異常的拋出應(yīng)該是在極其錯(cuò)誤的情況下,前面的錯(cuò)誤模型已經(jīng)介紹,但是有時(shí)候第三方庫中也會(huì)用到異常,這時(shí)候就需要我們進(jìn)行處理。
通常我們使用事務(wù)來處理一段異常,在非ARC情況下可以做如下處理:

EOCSomeClass *object;
    @try {
        object = [[EOCSomeClass alloc] init];
        [object doSomethingThatMayThrow];
    } @catch (NSException *exception) {
        NSLog(@"有一個(gè)異常要處理");
    } @finally {
        [object release];
    }

在非ARC的情況下我們可以通過@finally塊,把最后的引用釋放,但是在ARC下怎么處理呢?下面是ARC下相同功能的代碼:

EOCSomeClass *object;
    @try {
        object = [[EOCSomeClass alloc] init];
        [object doSomethingThatMayThrow];
    } @catch (NSException *exception) {
        NSLog(@"有一個(gè)異常要處理");
    } 

這時(shí)候不能寫release方法,所以@finally塊可以去掉,但是拋出異常的時(shí)候引用沒有被釋放啊,我們可能以為編輯器會(huì)給我們做處理,其實(shí)不是,默認(rèn)情況下編輯器是不會(huì)給我們做處理的,我們需要通過-fobjc-arc-exceptions這個(gè)編譯標(biāo)志來開啟這個(gè)功能,這時(shí)候編譯器會(huì)幫我們生成安全處理異常的附加代碼(附加代碼的代碼量還是很大的),這個(gè)功能默認(rèn)不開啟有兩個(gè)原因,第一個(gè)就是前面說的附加碼代碼量很大,另外一個(gè)原因就是系統(tǒng)任務(wù)如果拋出異常就是很嚴(yán)重的錯(cuò)誤,可以直接終止程序了,所以系統(tǒng)默認(rèn)是不開啟的。另外說一點(diǎn),如果編譯器發(fā)現(xiàn)我們編寫的是Objective-C++代碼,會(huì)默認(rèn)開啟這個(gè)狀態(tài),因?yàn)镃++中會(huì)頻繁使用異常。

33. 以弱引用避免保留環(huán)

保留環(huán)就是我們常說的循環(huán)引用,簡(jiǎn)單的保留環(huán)是兩個(gè)對(duì)象之間互相引用,復(fù)雜的保留環(huán)可能是三個(gè)或者更多對(duì)象之間的呈鏈?zhǔn)降难h(huán)引用,前面已經(jīng)介紹了,可以在聲明名屬性的時(shí)候使用assign、unsafe_unretained、weak三個(gè)修飾符解決保留環(huán)的問題,下面介紹一下這三個(gè)修飾符的不同點(diǎn)。
assign: 通常只用于整形類型(int、float、結(jié)構(gòu)體等)
unsafe_unretained: 通常用于對(duì)象
weak:通常用于對(duì)象
舉個(gè)例子介紹unsafe_unretained和weak區(qū)別,現(xiàn)系統(tǒng)持有兩個(gè)對(duì)象,對(duì)象A和對(duì)象B,并且對(duì)象A和對(duì)象B互相持有,當(dāng)系統(tǒng)銷毀對(duì)象A之后,如果是用weak修飾,那么對(duì)象B指向?qū)ο驛的引用就不存在了,這時(shí)候?qū)ο驜指向nil,如果是用unsafe_unretained修飾,在系統(tǒng)銷毀對(duì)象A之后,對(duì)象B指向?qū)ο驛的引用依然存在,由此可以看出unsafe_unretained是很不安全的,所以可以發(fā)現(xiàn),基本我們看不到unsafe_unretained修飾的屬性,一般情況下都是使用weak修飾。

34. 以“自動(dòng)釋放池塊”降低內(nèi)存峰值

OC中的自動(dòng)釋放池是引用計(jì)數(shù)機(jī)制中的一項(xiàng)特性,前面已經(jīng)介紹,自動(dòng)釋放池中對(duì)象的釋放是等到一次循環(huán)結(jié)束,而不是像release一樣馬上釋放,這樣就留下充足的時(shí)間處理這些對(duì)象,創(chuàng)建自動(dòng)釋放池用@autoreleasepool,不過在一般情況下我們無需擔(dān)心自動(dòng)釋放池的創(chuàng)建問題,因?yàn)橄到y(tǒng)會(huì)在他認(rèn)為需要的地方自動(dòng)創(chuàng)建一些自動(dòng)釋放池,例如主線程或者GCD機(jī)制中的線程。下面介紹幾個(gè)和自動(dòng)釋放池有關(guān)的知識(shí)點(diǎn),大家會(huì)發(fā)現(xiàn),我們創(chuàng)建好一個(gè)工程后,main函數(shù)總是這樣寫的:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

其實(shí)從技術(shù)角度看,這個(gè)自動(dòng)釋放池可有可無,因?yàn)榈鹊揭獔?zhí)行自動(dòng)釋放操作的時(shí)候已經(jīng)是程序的結(jié)尾了,那么為什么還要加呢,如果不加的話UIApplicationMain函數(shù)所自動(dòng)釋放的那些對(duì)象,就沒有自動(dòng)釋放池容納了,所以,這個(gè)釋放池可以理解成最外圍捕捉全部自動(dòng)釋放對(duì)象所用的池。
再看下面的代碼:

@autoreleasepool {
        // 做一些操作
        @autoreleasepool {
            // 做一些操作
        }
    }

從上面可以發(fā)現(xiàn)自動(dòng)釋放池可以嵌套使用,這樣做最大的好處是可以不用等到最外面的釋放池執(zhí)行釋放操作,內(nèi)部的釋放池可以先執(zhí)行釋放操作,自動(dòng)釋放池的范圍的由大括號(hào)決定的,這樣做可以降低內(nèi)存峰值,再看下面一個(gè)例子:

for (int i = 0; i < 1000000; i++) {
        @autoreleasepool {
            // 執(zhí)行創(chuàng)建對(duì)象操作
        }   
    }

從上面可以看出這個(gè)制動(dòng)釋放池的作用就是不用等到循環(huán)結(jié)束再執(zhí)行釋放操作,如果等到循環(huán)結(jié)束的時(shí)候再執(zhí)行釋放操作的話,內(nèi)存中創(chuàng)建的對(duì)象就太多了,這種情況在我們從數(shù)據(jù)庫中提取數(shù)據(jù)的時(shí)候會(huì)經(jīng)常發(fā)生,對(duì)于數(shù)據(jù)庫中的數(shù)據(jù)我們是不知道多少的,如果我們一下查詢出過多的數(shù)據(jù),這個(gè)時(shí)候如果要把數(shù)據(jù)轉(zhuǎn)換成對(duì)象就會(huì)出現(xiàn)上面的問題,這個(gè)時(shí)候自動(dòng)釋放池就派上用場(chǎng)了。
是否進(jìn)行自動(dòng)釋放池優(yōu)化,完全是根據(jù)應(yīng)用本身決定的,在ARC出現(xiàn)之前,有一種老式的制動(dòng)釋放池寫法,就是NSAutoreleasePool對(duì)象,這里不再介紹,感興趣可以自行查閱,總的來說這個(gè)對(duì)象更加重量級(jí),而現(xiàn)在的@autoreleasePool更加輕便好用。

35. 用“僵尸對(duì)象”調(diào)試內(nèi)存管理問題

如果向被回收的對(duì)象發(fā)送消息有時(shí)會(huì)造成崩潰,但是這種崩潰又不是一定的,為什么會(huì)出現(xiàn)這種情況呢,前面已經(jīng)介紹,被回收的對(duì)象占用的內(nèi)存沒有清空,只不過這一部分內(nèi)存放入可用內(nèi)存區(qū)域,如果還沒有被覆寫,那么對(duì)象仍然是存在的,這就是為什么有時(shí)候崩潰有時(shí)候不崩潰的原因,想要排查很困難,這時(shí)候就輪到僵尸對(duì)象上場(chǎng)了,當(dāng)開啟僵尸模式后,運(yùn)行期系統(tǒng)不會(huì)把回收的對(duì)象放到可用內(nèi)存區(qū)域,而是將回收對(duì)象變成僵尸對(duì)象,這樣所有的信息都被僵尸對(duì)象接收,系統(tǒng)的設(shè)定是僵尸對(duì)象在收到消息后,會(huì)拋出異常,并在拋出的信息中準(zhǔn)確的描述發(fā)送過來的信息,并且描述了是哪個(gè)對(duì)象變成了現(xiàn)在的僵尸對(duì)象,這就是大致的僵尸模式的工作模式,下面介紹一下如何開啟僵尸模式


開啟僵尸模式步驟

在僵尸模式拋出的異常信息中就有相關(guān)的對(duì)象的信息,例如有一個(gè)類EOCClass,在僵尸模式中拋出異常的時(shí)候我們會(huì)看到_NSZombie_EOCClass,從拋出的異常信息就可以追查出問題所在,有助于解決問題。
ARC模式下,由于內(nèi)存不用手動(dòng)管理,會(huì)很少出現(xiàn)僵尸對(duì)象,但容易產(chǎn)生上述問題的場(chǎng)景主要有兩個(gè):一是方法內(nèi)的局部對(duì)象,在其他方法使用; 二是異步過程的回調(diào),比如網(wǎng)絡(luò)操作。

36. 不要使用retainCount

前面已經(jīng)多次強(qiáng)調(diào)過這個(gè)方法的不可靠性,這里再強(qiáng)調(diào)一下,首先,在ARC下,只要調(diào)用這個(gè)方法編譯器就會(huì)報(bào)錯(cuò),在非ARC模式下,調(diào)用此方法也有很多風(fēng)險(xiǎn),這里只說一點(diǎn),retainCount返回的是某個(gè)時(shí)間點(diǎn)上的絕對(duì)保留計(jì)數(shù),這一時(shí)間點(diǎn)無法反應(yīng)生命周期全貌,并且OC中還有自動(dòng)釋放池機(jī)制,所以無論在哪種模式下都不應(yīng)該使用這個(gè)方法。
總的來說,ARC的出現(xiàn)大大的簡(jiǎn)化了內(nèi)存管理,但是ARC不代表不會(huì)出現(xiàn)內(nèi)存泄漏等問題,在寫代碼時(shí)還是要很細(xì)心,另外書中有許多例子沒有介紹,例如僵尸模式下系統(tǒng)是如何把一個(gè)對(duì)象變成僵尸對(duì)象的等,感興趣的同學(xué)可以自行查閱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 聲明:這個(gè)筆記的系列是我每天早上打開電腦第一件做的事情,當(dāng)然使用的時(shí)間也不是很多因?yàn)檫€有其他的事情去做,雖然吧自己...
    iSuAbner閱讀 667評(píng)論 2 4
  • 29.理解引用計(jì)數(shù) Objective-C語言使用引用計(jì)數(shù)來管理內(nèi)存,也就是說,每個(gè)對(duì)象都有個(gè)可以遞增或遞減的計(jì)數(shù)...
    Code_Ninja閱讀 1,754評(píng)論 1 3
  • “助推”是一種深含價(jià)值觀和目標(biāo),同時(shí)充分考慮到如何調(diào)動(dòng)資源、制定有效流程、使目標(biāo)得以實(shí)現(xiàn)、使價(jià)值觀得以堅(jiān)守的“硬球...
    gyl58365閱讀 1,712評(píng)論 0 1
  • 閱讀原文 在UIImage上面繪制內(nèi)容,步驟 得到圖片 UIImage* image=[UIImage image...
    學(xué)生陳希閱讀 404評(píng)論 0 0
  • 某一天晚上,我接到了十多條語音轟炸。 開始只是一條長(zhǎng)長(zhǎng)的對(duì)話框,后來是一發(fā)不可收拾。 通過電流傳輸過來的聲音有些失...
    羅衣酒閱讀 384評(píng)論 0 0

友情鏈接更多精彩內(nèi)容