Objective-C 和 Swift 語言的內(nèi)存管理方式都是基于引用計數(shù)「Reference Counting」的埋哟,引用計數(shù)是一個簡單而有效管理對象生命周期的方式。引用計數(shù)分為自動引用計數(shù)「ARC: Automatic Reference Counting」和手動引用計數(shù)「MRC: Manual Reference Counting」摘完,現(xiàn)在都是用 ARC 了,但是我們還是很有必要了解 MRC。
1. 引用計數(shù)的原理是什么?
當(dāng)我們創(chuàng)建一個新對象時钳榨,他的引用計數(shù)為1;
當(dāng)有一個新的指針指向這個對象時纽门,他的引用計數(shù)就加1薛耻;
當(dāng)對象關(guān)聯(lián)的某個指針不再指向他時,他的引用計數(shù)就減1赏陵;
當(dāng)對象的引用計數(shù)為0時饼齿,說明此對象不再被任何指針指向,這時我們就可以將對象銷毀蝙搔,回收內(nèi)存缕溉。
由于引用計數(shù)簡單有效,除了 Objective-C 語言外吃型,Microsoft 的 COM「Component Object Model」证鸥、C++11(基于引用計數(shù)的智能指針 share_prt)等語言也提供了基于引用計數(shù)的內(nèi)存管理方式。
舉個例子:
新建工程,Xcode 默認(rèn)開啟的是 ARC敌土,我們這里針對「AppDelegate.m」文件使用 MRC镜硕,進行以下配置:
選擇目標(biāo)工程运翼,然后在「Build Phases」的「Compile Sources」下的「AppDelegate.m」文件配置編譯器參數(shù)「Compiler Flags」值為「-fno-objc-arc」
復(fù)制代碼
1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
2? ? NSObject *objO = [NSObject new];
3? ? NSLog(@"Reference Count: %lu", (unsigned long)[objO retainCount]); // 1
4? ? NSObject *objB = [objO retain];
5? ? NSLog(@"Reference Count: %lu", (unsigned long)[objO retainCount]); // 2
6? ? [objO release];
7? ? NSLog(@"Reference Count: %lu", (unsigned long)[objO retainCount]); // 1
8? ? [objO release];
9? ? NSLog(@"Reference Count: %lu", (unsigned long)[objO retainCount]); // 1
10
11? ? [objO setValue:nil forKey:@"test"]; // 僵尸對象返干,向野指針發(fā)送消息會報錯(EXC_BAD_ACCESS)
12
13? ? return YES;
14 }
復(fù)制代碼
Xcode 默認(rèn)不會監(jiān)控僵尸對象,這里我們配置開啟他血淌,然后就可以看到具體的跟蹤信息了:
也可以通過選擇「Product」下的「Profile」來打開「Instruments」工具集矩欠。然后選擇「Zombies」,再單擊右下角的「Choose」按鈕進入檢測界面悠夯,這時點擊左上角的「Record」紅色圓點按鈕開始檢測癌淮。
1.1 上面例子,為什么最后一次通過 retainCount 獲取的值為1沦补,而不是為0呢乳蓄?
因為該對象的內(nèi)存已經(jīng)被回收,我們向一個被回收的對象發(fā)送 retainCount 消息夕膀,他的輸出結(jié)果是不確定的虚倒,如果該對象所占內(nèi)存被復(fù)用了,那么就可能造成程序異常崩潰产舞。
而且當(dāng)最后一次執(zhí)行 release 時魂奥,系統(tǒng)已經(jīng)知道馬上要回收內(nèi)存了,就沒必要再將 retainCount 減1易猫,因為不管減不減1耻煤,該對象都會被回收,回收后他所在內(nèi)存區(qū)域(包括 retainCount 值)就沒有意義了准颓。不將 retainCount 減1變?yōu)?哈蝇,可以減少一次內(nèi)存操作,加快對象的回收攘已。
1.2 什么是僵尸對象炮赦、野指針、空指針呢贯被?
僵尸對象:所占用內(nèi)存已經(jīng)被回收的對象眼五,僵尸對象不能再使用。
野指針:指向僵尸對象(不可用內(nèi)存)的指針彤灶,給野指針發(fā)送消息會報錯(EXC_BAD_ACCESS)看幼。
空指針:沒有指向任何對象的指針(存儲的是 nil、NULL)幌陕,給空指針發(fā)送消息不會報錯诵姜;空指針的一個經(jīng)典使用場景就是在開發(fā)中獲取服務(wù)器 API 數(shù)據(jù)時,轉(zhuǎn)換野指針為空指針搏熄,避免發(fā)送消息報錯棚唆。
2. 為什么需要引用計數(shù)暇赤?
從上面簡單例子,我們還看不出引用計數(shù)真正的用處宵凌,因為該對象的生命周期只是在一個方法內(nèi)鞋囊。在真實的應(yīng)用場景中,我們在方法內(nèi)使用臨時對象瞎惫,通常不需要修改他的引用計數(shù)溜腐,只需要在方法返回前銷毀對象就可以了。
然而瓜喇,引用計數(shù)真正派上用場的場景是在面向?qū)ο蟮某绦蛟O(shè)計架構(gòu)中挺益,用于對象之間傳遞和共享數(shù)據(jù)。
舉個例子:
假如對象 A 生成了一個對象 O乘寒,需要調(diào)用對象 B 的某個方法望众,將對象 O 作為參數(shù)傳遞過去。
在沒有引用計數(shù)的情況下伞辛,一般內(nèi)存管理的原則是「誰申請誰釋放」烂翰,那么對象 A 就需要在對象 B 不再需要對象 O 的時候,將對象 O 銷毀始锚。但對象 B 可能臨時用一下對象 O刽酱,也可以覺得他重要,將他設(shè)置為自己的一個成員變量瞧捌,在這種情況下棵里,什么時候銷毀對象 O 就成了一個難題了。
對于以上情況有兩種做法:
(1)對象 A 在調(diào)用完對象 B 的某個方法之后,馬上銷毀參數(shù)對象 O,然后對象 B 需要將對象 O 復(fù)制一份眯分,生成另一個對象 O2娩梨,同時自己來管理對象 O2 的生命周期清笨。但是這種做法有一個很大的問題,就是他帶來更多的內(nèi)存申請、復(fù)制、釋放的工作柱告。本來可以復(fù)用的對象,因為不方便管理他的生命周期笑陈,就簡單地把他銷毀际度,又重新構(gòu)造一份一樣的,實在太影響性能涵妥。
(2)對象 A 只負責(zé)生成對象 O乖菱,之后就由對象 B 負責(zé)完成對象 O 的銷毀工作。如果對象 B 只是臨時用一下對象 O,就可以用完后馬上銷毀窒所,如果對象 B 需要長時間使用對象 O鹉勒,就不銷毀他。這種做法看似解決了對象復(fù)制的問題吵取,但是他強烈依賴于 A 和 B 兩個對象的配合禽额,代碼維護者需要明確地記住這種編程約定。而且海渊,由于對象 O 的生成和釋放在不同對象中绵疲,使得他的內(nèi)存管理代碼分散在不同對象中哲鸳,管理起來也很費勁臣疑。如果這個時候情況更加復(fù)雜一些,例如對象 B 需要再向?qū)ο?C 傳遞參數(shù)對象 O徙菠,那么這個對象在對象 C 中又不能讓對象 C 管理讯沈。所以這種方法帶來的復(fù)雜度更高,更加不可取婿奔。
引用計數(shù)的出現(xiàn)很好地解決這個問題缺狠,在參數(shù)對象 O 的傳遞過程中,哪些對象需要長時間使用他萍摊,就把他的引用計數(shù)加1挤茄,使用完就減1。所有對象遵守這個規(guī)則冰木,對象的生命周期管理就可以完全交給引用計數(shù)了穷劈。我們也可以很方便地享受到共享對象帶來的好處。
2.1 什么是循環(huán)引用「Reference Cycles」問題踊沸,怎么解決呢歇终?
引用計數(shù)這種內(nèi)存管理方式雖然簡單,但有一個瑕疵就是他不能自動解決循環(huán)引用的問題逼龟。
舉個例子:
對象 A 和對象 B 相互引用對方作為自己的成員變量评凝,只有當(dāng)自己銷毀時,才將自己的成員變量的引用計數(shù)減1腺律,因為對象 A 和對象 B 的銷毀相互依賴奕短,這樣就造成我們所說的循環(huán)引用問題了。
循環(huán)引用會導(dǎo)致即使外界已經(jīng)沒有任何指針能夠訪問他們了匀钧,但是他們所占資源仍然無法釋放的情況翎碑。
解決循環(huán)引用問題主要有兩種方法:
(1)明確知道哪里存在循環(huán)引用,合理時機主動斷開環(huán)中的一個引用榴捡,使得對象得以回收杈女。這種方法不常用,因為他依賴開發(fā)人員自己手工顯式控制,相當(dāng)于回到以前「誰申請誰釋放」的內(nèi)存管理年代达椰。
(2)使用弱引用「Weak Reference」翰蠢,「weak」「__weak」類型,這種方法常用啰劲。弱引用雖然持有對象梁沧,但是并不增加他的引用計數(shù)。弱引用的一個經(jīng)典使用場景就是委托代理「delegate」協(xié)議模式蝇裤。
2.2 Xcode 中有什么工具可以檢測循環(huán)引用嗎廷支?
在 Xcode 中有「Instruments」工具集可以很方便地檢測循環(huán)引用。
舉個例子:
復(fù)制代碼
1 - (void)viewDidLoad {
2? ? [super viewDidLoad];
3
4? ? NSMutableArray *mArrFirst = [NSMutableArray array];
5? ? NSMutableArray *mArrSecond = [NSMutableArray array];
6? ? [mArrFirst addObject:mArrSecond];
7? ? [mArrSecond addObject:mArrFirst];
8 }
復(fù)制代碼
可以選擇「Product」下的「Profile」來打開「Instruments」工具集栓辜。
然后選擇「Leaks」恋拍,再單擊右下角的「Choose」按鈕進入檢測界面,這時點擊左上角的「Record」紅色圓點按鈕開始檢測藕甩。
3. Core Foundation 對象的內(nèi)存管理
ARC 是編譯器特性施敢,他不是運行時特性,更不是垃圾回收器「GC」狭莱。
ARC 能夠解決 iOS 開發(fā)中90%的內(nèi)存管理問題僵娃,但是另外10%的內(nèi)存管理問題是需要開發(fā)人員自己處理的,這主要是與底層 Core Foundation 對象交互的部分腋妙,底層 Core Foundation 對象由于不在 ARC 的管理下默怨,所以需要自己維護這些對象的引用計數(shù)。
實際上 Core Foundation 對象使用的 CFRetain 和 CFRelease 方法骤素,可以認(rèn)為與 Objective-C 對象的 retain 和 release 方法等價匙睹,所以我們可以以 MRC 的方式進行類似管理。
3.1 在 ARC 中谆甜,通過什么方式可以把 Core Foundation 對象轉(zhuǎn)換為 Objective-C 對象呢垃僚?
轉(zhuǎn)換的過程,其實是告訴編譯器规辱,對象的引用計數(shù)如何調(diào)整谆棺。
這里我們可以使用橋接「bridge」相關(guān)關(guān)鍵字來進行轉(zhuǎn)換工作,以下是這些(雙下劃線)關(guān)鍵字的說明:
(1)__bridge:只做類型轉(zhuǎn)換罕袋,不修改相關(guān)對象的引用計數(shù)改淑,原來的 Core Foundation 對象在不用時,需要調(diào)用 CFRelease 方法浴讯。
(2)__bridge_retained:類型轉(zhuǎn)換后朵夏,將相關(guān)對象的引用計數(shù)加1,原來的 Core Foundation 對象在不用時榆纽,需要調(diào)用 CFRelease 方法仰猖。
(3)__bridge_transfer:類型轉(zhuǎn)換后捏肢,將相關(guān)對象的引用計數(shù)交給 ARC 管理,原來的 Core Foundation 對象在不用時饥侵,不需要調(diào)用 CFRelease 方法鸵赫。
我們根據(jù)具體的業(yè)務(wù)邏輯,合理使用上面的三種轉(zhuǎn)換關(guān)鍵字躏升,就可以解決 Core Foundation 對象與 Objective-C 對象相對轉(zhuǎn)換的問題了辩棒。