一、簡介
OC 在創建對象時,不會直接返回該對象,而是返回一個指向對象的指針。
OC 在內存管理上采用了引用計數,它是一個簡單而有效管理對象生命周期的方式。在對象內部保存一個用來表示被引用次數的數字,init、new 和 copy 都會讓計數 +1,調用 release 讓計數 -1。當計數等於 0 的時候,系統調用 dealloc 方法來銷毀對象。
A * a = [[A alloc] init]; // retain count = 1
A * b = a; // 指針賦值時,retain count 不會自動增加
[b retain]; // retain count = 2
{
OBJC_EXTERN int _objc_rootRetainCount(id);
NSObject * obj = [[NSObject alloc] init];
// 創建對象並引用,引用計數為 1
NSLog(@"obj retainCount:%lu", (unsigned long)_objc_rootRetainCount(obj));
NSObject * obj1 = [[NSObject alloc] init];
// 創建對象並引用,引用計數為 1
NSLog(@"obj1 retainCount:%lu", (unsigned long)_objc_rootRetainCount(obj1));
// obj 指向了 obj1 所指的對象 B,失去了對原來對象A的引用,所以對象A的引用計數-1,為 0。A 被銷毀
// 對於 B,obj 引用了它,所以引用計數 +1,為 2
obj = obj1;
// self.obj 又引用了 A,所以引用計數 +1,為 3
self.obj = obj;
NSLog(@"strong obj1 retainCount:%lu",(unsigned long)_objc_rootRetainCount(obj1));
NSLog(@"strong obj retainCount:%lu",(unsigned long)_objc_rootRetainCount(obj));
}
引用計數分為自動引用計數「ARC : Automatic Reference Counting」和手動引用計數「MRC : Manual Reference Counting」。
二、原理
三、示例
NSObject * obj1 = [NSObject new];
NSLog(@"引用計數: %lu", (unsigned long)[obj1 retainCount]);
NSObject * obj2 = [obj1 retain];
NSObject * obj3 = [obj1 retain];
NSLog(@"引用計數: %lu", (unsigned long)[obj1 retainCount]);
[obj1 release];
NSLog(@"引用計數: %lu %@", (unsigned long)[obj1 retainCount], obj1);
[obj1 release];
NSLog(@"引用計數: %lu %@", (unsigned long)[obj1 retainCount], obj1);
[obj1 release];
NSLog(@"引用計數: %lu %@", (unsigned long)[obj1 retainCount], obj1);
引用計數:1
引用計數:3
引用計數:2 <NSObject:0x60400001ecd0>
引用計數:1 <NSObject:0x60400001ecd0>
*** -[NSObject retainCount]: message sent to deallocated instance 0x60400001ecd0
根據 Debug 輸出可以看到:obj1 可以調用多次 release 方法。
從兩次打印 obj1 的地址相同可以猜測,在 [obj1 release] 執行之后對象的引用計數 -1,不再強引用對象,但 obj1 仍然指向對象所在的那片內存空間。在第三次執行 release 后,對象的引用計數為 0,對象所在的內存空間被銷毀,但是 obj1 指針仍然存在,此時調用 retainCount 會報野指針錯誤。可以通過置 obj1 = nil 解決這個問題。
對 Linux 文件系統比較了解的可能發現,引用計數的這種管理方式類似於文件系統里面的硬鏈接。在 Linux 文件系統中,我們用 ln 命令可以創建一個硬鏈接(相當於 retain),當刪除一個文件時(相當於 release),系統調用會檢查文件的 link count 值,如果大於 1,則不會回收文件所占用的磁盤區域。直到最后一次刪除前,系統發現 link count 值為 1,則系統才會執行直正的刪除操作,把文件所占用的磁盤區域標記成未用。
四、僵屍對象、野指針、空指針
僵屍對象:所占用內存已經被回收的對象,僵屍對象不能再使用。
野指針:指向僵屍對象(不可用內存)的指針,給野指針發送消息會報錯(EXC_BAD_ACCESS)。
空指針:沒有指向任何對象的指針(存儲的是 nil、NULL),給空指針發送消息不會報錯;空指針的一個經典使用場景就是在開發中獲取服務器 API 數據時,轉換野指針為空指針,避免發送消息報錯。
五、為什么需要引用計數?
引用計數真正派上用場的場景是在面向對象的程序設計架構中,用於對象之間傳遞和共享數據。
舉個例子:
對象 A 生成了一個對象 O,需要調用對象 B 的某個方法,並將對象 O 作為參數傳遞過去。
[objB doSomething:O];
在沒有引用計數的情況下,一般內存管理的原則是「誰申請誰釋放」。
那么對象 A 就需要在對象 B 不再需要 O 的時候,將 O 銷毀。但對象 B 可能臨時用一下 O,也可能將它設置為自己的一個成員變量,在這種情況下,什么時候銷毀就成了一個難題了。
對於以上情況有兩種做法:
對象 A 在調用完對象 B 的某個方法之后,馬上銷毀參數 O;然后對象 B 需要將對象 O 復制一份,生成另一個對象 O2,同時自己來管理對象 O2 的生命周期。
這種做法帶來更多的內存申請、復制、釋放的工作。本來可以復用的對象,因為不方便管理它的生命周期,就簡單地把它銷毀,又重新構造一份一樣的,實在太影響性能。
對象 A 只負責生成 O,之后就由對象 B 負責完成 O 的銷毀工作。如果對象 B 只是臨時用一下 O,就可以用完后馬上銷毀;如果對象 B 需要長時間使用 O,就不銷毀它。
這種做法看似解決了對象復制的問題,但是它強烈依賴於 A 和 B 兩個對象的配合,代碼維護者需要明確地記住這種編程約定。而且,由於 O 的生成和釋放在不同對象中,使得它的內存管理代碼分散在不同對象中,管理起來也很費勁。如果這個時候情況更加復雜一些,例如對象 B 需要再向對象 C 傳遞參數 O,那么這個對象在對象 C 中又不能讓對象 C 管理。所以這種方法帶來的復雜度更高,更加不可取。
引用計數的出現很好地解決這個問題,在參數 O 的傳遞過程中,哪些對象需要長時間使用它,就把它的引用計數 +1,使用完就-1。所有對象遵守這個規則,對象的生命周期管理就可以完全交給引用計數了。我們也可以很方便地享受到共享對象帶來的好處。
六、ARC 下的內存管理問題
問題主要體現在:
- 過度使用 block 之后,無法解決循環引用問題。
- 遇到底層 Core Foundation 對象,需要手工管理它們的引用計數時,顯得一籌莫展。
6.1 循環引用
引用計數這種管理內存的方式雖然很簡單,但是有一個比較大的瑕疵,即它不能很好的解決循環引用問題。如下圖所示:對象 A和對象 B,相互引用了對方作為自己的成員變量,只有當自己銷毀時,才會將成員變量的引用計數減 1。因為對象 A 的銷毀依賴於對象 B 銷毀,而對象 B 的銷毀又依賴於對象 A 的銷毀,這樣就造成了循環引用 Reference Cycle 的問題,這兩個對象即使在外界已經沒有任何指針能夠訪問到它們了,它們也無法被釋放。
不止兩對象存在循環引用問題,多個對象依次持有對方,形式一個環狀,也可以造成循環引用問題,而且在真實編程環境中,環越大就越難被發現。下圖是 4 個對象形成的循環引用問題。
6.2 主動斷開循環引用
解決循環引用問題主要有兩個辦法。第一個辦法:明確知道這里會存在循環引用,在合理的位置主動斷開環中的一個引用,使得對象得以回收。如下圖所示:
主動斷開循環引用這種方式常見於各種與 block 相關的代碼邏輯中。
不過,主動斷開循環引用這種操作依賴於程序員自己手工顯式地控制,相當於回到了以前 “誰申請誰釋放” 的內存管理年代,它依賴於程序員自己有能力發現循環引用並且知道在什么時機斷開循環引用回收內存,所以這種解決方法並不常用,更常見的辦法是使用弱引用的辦法。
6.3 使用弱引用
弱引用雖然持有對象,但是並不增加引用計數,這樣就避免了循環引用的產生。在 iOS 開發中,弱引用通常在 delegate 模式中使用。如下所示:
6.4 弱引用的實現原理
弱引用的實現原理是這樣,系統對於每一個有弱引用的對象,都維護一個表來記錄它所有的弱引用的指針地址。這樣,當一個對象的引用計數為 0 時,系統就通過這張表,找到所有的弱引用指針,繼而把它們都置成 nil。
從這個原理中,我們可以看出,弱引用的使用是有額外的開銷的。雖然這個開銷很小,但是如果一個地方我們肯定它不需要弱引用的特性,就不應該盲目使用弱引用。舉個例子,有人喜歡在手寫界面的時候,將所有界面元素都設置成 weak 的,這某種程度上與Xcode 通過 Storyboard 拖拽生成的新變量是一致的。但是我個人認為這樣做並不太合適。因為:
在創建這個對象時,需要注意臨時使用一個強引用持有它,否則因為 weak 變量並不持有對象,就會造成一個對象剛被創建就銷毀掉。
大部分 ViewController 的視圖對象的生命周期與 ViewController 本身是一致的,沒有必要額外做這個事情。
早先蘋果這么設計,是有歷史原因的。在早年,當時系統收到 Memory Warning 的時候,ViewController 的 View 會被 unLoad 掉。這個時候,使用 weak 的視圖變量是有用的,可以保持這些內存被回收。但是這個設計已經被廢棄了,替代方案是將相關視圖的 CALayer 對應的 CABackingStore 類型的內存區會被標記成 volatile 類型,詳見《再見,viewDidUnload方法》。
6.5 檢測循環引用