1. 程式人生 > >《iOS高階記憶體管理程式設計指南》學習筆記

《iOS高階記憶體管理程式設計指南》學習筆記

Object-C 一共有3種記憶體管理方式: 1. MRR (Manual Retain-Release)手動持有-釋放。採用了引用計數模型,由基礎類NSObject和執行時(Runtime Eviroment)共同提供。 2. ARC (Automatic Retain-Count)自動引用計數。此方式採用與MRR相同的引用計數系統,但是在編譯時(Compile-time)插入了記憶體管理的方法。 3. 垃圾回收方式。系統自動跟蹤物件與物件之間的引用關係。對於沒有引用的物件,自動回收。這種模式與MRR和ARC都不同,且只能用於Mac OS
錯誤的記憶體管理方式一般有2種: 1. 釋放或者覆蓋了正在使用中的資料。造成記憶體異常,程式崩潰。 2. 不用的資料卻沒有釋放,導致記憶體洩露。
所有權策略通過引用計數來實現      通常稱之為“retain count”。每個物件都有一個retain count。      當建立一個物件時,它的retain count 為1。      物件呼叫 retain方法時,它的retain count +1。      物件呼叫 release方法時,它的retain count -1。      物件呼叫 autorelease方法時,它在retain count將在未來某事 -1。      當物件retain count 為0,就會被dealloc 正常使用下,這個計數並不需要關心,只需要遵守所有權使用規則就行。
基礎使用規則:
1. 用retain 實現對一個物件的持有 2. 不在需要使用一個物件時,必須用release放棄持有 3. 正在使用的物件不能使用release 4. retain與release成對出現 5.autorelease 延遲release,當沒有被持有時會自動釋放。常用於返回值 6.通過 “&” 返回的物件是沒有所有權的。例如 NSError 7.alloc、new、copy、mutableCopy等方法會返回物件的所有權,而類方法建立的則基本為autorelease

Core Fundation 物件的使用規則:
set和get方法      get:直接返回指標即可      set:由於有新值匯入,為了保證新值在被使用前不會被釋放,需要先給新值做retain操作。然後在對舊值做release,最後將新值賦值給指標      初始化方法和dealloc不要使用 get和set方法,而應該直接初始化指標。(可以將初始化放入get方法中)
迴圈引用規則:      retain就是對一個物件的強引用,在引用被釋放前,該物件是無法被dealloc的,這就可能出現一個迴圈引用的問題:兩個物件相互強引用。      解決方法就是將其中一者的引用變成弱引用。 Cocoa的規則是:父物件建立對子物件的強引用,而子物件只對父物件弱引用。      在Cocoa種,弱引用的例子有:Table的datasource、delegate,Outline試圖專案的Outline view item、觀察者(Notification Observer)和其他的target以及delegate。      當你向一個Notification Center註冊一個物件時,Notification Center對這個物件是弱引用的,並且在有訊息需要通知這個物件時,就傳送訊息給這個物件,當這個物件dealloc的時候,你必須向Notification Center取消這個物件的註冊。這樣,這個Notification Center就不會再發送訊息到這個不存在的物件了。同樣,當一個delegate物件被dealloc的時候,必須向其他物件傳送一個setDelegate:訊息,並傳遞nil引數,從而將代理關係撤銷。這些訊息通常在物件的dealloc方法中發出。
Cocoa的所有權策略:
     返回的物件,在呼叫者的呼叫方法中,始終保持有效。所以在方法內部,不必擔心你收到的返回物件會被dealloc。 但也有 例外:      1.當一個物件從NSarray(Dictionary、Set也一樣)中刪除的時候           a = [array objectAtIndex:n];           [array removeObjectAtIndex:n];           當一個物件從array中被刪除,系統會立即呼叫物件的release方法(而不是autorelease)。如果這個時候,這個array是該物件的唯一屬主,那麼這個物件a會立即被dealloc。           2.當父物件被dealloc的時候           id A = [[A alloc] init];           B = [A child];           [A release];//或者 self.A = nil;           有時候,通過父物件A獲取子物件B,然後間接或直接地release了父物件A。如果父物件A的release造成了它被dealloc,且該父物件A恰好是子物件B的唯一屬主,那麼子物件B就會同時被dealloc(假定父物件的dealloc方法中對子物件傳送的是release訊息,而不是autorelease)。

NSArray、NSDictionary、NSSet容器擁有其包容的物件的所有權      如果一個物件放入了容器中,容器就會獲得該物件的所有權。當容器自己release的時候,或者該物件從容器中刪除時,容器會放棄該物件的所有權。

自動釋放池(Autorelease Pool)      Autorelease Pool 的機制,為你提供了一個“延時”release 物件的機制。當你既想放棄物件所有權,但又不想立即發生放棄行為的時候(比如作為返回值return)。           Autorelease Pool 是NSAutorelease類的一個例項,它是得到了autorelease訊息的物件的容器。在autorelease pool被dealloc的時候,它會自動給池中的物件傳送release訊息。一個物件可以被多次放入到同一個autorelease pool,每一次放入(呼叫 autorelease方法)都會造成將來收到一次release。           多個autorelease pool之間的關係,通常描述是“巢狀關係”,實際上是按照棧的方式工作的(後進先出)。當一個新的autorelease pool建立後,它就位於這個棧頂。當pool被dealloc的時候,就從棧中刪除。當物件呼叫autorelease方法時,實際上它會被放到“這個執行緒”“當時”位於棧頂的那個pool中(由此可以推定,每個執行緒都有一個私有的autorelease pool的棧)。           Cocoa希望程式中長期存在一個 autorelease pool。如果 pool不存在,autorelease的物件就無從 release了,從而造成記憶體洩露。當程式中沒有autorelease pool,你的程式還在 呼叫物件的autorelease方法,就會發出一個錯誤日誌。AppKit和UIkit框架自動在每個訊息迴圈的開始都建立一個池(比如滑鼠按下時間、觸控事件)並在結尾處銷燬這個pool。正因此如此,你實際上不需要建立autorelease pool,甚至不用知道如果建立 autorelease pool。      上面說的是通常情況,下面是例外的情況:      1.如果你寫的程式,不是基於UI Framework。例如你寫的是一個基於命令列的程式。      2.如果你程式中的一個迴圈,在迴圈體重建立了大量的臨時物件。           你可以在迴圈體內部新建一個autorelease pool ,並在一次迴圈結束時銷燬這些臨時物件。這樣可以減少程式對記憶體的佔用峰值。      3.如果你發起了一個secondary執行緒(main執行緒以外的執行緒)。這時你“必須”線上程的最初執行程式碼中建立autorelease pool,否則你的程式就記憶體洩露了。             通常我們用alloc和init來新建一個NSAutoreleasePool物件,用drain訊息來銷燬這個pool。如果你呼叫 pool物件的autorelease 或者retain方法,會出現程式異常。Autorelease Pool 必須在其所“誕生”的上下文環境(對方法或函式的調用出,或者迴圈體的內部)中 進行drain。      Autorelease Pool 必須用 inline(作為區域性變數) 的方法使用,你永遠不需要把一個這樣的pool作為成員變數來處理。
使用本地Autorelease Pool來減少記憶體佔用峰值      許多程式所用的臨時物件都是autorelease的,因此這些物件在pool被銷燬錢是佔用記憶體的。想要減少對記憶體的佔用峰值,就應該使用本地的autorelease pool。當pool被銷燬時,那些臨時物件都會被release。       Demo:      NSArray *urls = <# 一個包含url的陣列 #> ;      for(NSURL *url in urls ) {           NSAutoreleasePool *loopPool =[[NSAutoreleasePool alloc] init];           NSError *error = nil;           NSString *fileContents = [[[NSString alloc] initWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error]autorelease];           // 使用這些string           [loopPool drain];      }            這個 for 迴圈每次處理一個檔案。從迴圈體的開始,建立一個池,在迴圈結束時將這個pool銷燬掉,在pool中的所有autorelease物件都會被release。      在 autorelease pool已經dealloc之後,pool中的物件被視為失效,而不要再使用他們,或者把他們作為返回值返回。如果你必須在autorelease之後還要使用某個臨時物件,你可以呼叫該物件的retain方法,然後等這個pool已經呼叫了drain後,再次呼叫autorelease。 Demo:
- (id)findMatchingObject:(id)anObject {      id match = nil;      while (match == nil) {           NSAutoreleasePool *subPool = [[NSAutoreleasePool alloc] init];           //建立大量臨時資料           match = [self expensiveSearchForObject:anObject];           if (match != nil) {                [match retain];           }                    [subPool drain];      }      return [match autorelease]; }          在subPool有效的時候,我們呼叫 match物件的retain方法。在subPool被drain之後,我們又呼叫了match的autorelease方法。通過這幾步後,match沒有進入subPool,而是進入了比subPool更早的一個autorelease pool。這樣實際上是延長了match 物件的生命週期,使得 match 可以在迴圈體之外也能被使用,還是得match可以作為返回值返回給findMatchingObject的呼叫者。       Autorelease Pool 和執行緒      Cocoa程式的每一個執行緒都維護著一個自己的NSAutorelease物件的棧。當執行緒結束的時候,這些Pool物件就會被release。如果你寫的程式僅僅是一個基於Fondation的程式,又或者你detach一個執行緒(關於detached thread,請參考 Threading Programming Guide),你需要新建一個你自己的autorelease pool。      如果你的程式是一個長期執行的程式,可能會產生大量的臨時資料,這是你必須週期性地銷燬、新建 autorelease pool (Kit 在主執行緒中就是這麼做的),否則 autorelease 物件就會積累並吃掉大量記憶體。如果你detached執行緒不呼叫Cocoa,你就不必新建autorelease pool。       注意:除非是Cocoa 運行於多執行緒模式,否則如果你使用POSIX執行緒 API 來啟動一個secondary執行緒,而不是使用NSThread,你是不能使用Cocoa的,當然也就不能使用NSAutorelease。Cocoa只有在detach了它的第一個NSThread物件之後,才能進入多執行緒模式。為了在secondary POSIX 執行緒中使用Cocoa,你的程式首先要做的是 detach 至少1個NSThread,然後立即結束這個執行緒。你可以用NSThread的isMultiThreaded 方法來檢測Cocoa 是否處於執行緒模式、
Autorelease Pool的作用域(Scope)      一個autorelease pool的作用域實際是由它在棧中的位置所決定的。最頂上的pool,就是當下存放autorelease物件的pool。如果這時新建了一個pool,原有的pool就離開了scope,直到這個new pool 被drain後,原有的pool 才會再次回到最頂端,進入scope。drain後的pool,就永遠不再Scope了。           如果你drain 一個pool,但這個pool並不是棧頂,那麼棧內位於它上面的所有pool都drain了(這意味著所有他們容納的物件都將呼叫release訊息)。如果你不小心忘記了drain一個pool,那從巢狀結構上來看,當更外一層pool drain的時候,會摧毀這個pool。            這種特性,對於出現程式執行異常的情形是有用的。當異常出現,系統立即從當前執行的程式碼中跳出,當前現場有效的pool被 drain。然而一旦這個pool不是棧頂的那個pool,那麼它上面的pool都會被 drain。最終,比這個pool更早的pool會成為棧頂。這種行為機制,使得Exception Handler 不必處理異常發生所在現場autorelease物件的release工作。對於Exception Handler而言,既不希望也不必要為autorelease pool 中的物件的release方法,除非 handler is re-raising the exception(原文沒翻譯,我也沒看懂,記錄下)
記憶體垃圾回收      記憶體垃圾回收系統(Garbage Collection Programming Guide 中 講述)並不使用 autorelease pool。      如果你開發的是一個混合框架(既用到了記憶體垃圾回收,還用到了引用計數器),那麼autorelease pool 為垃圾 記憶體回收者提供了線索。當autorelease pool 進行release 的時候,恰恰是提示垃圾記憶體回收者現在需要回收記憶體。      在垃圾回收的環境中,release 實際上什麼都不做。正因為如此,NSAutoreleasePool 才提供了一個drain 方法。這個方法在引用計數環境下等同於release,但在垃圾回收環境下,觸發了記憶體回收的行為(前提是此時記憶體新分配的數量,超過了閥值)。所以如果摧毀autorelease pool時候應該用drain,而不是release。
關於dealloc 1. 永遠不需要手動呼叫dealloc 2. 結尾必須呼叫super的dealloc 3. 不可將系統的資源與物件的生命週期繫結。(比如不能將關鍵系統資源在物件被dealloc的時候才釋放) 4. 程序的記憶體會在退出時自動回收。當應用退出,物件可能收不到dealloc訊息。和呼叫所有物件的記憶體管理方法比起來,系統的回收效率更高。 5. 不要用dealloc管理關鍵系統資源(檔案控制代碼、網路連線、快取等)。因為你無法設計出一個類,想讓系統什麼時候呼叫dealloc就什麼時候呼叫(你能做的只是release,至於release是否會導致系統一定呼叫dealloc,還需要看物件是否有其他屬主)。由於系統性能的下降、自身的Bug,有可能推遲或擱置dealloc的呼叫。      1.物件圖的拆除順序問題。           實際上,物件圖的拆除是沒有任何順序保證的。也許你認為、希望有一個具體明確的順序,實際上是無法預計的。      2.系統稀缺資源不能回收。           例如記憶體洩露問題,檔案控制代碼被用光。      3.釋放資源的操作由其他執行緒來做。           如果一個物件在一個不確定的時刻被放入了autorelease池中。它將被執行緒池中的執行緒來dealloc。這對於只能提供單一執行緒訪問的資源而言,是致命的錯誤。          正確的做法:如果物件管理了稀缺資源,它必須知道它什麼時候不再需要這些資源,並在此時立即釋放資源。通常情況下,此時,你會呼叫release來delloc,但因為此前你已經釋放了資源,這裡就不會遇到任何問題。