Effective objective-c-- 內存管理
- 前言
- 理解引用計數
- 引用計數工作原理
- 屬性存取方法中的內存管理
- 自動釋放池
- 保留環
- 要點
- 以ARC簡化引用計數
- 使用ARC時必須遵循的方法和命名規則
- 變量的內存管理語義
- ARC如何清理實例變量
- 覆寫內存管理方法
- 要點
- 在dealloc方法中只釋放引用并解除監聽
- 要點
- 編寫“異常安全代碼“時留意內存管理問題
- 以弱引用避免保留環
- unsafe_unretained 和 weak
- 要點
- 以”自動釋放池“釋放內存峰值
- 要點
- 不要使用retainCount
前言
寒假比較忙沒能能認真看,就拖到學校了來看了,進度落后很多了,只能盡量趕了;
理解引用計數
Objective-C 語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數器。如果想使某個對象繼續存活,那就遞增其引用計數:用完了之后,就遞減其計數。計數變為 0,就表示沒人關注此對象了,于是,就可以把它銷毀。
要注意開啟ACR功能后,引用計數的方法無法使用 ;
引用計數工作原理
在引用計數架構下,對象有個計數器,用以表示當前有多少個事物想令此對象繼續存活下去。這在 Objective-C 中叫做 “保留計數” (retain count),不過也可以叫 “引用計數”(reference count)。NSObject 協議聲明了下面三個方法用于操作計數器,以遞增或遞減其值:
Retain 遞增保留計數。
release 遞減保留計數。
autorelease 待稍后清理 “自動釋放池”(autorelease pool)時,再遞減保留計數。
查看保留計數的方法叫做 retainCount,此方法不太有用
圖演示了對象自創造出來之后歷經一次 “保留” 及兩次 “釋放” 操作的過程。
使用引用計數的方法要關閉ACR,方法如下:在bulid settings->all->Combined->Apple Clang-language-oBjectiveC->Automatic Reference Counting設置為NO
看下面這段代碼:
NSMutableArray* array = [[NSMutableArray alloc] init] ;NSNumber* number = [[NSNumber alloc] initWithInt:15] ;
// [array addObject:number] ;[number release] ;NSLog(@"%@",number) ;
// NSLog(@"%lu",(unsigned long)[number retainCount]) ;
// NSLog(@"%@",[array objectAtIndex:0]) ;[array release] ;
上面這段代碼我是想看看number在引用計數為0是調用該對象會不會崩潰,不過出乎意料的是是沒有崩,這里的解釋是:
在某些情況下,對已釋放的對象進行訪問可能不會立即導致崩潰。這是因為已釋放的對象在內存中仍然存在一段時間,并且指針仍然指向該內存位置。這被稱為“懸垂指針”(Dangling Pointer)。
當你嘗試訪問已釋放的對象時,可能會發生以下情況之一:
你可能會幸運地訪問到一個仍然有效的對象。在這種情況下,你可能不會立即遇到崩潰或錯誤。
你可能會訪問到無效的對象或者已經被其他對象覆蓋的內存。在這種情況下,你可能會遇到崩潰、內存訪問錯誤或者其他不可預測的行為。
書上的例子:
```objectivec
NSMutableArray *array = [[NSMutableArray alloc] init];NSNumber *number = [[NSNumber alloc] initWithInt:2023];[array addObject:number];[number release];[array release];
創建數組之后,把number加入其中的時候,系統會為number retain一次,也就是number的引用計數為2,接下來不需要number對象的時候我們釋放了它,在這個例子里能知道number的引用計數現在還為1
為了避免在不經意間使用了無效對象,一般relase之后都會清空指針,這樣能保證不出現懸空指針
NSMutableArray *array = [[NSMutableArray alloc] init];NSNumber *number = [[NSNumber alloc] initWithInt:2023];[array addObject:number];[number release];[array release];number == nil;
屬性存取方法中的內存管理
不光是數組,其他對象也可以保留別的對象,這一般通過訪問 “屬性”來實現,而訪問屬性時,會用到相關實例變量的獲取方法及設置方法。若屬性為 “strong 關系”(strong relationship),則設置的屬性值會保留。比方說,有個名叫 foo 的屬性由名為 _foo 的實例變量所實現,那么,該屬性的設置方法會是這樣:
此方法將保留新值并釋放舊值,然后更新實例變量,令其指向新值。順序很重要。假如還未保留新值就先把舊值釋放了,而且兩個值又指向同一個對象,那么,先執行的 release 操作就可能導致系統將此對象永久回收。而后續的 retain 操作則無法令這個已經徹底回收的對象復生,于是實例變量就成了懸掛指針了。(這里上面也提到了) ;
自動釋放池
在 Objective-C 的引用計數架構中,自動釋放池是一項重要特性。調用 release 會立刻遞減對象的保留計數(而且還有可能令系統回收此對象),然而有時候可以不調用它,改為調用 autorelease,此方法會在稍后遞減計數,通常是在下一次 “事件循環”(event loop)時遞減,不過也可能執行得更早些。
對于上面這個方法,我們需要延遲str對象的回收釋放,也就是說,我們要用automatic release ;
用 autorelease,它會在稍后釋放對象,從而給調用者留下了足夠長的時間,使其可以在需要時先保留返回值。換句話說,此方法可以保證對象在跨越 “方法調用邊界”(method callboundary)后一定存活。實際上,釋放操作會在清空最外層的自動釋放池(參見第 34 條)時執行,除非你有自己的自動釋放池,否則這個時機指的就是當前線程的下一次事件循環
autorelease 能延長對象生命周期,使其在跨越方法調用邊界后依然可以存活一段時間。
保留環
使用引用計數機制時,經常要注意的一個問題就是 “保留環”(retain cycle),也就是呈環狀相互引用的多個對象。這將導致內存泄漏,因為循環中的對象其保留計數不會降為 0。對于循環中的每個對象來說,至少還有另外一個對象引用著它。圖里的每個對象都引用了另外兩個對象之中的一個。在這個循環里,所有對象的保留計數都是 1。
在垃圾收集環境中,通常將這種情況認定為 “孤島”(island of isolation)。此時,垃圾收集器會把三個對象全都回收走。而在 Objective-C 的引用計數架構中,則享受不到這一便利。通常采用 “弱引用”(weak reference,參見第 33 條)來解決此問題,或是從外界命令循環中的某個對象不再保留另外一個對象。這兩種辦法都能打破保留環,從而避免內存泄漏。
要點
- 引用計數機制通過可以遞增遞減的計數器來管理內存。對象創建好之后,其保留計數至少為 1。若保留計數為正,則對象繼續存活。當保留計數降為 0 時,對象就被銷毀了。
- 在對象生命期中,其余對象通過引用來保留或釋放對象。保留于釋放操作分別會遞增及遞減保留計數。
以ARC簡化引用計數
此代碼有內存泄漏問題,因為 if 語句塊末尾并未釋放 message 對象。由于在 if 語句之外無法引用 message,所以此對象所占的內存泄漏了(這里“泄漏”的意思是:沒有正確釋放已經不再使用的內存)。
- 由于 ARC 會自動執行 retain、release 、autorelease 等操作,所以直接在 ARC 下調用這些內存管理方法是非法的。
ARC的優點除了方便外,還有:ARC 在調用這些方法時,并不通過普通的 Objective-C 消息派發機制,而是直接調用其底層 C 語言版本。這樣做性能更好,因為保留及釋放操作需要頻繁執行,所以直接調用底層函數能節省很多 CPU 周期。
使用ARC時必須遵循的方法和命名規則
將內存管理語義在方法名中表示出來早已成為 Objective-C 的慣例,而 ARC 則將之確立為硬性規定。這些規則簡單地體現在方法名上。若方法名以下列詞語開頭,則其返回的對象歸調用者所有:
- alloc
- new
- copy
- mutableCopy
歸調用者所有的意思是: 調用上述四種方法的那段代碼要負責釋放方法所返回的對象。也就是說,這些對象的保留計數是正值,而調用了這四種方法的那段代碼要將其中一次保留操作抵消掉。
要注意:如果還有其他對象保留此對象,并對其調用了 autorelease,那么保留計數的值可能比 1 大,這也是 retainCount 方法不太有用的原因之一。
若方法名不以上述四個詞語開頭,則表示其所返回的對象并不歸調用者所有。 在這種情況下,返回的對象會自動釋放
維系這些規則所需的全部內存管理事宜均由 ARC 自動處理
變量的內存管理語義
ARC 也會處理局部變量與實例變量的內存管理。
默認情況下,每個變量都是指向對象的強引用。
對于以下的代碼:
在非ARC下執行的setter方法的實現是這樣的:
但這個方法很明顯是不安全的,如果只有當前對象還在引用這個值,那么設置方法中的釋放操作會使該值的保留計數降為0,從而導致系統將其回收。接下來再執行保留操作,就會令應用程序崩潰。
ARC 會用一種安全的方式來設置:先保留新值,再釋放舊值,最后設置實例變量。
在應用程序中,可用下列修飾符來改變局部變量與實例變量的語義:
__strong: 默認語義,保留此值。
__unsafe_unretained: 不保留此值,這么做可能不安全,因為等到再次使用變量時,其對象可能已經回收了。
__weak: 不保留此值,但是變量可以安全使用,因為如果系統把這個對象回收了,那么變量也會自動清空。
__autoreleasing: 把對象 “按引用傳遞” (pass by reference)給方法時,使用這個特殊的修飾符。此值在方法返回時自動釋放。
- 我們經常會給局部變量加上修飾符,用以打破由“塊”(block),所引入的“保留環”(retain cycle)。塊會自動保留其所捕獲的全部對象,而如果這其中有某個對象又保留了塊本身,那么就可能導致 “保留環”。可以用 __weak 局部變量來打破這種 “保留環”:
ARC如何清理實例變量
要管理其內存,ARC 就必須在 “回收分配給對象的內存”(deallocate)
當手動管理引用計數時,你可能會像下面這樣自己來編寫 dealloc 方法:
如果有非 Objective-C 的對象,不需要像原來那樣調用超類的 dealloc 方法。
ARC 環境下,dealloc 方法可以像這樣寫:
因為 ARC 會自動生成回收對象時所執行的代碼,所以通常無須再編寫 dealloc 方法。這能減少項目源代碼的大小,而且可以省去其中一些樣板代碼(boilerplate code)。
覆寫內存管理方法
不使用ARC時,可以覆寫內存管理方法。比方說,在實現單例類的時候,因為單例不可釋放,所以我們經常覆寫release方法,將其替換為“空操作”
要點
- 有 ARC 之后,程序員就無須擔心內存管理問題了。使用 ARC 來編程,可省去類中的許多 “樣板代碼”。
- ARC 管理對象生命期的辦法基本上就是:在合適的地方插入 “保留” 及 “釋放”操作。
- 在 ARC 環境下,變量的內存管理語義可以通過修飾符指明,而原來需要手工執行 “保留” 及 “釋放”操作。
- 由方法所返回的對象,其內存管理語義總是通過方法名來體現。ARC 將此確定為開發者必須遵守的規則。
- ARC 只負責管理 Objective-C 對象的內存。尤其要注意: CoreFoundation 對象不歸 ARC
管理,開發者必須適時調用 CFRetain/CFRelease。
在dealloc方法中只釋放引用并解除監聽
對象在經歷其生命期后,最終會為系統所回收,這時就要執行 dealloc 方法了。在每個對象的生命期內,此方法僅執行一次,也就是當保留計數降為 0 的時候。
實際上,程序庫會以開發者察覺不到的方式操作對象,從而使回收對象的真正時機和預期的不同。你決不應該自己調用 dealloc 方法,運行期系統會在適當的時候調用它。
那么,應該在 dealloc 方法中做什么呢?
主要就是釋放對象所擁有的引用,也就是把所有 Objective-C 對象都釋放掉,對象所擁有的其他非 Objective-C 對象也要釋放。比如 CoreFoundation 對象就必須手工釋放,因為它們是由純C 的API 所生成的。
所以可以是:
最好還要加上[super dealloc] ;
編寫 dealloc 方法時還需要注意,不要在里面隨便調用其他方法。
- 如果在這里所調用的方法又要異步執行某些任務,或是又要繼續調用它們自己的某些方法,那么等到那些任務執行完畢時,系統已經把當前這個待回收的對象徹底摧毀了。這會導致很多問題,且經常使應用程序崩潰,因為那些任務執行完畢后,要回調此對象,告訴該任務已完成,而此時如果對象已摧毀,那么回調操作就回出錯。
- 在 dealloc 里也不要調用屬性的存取方法,因為有人可能會覆寫這些方法,并與其中做一些無法在回收階段安全執行的操作。
要點
- 在 dealloc 方法里,應該做的事情就是釋放指向其他對象的引用,并取消原來訂閱的“鍵值觀測”(KVO)或
NSNOtificationCenter 等通知,不要做其他事情。 - 如果對象持有文件描述符等系統資源,那么應該專門編寫一個方法來釋放此種資源。這樣的類要和其使用者約定: 用完資源后必須調用 close
方法。 - 執行異步任務的方法不應該在 dealloc 里調用; 只能在正常狀態下執行的那些方法也不應在 dealloc
里調用,因為此時對象已處于正在回收的狀態了。
編寫“異常安全代碼“時留意內存管理問題
純 C 中沒有異常,而 C++ 與 Objective-C 都支持異常。在當前的運行期系統中,C++ 與 Objective-C 的異常相互兼容,也就是說,從其中一門語言里拋出的異常能用另外一門語言所編寫的 “異常處理程序”(exception handler)來捕獲。
比如使用 Objective-C++ 來編碼時,或是編碼中用到了第三方程序庫而此程序庫所拋出的異常又不受你控制時,就需要捕獲及處理異常了。
C++ 的析構函數(destructor)由 Objective-C 的異常處理例程(exception-handle routine)來運行。這對于 C++ 對象很重要,由于拋出異常會縮短其生命周期,所以發生異常時必須析構,不然就會泄漏,
手動管理的方式如下:
但上面的方法中,如果dosomethingThatMayThrow方法判斷拋出異常,會直接進入catch塊,所以沒能release導致內存泄漏,解決方法時在設一個@finalllay塊,將release操作放到其中。
以弱引用避免保留環
首先我們要了解什么是保留環:
對象圖里經常會出現一種情況,就是幾個對象都以某種方式互相引用,從而形成“環”(cycle)。由于 Objective-C 內存管理模型使用引用計數架構,所以這種情況通常會泄漏內存,因為最后沒有別的東西會引用環中的對象。
下面給一個書上的例子;
兩個類之間的引用關系如圖:
上面就是一個簡單的保留環,即使外界不在引用這兩個對象,這兩個對象依賴于彼此的引用關系,其引用計數不會降到0,即他們不會被系統自動回收,造成內存泄漏 ;
下面用圖表示一個更復雜的保留環,
如果只剩一個引用還指向保留環中的實例,而現在又把這個引用移除,那么整個保留環就泄漏了。也就是說,沒辦法再訪問其中的對象了。圖中所示的保留環更復雜一些,其中有四個對象,只有 ObjectB 還為外界所引用,把僅有的這個引用移除之后,四者所占內存就泄漏了。
避免保留環的最佳方式就是弱引用。這種引用經常用來表示 “非擁有關系”(nonowning relationship)。將屬性聲明為 unsafe_unretained
可以把上面的代碼修改成下面這樣:
修改之后,EOCClassB 實例就不再通過 other 屬性來擁有 EOCClassA 實例了。屬性特質 (attribute) 中的 unsafe_unretained 一詞表明,屬性值可能不安全,而且不歸此實例所擁有。如果系統已經把屬性所指的那個對象回收了,那么在其上調用方法可能會使應用程序崩潰。由于本對象并不保留屬性對象,因此其有可能為系統所回收。
unsafe_unretained 和 weak
weak與 unsafe_unretained 的作用完全相同。然而,只要系統把屬性回收,屬性值就會自動設為 nil。
使用 weak 而非 unsafe_unretained 引用可以令代碼更安全。應用程序也許會顯示出錯誤的數據,但不會直接崩潰。這么做顯然比令終端用戶直接看到程序退出要好。
要點
- 將某些引用設為 weak,可避免出現 “保留環”。
- weak 引用可以自動清空,也可以不自動清空。自動清空(autonilling)是隨著 ARC
而引入的新特性,由運行期系統來實現。在具備自動清空功能的弱引用上,可以隨意讀取其數據,因為這種引用不會指向已經回收過的對象。
以”自動釋放池“釋放內存峰值
創建自動釋放池所用語法如下:
一般情況下無須擔心自動釋放池的創建問題。
通常只有一個地方需要創建自動釋放池,那就是在 main 函數里,我們是自動釋放池來包裹應用程序的主入口點 (main application entry point)。
自動釋放池于左花括號處創建,并于對應的右花括號處自動清空。位于自動釋放池范圍內的對象,將在此范圍末尾處收到 release 消息。自動釋放池可以嵌套。系統在自動釋放對象時,會把它放到最內層的池里。
將自動釋放池嵌套用的好處是,可以借此控制應用程序的內存峰值,使其不致過高。
上面這段代碼的for循環中會產生許多多余的變量占用內存,由于無法通過指針來直接調用release來釋放內存,所以這里最好設置自動釋放池來控制內存
自動釋放池機制就像 “棧”(stack)一樣。系統創建好自動釋放池之后,就將其推入棧中,而清空自動釋放池,則相當于將其從棧中彈出。在對象上執行自動釋放操作,就等于將其放入棧頂的那個池里。
是否應該用池來優化效率,完全取決于具體的應用程序。首先得監控內存用量,判斷其中有沒有需要解決的問題,如果沒完成這一步,那就別急著優化。盡管自動釋放池塊的開銷不太大,但畢竟還是有的,所以盡量不要建立額外的自動釋放池。
要點
- 自動釋放池排布在棧中,對象收到 autorelease 消息后,系統將其放入最頂端的池里。
- 合理運用自動釋放池,可降低應用程序的內存峰值。
- @autoreleasepool 這種新式寫法能創建出更為輕便的自動釋放池。
不要使用retainCount
如果在 ARC 中調用,編譯器就會報錯,這和在 ARC 中調用 retain、release、autorelease 方法時的情況一樣。雖然此方法已經正式廢棄了,但還是經常有人誤解它,其實這個方法根本就不應該調用。若在不啟用 ARC 的環境下編程(說真的,還是在 ARC 下編程比較好),那么仍可調用此方法,而編譯器不會報錯。
此方法之所以無用,其首要原因在于:它返回的保留計數只是某個給定時間點上的值。該方法并未考慮到系統會稍后把自動釋放池清空,因而不會將后續的釋放操作從返回值里減去,這樣的話,此值就未必能真實反映實際的保留計數了。
那么,只為了調試而使用 retainCount 方法行不行呢?即便只為調試,此方法也不是很有用。由于對象可能處在自動釋放池中,所以其保留計數未必如想象般精確。而且其他程序庫也可能自行保留或釋放對象,這都會擾亂保留計數的具體取值。看了具體的計數值之后,你可能還誤以為是自己的代碼修改了它,殊不知其實是由深埋在另外一個程序庫中的某段代碼所改的。
- 對象的保留計數看似有用,實則不然,因為任何給定時間點上的“絕對保留計數”(absolute retain
count)都無法反映對象生命期的全貌。 - 引入 ARC 之后,retainCount 方法就正式廢止了,在 ARC 下調用該方法會導致編譯器報錯。