【iOS】自動引用計數 (循環引用)


歷史版本

ARC(Automatic Reference Counting,自動引用計數)極大地減少了Cocoa開發中的常見編程錯誤:retainrelease不匹配。ARC並不會消除對retainrelease的調用,而是把這項原本大都屬於開發者的工作移交給了編譯器。這樣做的好處是顯而易見的,但是必須知道retainrelease是仍然在使用的。ARC並不等同垃圾回收。思考下面這段代碼,它對一個實例變量賦值:

1
2
3
@property (nonatomic, readwrite, strong) NSString *title;
...
_title = [NSString stringWithFormat:@"Title"];

在非ARC環境中,_title沒有被保留過,那么賦給它的NSString就是autorelease的,會在事件循環結束時被釋放。之后如果再次訪問_title,程序就會崩潰。這類錯誤極其常見,調試起來會異常困難。另外,如果_title之前已經有一個值,那舊的值就會因為沒有被釋放掉而引起內存泄漏。

使用ARC,編譯器會在合適的位置自動插入一些代碼,下面的代碼與前面的代碼是等同的:

1
2
3
4
id oldTitle = _title;
_title = [NSString stringWithFormat:@"Title"];
[_title retain];
[oldTitle release];

retainrelease仍然會被調用,所以有一些開銷,在release的時候可能還會調用dealloc方法。這段代碼與程序員手動調用retainrelease的代碼在運行結果上是完全一致的。垃圾回收機制是在運行時起作用的,會影響運行效率,而ARC是在編譯時插入內存管理代碼,不影響運行時效率,因此內存回收比垃圾回收時的效率要高,能夠提升系統性能。正如其他一些編譯器優化方式一樣,這種編譯器可以自由地以多種方式優化內存管理,而讓程序員手動去做這些工作是不現實的。在多數情況下,使用ARC生成內存管理代碼的程序比程序員手工添加內存管理代碼的對等程序運行更快!

ARC不是垃圾回收,尤其是它不能像Snow Leopard中的垃圾回收機制那樣處理循環引用(保留)。圖3-1中的對象A與對象B之間存在循環保留。

圖3-1 循環保留

在Snow Leopard的垃圾回收機制下,如果從“外部對象”到“對象A”的引用鏈接中斷了,對象A和對象B都會被銷毀,因為它們從程序中孤立出去了。而在ARC中,對象A和對象B都不會被銷毀,因為它們的引用計數都大於0。因此,在iOS開發中,必須要做好對強引用(strong reference)的跟蹤管理以免出現循環引用。

屬性關系有兩種主要類型:strongweak,相當於非ARC環境里的retainassign。只要存在一個強引用,對象就會一直存在,不會被銷毀。強引用類似於C++中的shared_ptr,只不過管理引用計數的代碼是在編譯時生成的,而shared_ptr是在運行時通過操作符重載確定的。

Objective-C中一直存在循環引用的問題,但在實際應用中很少出現循環引用。對於過去那些使用assign屬性的地方,在ARC環境中要使用weak代替。大部分引用循環是由委托(delegate)引起的,所以應該總是把delegate屬性聲明為weak。當引用的對象被銷毀之后,weak引用會被自動置為nil,與assign相比這是一個巨大的進步,因為assign可以指向被釋放掉的內存,導致程序崩潰。


在ARC出現以前,合成屬性(synthesized property)的默認存儲類型是assign;而在ARC中,默認的存儲類型是strong。這在進行代碼轉換時可能會造成一些困惑。建議為所有屬性顯式指明存儲類型。 循環保留產生的另一個主要原因是塊,其介紹詳見第23章。


將代碼轉換到ARC的時候,要注意以下兩點。

  •   不要再使用retainreleaseautorelease,可以直接將這些代碼刪除。ARC在編譯時會自動在合適的位置插入內存管理代碼。
  •   如果dealloc方法只是用於釋放實例變量,那么dealloc方法也沒有存在的必要了,可以直接刪掉。在任何情況下都不需要再使用release,ARC會自動處理好這些事情。如果仍然需要使用dealloc來做一些其他的事情(比如移除KVO觀察),記得不要再調用[super dealloc],否則編譯器會給出錯誤信息。

正如前面所說的,ARC並不是垃圾回收,它是編譯器的一種功能,可以在代碼中合適的位置自動插入調用retainrelease的語句。這也意味着,如果現存的使用手動內存管理方式的代碼很好地遵守了命名約定,那么這些代碼跟ARC就是完全互通的。舉例來說,如果調用了一個名為copySomething的方法,ARC會認為這個方法會對返回對象的引用計數加1,如果有必要,ARC會在合適的位置插入一個release。引用計數的加1操作既可以在ARC代碼中進行,也可以在某段手動進行內存管理的代碼中進行,這對於ARC來說是無所謂的。

但是,如果沒有遵守Cocoa命名約定,這種情況下就會出錯。比如有一個名為copyRight的方法,它以autorelease的形式返回一個版權信息的字符串,這種情況下ARC的行為取決於調用代碼和被調用代碼是否都是使用ARC進行編譯的。

方法名是以copy開頭的,因此ARC認為copyRight方法會對返回對象的引用計數加1。如果copyRight方法的代碼與調用copyRight方法的代碼都是使用ARC編譯的,ARC就會在copyRight方法代碼中添加一個retain語句,同時在調用copyRight方法的代碼中添加一個release語句,這種情況下程序依然會正常工作。雖然對執行效率有一點點影響,但是程序不會崩潰,也不會造成內存泄漏。

然而,如果調用copyRight方法的代碼是使用ARC編譯的(ARC就會在代碼后邊添加一個release語句),而copyRight方法代碼沒有使用ARC編譯的話(方法體中就沒有retain語句),程序就會崩潰。反過來,如果copyRight方法代碼使用ARC編譯(ARC就會在方法代碼中添加一個retain語句),而調用代碼沒有使用ARC編譯的話(調用代碼中就不會有release語句),代碼就會造成內存泄漏。

最好的解決辦法就是遵守Cocoa的命名約定。在這個例子中,如果將方法名改為copyright,就可以完全避免這種問題。ARC根據駝峰式大小寫方式對方法的名稱進行單詞分隔,然后使用相應的內存管理規則。

如果對一個錯誤命名的方法進行重命名是不現實的,那么還可以在方法聲明中使用NS_RETURNS_RETAINED或者NS_RETURNS_NOT_RETAINED修飾符來告訴編譯器應該使用哪種內存管理規則。這些修飾符是在NSObjCRuntime.h中定義的。

為了讓編譯器能夠恰當地添加retainrelease語句,編寫ARC代碼時需要注意以下四點。

  • 不要調用retainreleaseautorelease。直接將這些代碼刪除,這是最簡單的規則。另外,也不可以覆蓋這幾個方法(永遠不要試圖去覆蓋這幾個方法)。如果你想實現Singleton模式,請閱讀第4章,了解如何在不覆蓋這些方法的情況下實現Singleton模式。

  • C語言結構體中不要有對象指針。這種情況很少出現。但是如果C語言結構體中確實有一個對象,要么把它存儲到另一個對象中,要么就把它轉換成void*類型(下一條規則會介紹如何轉換到void*)。可以在任何時候通過調用free方法銷毀一個C語言結構體,這會影響到對結構體中對象的自動跟蹤。

  • idvoid*類型只能通過橋接轉換進行轉換。在使用了Core Foundation的代碼中會有很多這種情況。第27章會詳細介紹橋接轉換及如何結合ARC來使用。

  • 不要使用NSAutoreleasePool。不要手動創建自己的自動釋放池,直接把需要自己的池的代碼放入@autoreleasepool{}代碼塊中即可。如果你有一些特殊的代碼用於控制自動釋放池的釋放,那些代碼實際上很可能是不必要的。@autoreleasepoolNSAutoreleasePool快了大約20倍。

對於大部分代碼來說,只要遵守這些規則就不會有任何問題。在Xcode中有一個小工具可以替你完成大部分代碼轉換的工作,它位於Edit → Refactor → Convert to Objective-C ARC。

ARC應該是Objective-C中繼自動釋放池之后最重要的改進了,應該盡可能地使用。如果不能將代碼全部轉換到ARC,就盡可能地轉換。使用ARC編譯的程序速度更快,bug更少,也比手動內存管理代碼更容易編寫。現在就開始使用ARC吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM