Objective-C內存管理之引用計數

  初學者在學習Objective-c的時候,很容易在內存管理這一部分陷入混亂狀態,很大一部分緣由是沒有弄清楚引用計數的原理,搞不明白對象的引用數量,這樣就固然沒法完全釋放對象的內存了,蘋果官方文檔在內存管理這一部分說的很是簡單,只有三條準則:程序員

  1. 當你使用new、alloc或copy方法建立一個對象時,該對象的保留指針爲1,當再也不使用該對象的時候,你應該想該對象發送一條release或autorelease消息,這樣,該對象在其壽命結束時將被銷燬。
  2. 當你經過其餘方法得到一個對象時,假設該對象的保留計數器爲1,並且已經設置爲自動釋放,那麼你不須要執行任何操做來確保該對象被銷燬。若是你打算在一段時間內擁有該對象,則須要保留它並確保它在操做完成時釋放它。
  3. 若是你保留了某個對象,就須要(最終)釋放或自動釋放該對象。必須保持retain方法和release方法的使用次數相同。

  若是在寫代碼的時候遵照這些準則,能夠避免內存泄露,可是若是僅靠對這些準則的「記憶」來寫代碼的話,恐怕本身內心都不會有底,一旦遇到問題分析問題的時候很難從根本上找到問題出現的緣由,本文分享了本身在理解引用計數時的分析過程,結合相關圖形,但願能讓你們深入理解對象引用計數的原理。objective-c

遇到了問題?分析而後測試

  當前對象的引用計數是多少呢?數據結構

     爲何要提出這個問題,由於不少人會搞混對象的指針數量與引用數量的關係,不理解這個問題就弄不明白對象的引用計數到底爲多少,固然就沒法正確釋放內存了。在說這個以前先簡單瞭解一下堆內存與棧內存的概念,學習

  1. 棧內存:由編譯器負責分配,存放局部環境中定義的基本變量的值,例如方法中的基本變量等,離開局部環境時會由編譯器自動釋放其內存空間。
  2. 堆內存: 通常由程序員經過new或alloc等來手動分配,使用完後也須要程序員手動釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事。

     變量名其實是一個符號地址,在對程序編譯鏈接時由系統給每個變量名分配一個內存地址。在程序中從變量中取值,其實是經過變量名找到相應的內存地址,從其存儲單元中讀取數據。指針是一個特殊的變量,由於它存放的是一個變量的地址。以下圖所示:測試

 

  上面這個內存模型相信你們都知道,指針與對象存在一個間接(指向)的關係,所以當指針指向一個對象的時候,不少人就會以爲這個指針引用到了該對象,進而就認爲當指針指向一個對象的時候,該對象的引用計數就會加1,這種理解是一種感性的理解。實際上對於一個對象來講,它是不知道指向它的指針有多少個的,它的釋放僅僅依靠的是引用計數,那麼什麼是引用計數呢?在objective-c中,大部分對象都繼承於NSObject,NSObject包含一個用來保存引用數量的字段retainCount,說白了該字段就是引用計數,NSObject類的部分定義以下:
- (id)retain;
- (oneway void)release;
- (id)autorelease;
- (NSUInteger)retainCount;
- (NSString *)description;

所以,爲了便於理解,咱們能夠把NSObject簡化爲以下模型:spa

 

  對象可否釋放就是判斷其引用次數是否爲零,也就是判斷該對象的retainCount字段是否等於0,而指向該對象指針數量跟該對象retainCount字段的值並無關係,所以指針數量並不等於引用數量,當指針指向該對象的時候,僅僅是給該指針變量賦值了,並無修改對象的retainCount值,所以,指針指向一個對象的時候,該對象的引用計數是沒有改變的。3d

  以上面那段代碼爲例,咱們調用Test對象的new方法的時候,會自動將retainCount的值設置爲1,當咱們將test1賦值給test2的時候,只是一個指針賦值,並無修改對象的retainCount值,因此引用計數不變,依舊爲1。指針

測試用例:code

 1         Engine *engine1 = [Engine new];
 2         NSLog(@"經過new消息建立對象engine1:");
 3         //輸出engine1指針地址
 4         NSLog(@"engine1 address is %p.",engine1);
 5         //輸出engine1的retainCount
 6         NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]);
 7         
 8         Engine *engine2 = engine1;
 9         NSLog(@"將指針engine1複製給指針engine2:");
10         //輸出engine2指針地址
11         NSLog(@"engine2 address is %p.",engine2);
12         //輸出engine1的retainCount
13         NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]);
14         //輸出engine2的retainCount
15         NSLog(@"engine2 retainCount is %lu",(unsigned long)[engine2 retainCount]);
16         
17         Engine *engine3 = [engine1 retain];
18         NSLog(@"經過retain消息得到對象engine3:");
19         //輸出engine3指針地址
20         NSLog(@"engine3 address is %p.",engine3);
21         //輸出engine1的retainCount
22         NSLog(@"engine1 retainCount is %lu",(unsigned long)[engine1 retainCount]);
23         //輸出engine2的retainCount
24         NSLog(@"engine2 retainCount is %lu",(unsigned long)[engine2 retainCount]);
25         //輸出engine3的retainCount
26         NSLog(@"engine3 retainCount is %lu",(unsigned long)[engine3 retainCount]);
27         
28         [engine2 release];
29         NSLog(@"給對象engine2發送消息release");
30         NSLog(@"engine2 address is %p.",engine2);
31         NSLog(@"engine2 retainCount is %lu.",(unsigned long)[engine2 retainCount]);

輸出結果以下:對象

  從上面的輸出結果能夠得出如下幾點結論:

  1. 和本文一開始分析得出的結果同樣,經過指針賦值並不能改變對象的引用計數。
  2. 不管是經過指針賦值仍是經過retain得到對象,它們都指向同一個內存地址,即:指向同一個對象
  3. 在對象的引用計數歸零以前,全部指向它的指針都是可用的。經過某個指針發送release消息僅僅是讓引用計數減一,該指針自己不會被銷燬。

  由於這裏須要輸出引用計數,就沒有采用ARC,因此會有一個小問題,那就是當退出局部環境的時候,即便局部指針所指向的對象已被銷燬,局部指針變量的值仍然沒有改變,所以須要手動賦值爲nil。若是採用ARC的話,會自動回收內存並將指針賦值爲nil。

總結

  不論是直接經過指針賦值仍是經過retain或者copy來保留對象,都會增長指向對象的指針數量,這些指針都指向同一塊內存地址,由於對象所分配的內存地址是不變的。

  指向對象的指針的多少跟引用計數沒有任何關係,可是經過retain、copy或release能夠改變對象的引用計數。

  僅當引用計數爲0時纔會釋放對象佔用的內存空間。  

  哎,真是「落花有意流水無情」啊,哪怕再多的指針「愛上對象」,人家這輩子卻也只認引用計數。

相關文章
相關標籤/搜索