理解 iOS 的內(nèi)存管理

引言:

ARC的出生及成長背景

蘋果在 2011 年的時候,在 WWDC 大會上提出了自動的引用計數(shù)(ARC)艰躺。ARC 背后的原理是依賴編譯器的靜態(tài)分析能力,通過在編譯時找出合理的插入引用計數(shù)管理代碼,從而徹底解放程序員搬男。

在 ARC 剛剛出來的時候,業(yè)界對此黑科技充滿了懷疑和觀望彭沼,加上現(xiàn)有的 MRC 代碼要做遷移本來也需要額外的成本缔逛,所以 ARC 并沒有被很快接受。直到 2013 年左右姓惑,蘋果認為 ARC 技術(shù)足夠成熟褐奴,直接將 macOS(當(dāng)時叫 OS X)上的垃圾回收機制廢棄,從而使得 ARC 迅速被接受于毙。

2014 年的 WWDC 大會上敦冬,蘋果推出了 Swift 語言,而該語言仍然使用 ARC 技術(shù)望众,作為其內(nèi)存管理方式匪补。

以下是引用唐巧大神的話:

為什么我要提這段歷史呢?就是因為現(xiàn)在的 iOS 開發(fā)者實在太舒服了烂翰,大部分時候夯缺,他們根本都不用關(guān)心程序的內(nèi)存管理行為。但是甘耿,雖然 ARC 幫我們解決了引用計數(shù)的大部分問題踊兜,一些年輕的 iOS 開發(fā)者仍然會做不好內(nèi)存管理工作。他們甚至不能理解常見的循環(huán)引用問題佳恬,而這些問題會導(dǎo)致內(nèi)存泄漏捏境,最終使得應(yīng)用運行緩慢或者被系統(tǒng)終止進程于游。

所以,我們每一個 iOS 開發(fā)者垫言,需要理解引用計數(shù)這種內(nèi)存管理方式贰剥,只有這樣,才能處理好內(nèi)存管理相關(guān)的問題筷频。

ARC 出現(xiàn)之前的 MRC 時代

MRC 時期蚌成,前輩們是這樣寫 iOS 代碼的

我們先寫好一段 iOS 的代碼,然后屏住呼吸凛捏,開始運行它担忧,不出所料,它崩潰了坯癣。在 MRC 時代瓶盛,即使是最牛逼的 iOS 開發(fā)者,也不能保證一次性就寫出完美的內(nèi)存管理代碼示罗。于是惩猫,我們開始一步一步調(diào)試,試著打印出每個懷疑對象的引用計數(shù)(Retain Count)鹉勒,然后帆锋,我們小心翼翼地插入合理的 retain 和 release 代碼。經(jīng)過一次又一次的應(yīng)用崩潰和調(diào)試禽额,終于有一次锯厢,應(yīng)用能夠正常運行了!于是我們長舒一口氣脯倒,露出久違的微笑实辑。

引用計數(shù)

這里面提到了引用計數(shù),那么什么是引用計數(shù)藻丢?

引用計數(shù)(Reference Count)是一個簡單而有效的管理對象生命周期的方式剪撬。當(dāng)我們創(chuàng)建一個新對象的時候,它的引用計數(shù)為 1悠反,當(dāng)有一個新的指針指向這個對象時残黑,我們將其引用計數(shù)加 1,當(dāng)某個指針不再指向這個對象是斋否,我們將其引用計數(shù)減 1梨水,當(dāng)對象的引用計數(shù)變?yōu)?0 時,說明這個對象不再被任何指針指向了茵臭,這個時候我們就可以將對象銷毀疫诽,回收內(nèi)存。由于引用計數(shù)簡單有效,除了 Objective-C 和 Swift 語言外奇徒,微軟的 COM(Component Object Model )雏亚、C++11(C++11 提供了基于引用計數(shù)的智能指針 share_prt)等語言也提供了基于引用計數(shù)的內(nèi)存管理方式。

手動管理引用計數(shù)的思考方式:

  • 自己生成的對象摩钙,自己持有
  • 非自己生成的對象罢低,自己也能持有
  • 不再需要自己持有的對象時釋放
  • 非自己持有的對象無法釋放

有了這種思考方式,我們就生成了對應(yīng)的 Objective-C 方法來管理引用計數(shù)胖笛。
下表是對象操作與 Objective-C 方法的對應(yīng)

對象操作 Objective-C 方法 引用計數(shù)
生成并持有對象 alloc/new/copy/mutableCopy 等方法 引用計數(shù)+1
持有對象 retain 引用計數(shù) +1
釋放對象 release 引用計數(shù) -1
廢棄對象 dealloc 引用計數(shù) -1

如圖奕短,可清晰的看到 對象操作與 Objective-C 方法的對應(yīng)

對應(yīng)關(guān)系

既然到了這兒,我們也能大概猜到 MRC 下程序員們是如何管理內(nèi)存的了

在 MRC 模式下匀钧,所有的對象都需要手動的添加 retain、release 代碼來管理內(nèi)存谬返。使用 MRC 之斯,需要遵守誰創(chuàng)建,誰回收的原則遣铝。也就是誰 alloc 佑刷,誰 release ;誰 retain 酿炸,誰 release瘫絮。
當(dāng)引用計數(shù)為0的時候,必須回收填硕,引用計數(shù)不為0麦萤,不能回收,如果引用計數(shù)為0扁眯,但是沒有回收壮莹,會造成內(nèi)存泄露。如果引用計數(shù)為0姻檀,繼續(xù)釋放命满,會造成野指針。為了避免出現(xiàn)野指針绣版,我們在釋放的時候胶台,會先讓指針= nil。

這塊兒先不介紹這幾個方法的底層實現(xiàn)杂抽,我們只是簡單的通過一段簡單的代碼看看這幾個方式是如何進行內(nèi)存管理的诈唬。

我們首先要修改工程設(shè)置,給 main.m 加上 -fno-objc-arc 的編譯參數(shù)默怨,這個參數(shù)可以啟動手動管理引用計數(shù)的模式讯榕。
然后,我們先輸入如下代碼,通過 Log 看到相應(yīng)的引用計數(shù)的變化愚屁。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init]; // 引用計數(shù) +1
        NSLog(@"Reference Count = %lu", (unsigned long)[object retainCount]);
        NSObject *another = [object retain];//引用計數(shù) +1
        NSLog(@"Reference Count = %lu", [object retainCount]);
        [another release];//引用計數(shù) -1
        NSLog(@"Reference Count = %lu", [object retainCount]);
        [object release];// 到這兒济竹,引用計數(shù)就為 0 了。
        
    }
    return 0;
}
// 打印的結(jié)果為:
2017-05-23 16:11:35.035467+0800 MRC[1148:75832] Reference Count = 1
2017-05-23 16:11:35.041784+0800 MRC[1148:75832] Reference Count = 2
2017-05-23 16:11:35.041806+0800 MRC[1148:75832] Reference Count = 1

為什么需要引用計數(shù)

看完上述代碼霎槐,大家可能會覺得送浊,這就是引用計數(shù)啊,這不挺簡單的嗎丘跌?但是袭景,我要告訴大家的,上面那段代碼只是非常簡單的例子闭树,我們還看不出來引用計數(shù)真正的用處耸棒。因為該對象的生命期只是在一個函數(shù)內(nèi),所以在真實的應(yīng)用場景下报辱,我們在函數(shù)內(nèi)使用一個臨時的對象与殃,通常是不需要修改它的引用計數(shù)的,只需要在函數(shù)返回前將該對象銷毀即可碍现。

引用計數(shù)真正派上用場的場景在于面向?qū)ο蟮某绦蛟O(shè)計架構(gòu)中幅疼,用于對象之間傳遞和共享數(shù)據(jù)

假如對象 A 生成了一個對象 M昼接,需要調(diào)用對象 B 的某一個方法爽篷,將對象 M 作為參數(shù)傳遞過去。在沒有引用計數(shù)的情況下慢睡,一般內(nèi)存管理的原則是 “誰申請誰釋放”逐工,那么對象 A 就需要在對象 B 不再需要對象 M 的時候,將對象 M 銷毀漂辐。但對象 B 可能只是臨時用一下對象 M钻弄,也可能覺得對象 M
很重要,將它設(shè)置成自己的一個成員變量者吁,那這種情況下窘俺,什么時候銷毀對象 M 就成了一個難題。


對于這種情況复凳,我們可以在對象 A 在調(diào)用完對象 B 后直接釋放參數(shù)對象 M瘤泪, B 在對參數(shù) M 做一個 Copy ,生成另一個對象 M1育八,B 自己管理 M1 对途。


還有一種方法就是對象 A 在構(gòu)造完對象 M 之后,始終不銷毀對象 M髓棋,由對象 B 來完成對象 M 的銷毀工作实檀。如果對象 B 需要長時間使用對象 M惶洲,它就不銷毀它,如果只是臨時用一下膳犹,則可以用完后馬上銷毀恬吕。如果情況在復(fù)雜點,出現(xiàn)個對象 C须床,那么我們的工作是不是就更復(fù)雜了呢铐料。


但是上述兩種方法要么使得工作量大增,影響性能豺旬,要么使得對象間的耦合太過緊密钠惩,增大復(fù)雜性。

所以族阅,這個時候篓跛,我們的引用計數(shù)就可以很好的解決這個問題。在參數(shù) M 的傳遞過程中坦刀,哪些對象需要長時間使用這個對象举塔,就把它的引用計數(shù)加 1,使用完了之后再把引用計數(shù)減 1求泰。所有對象都遵守這個規(guī)則的話,對象的生命期管理就可以完全交給引用計數(shù)了计盒。我們也可以很方便地享受到共享對象帶來的好處渴频。

ARC 下的內(nèi)存管理

ARC 能夠解決 iOS 開發(fā)中 90% 的內(nèi)存管理問題,但是另外還有 10% 內(nèi)存管理北启,是需要開發(fā)者自己處理的卜朗,這主要就是與底層 Core Foundation 對象交互的那部分,底層的 Core Foundation 對象由于不在 ARC 的管理下咕村,所以需要自己維護這些對象的引用計數(shù)场钉。

這里我們先拋出 ARC 不能解決的問題:

  • Block 等引發(fā)的循環(huán)引用問題
  • 底層 Core Foundation 對象需要手動管理

所有權(quán)修飾符

ARC 有效時,id 類型和對象類型同 C 語言其他類型不同懈涛,其類型上必須附加所有權(quán)修飾符逛万。所有權(quán)修飾符一共有四種。

  • _strong 修飾符
  • _strong修飾符:id 類型和對象類型默認的所有權(quán)修飾符批钠;它可以保證將這些修飾符的自動變量初始化為nil.
  • _strong 修飾符表示對對象的“強引用”; 附有_strong 修飾符的變量之間可以互相賦值宇植。
  • 持有強引用的變量在超出其作用域時被廢棄,隨著強引用的失效埋心,引用的對象會隨之消失
  • 通過 _strong 修飾符指郁,不必再次鍵入 retain 和 release
{
    //  ARC 有效時
    id obj = [[NSObject alloc] init];//自己生成并持有對象
    //因為對象obj 強引用,自己也持有對象
}
  <!--//超出作用域,強引用失效拷呆,自動釋放自己持有的對象-->
{
    //  ARC 無效時闲坎,該方法與 ARC 有效時一樣
    id obj = [[NSObject alloc] init];//自己生成并持有對象
    [obj release];// 需要自己調(diào)用 release 方法來釋放
}
  • _weak 修飾符
  • 弱引用疫粥,不持有所指向?qū)ο蟮乃袡?quán)
  • 可以避免循環(huán)引用
  • 在持有某對象的弱引用時,若該對象被廢棄腰懂,則此弱引用將自動失效且處于 nil 被賦值的狀態(tài)梗逮。
// 避免循環(huán)引用
__weak __typeof(self) weakSelf = self;
{
    // 自己生成并持有對象
    id _strong obj0 = [NSObject alloc] init];
    // 因為 obj0 變量為強引用,所以自己持有對象
    id _weak obj1 = obj0;
    // obj1 變量持有生成對象的弱引用
}
/*
* 因為 obj0 變量超出其作用域悯恍,強引用失效
* 所以自動釋放自己持有的對象
* 因為對象的所有者不存在库糠,所以廢棄該對象
 */
  • _unsafe_unretained 修飾符
  • 不安全的所有權(quán)修飾符,附有 _unsafe_unretained 修飾符的變量不屬于編譯器測內(nèi)存管理對象
  • 為兼容iOS5以下版本的產(chǎn)物涮毫,可以理解成MRC下的weak
  • 在使用 _unsafe_unretained 修飾符時瞬欧,賦值給附有 _strong 修飾符的變量時,要確保被賦值的對象確實存在
  • _autoreleasing 修飾符
  • 自動釋放對象的引用罢防,一般用于傳遞參數(shù)
  • 在 ARC 有效時艘虎,用 @autoreleasepool 塊替代 NSAutoreleasePool 類,用附有 _autoreleasing 修飾符的變量替代 autorelease 方法咒吐。
  • 當(dāng)沒有顯示指定所有權(quán)修飾符野建, id obj 和附有 _strong 修飾符 的obj 是完全一樣的。編譯器在對象變量超過作用域時恬叹,釋放它并且自動將它注冊到 autoreleasepool 中候生。
  • 使用 _weak 修飾符的變量時,要訪問注冊到 autoreleasepool 的對象
  • id 的指針或?qū)ο蟮闹羔樤跊]有顯示指定時會被附加上 _autoreleasing 修飾符
id _weak obj1 = obj0;
NSLog(@"class= %@",[obj1 class]);

上述代碼與以下代碼相同

id _weak obj1 = obj0;
id _autoreleasing tmp = obj1;
NSLog(@"class= %@",[obj1 class]);

autoreleasepool 范圍以塊級源代碼表示绽昼,提高了程序的可讀性唯鸭,所以今后在ARC無效時也推薦使用 @autoreleaseepool 塊。
另外硅确,無論 ARC 是否有效目溉,調(diào)試用的非公開函數(shù) _objc_autoreleasePoolPrint() 都可使用。
_objc_rootRetainCount(obj)
利用這一函數(shù)可有效的幫助我們調(diào)試注冊到 autoreleasepool 上的對象

ARC 的規(guī)則

  • 不能使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 須遵循內(nèi)存管理的方式命名規(guī)則
  • 不要顯示調(diào)用 dealloc
  • 使用 @autorealeasepool 塊代替 NSAutoreleasePool
  • 不要使用區(qū)域(NSZone)
  • 對象型變量不能作為 C 語言結(jié)構(gòu)體的成員
  • 顯示轉(zhuǎn)換 'id' 和 'void'

循環(huán)引用問題

簡單的來說循環(huán)引用就是對象 A 和對象 B菱农,相互引用了對方作為自己的成員變量缭付,只有當(dāng)自己銷毀時,才會將成員變量的引用計數(shù)減 1循未。因為對象 A 的銷毀依賴于對象 B 銷毀陷猫,而對象 B 的銷毀與依賴于對象 A 的銷毀,這樣就造成了我們稱之為循環(huán)引用(Reference Cycle)的問題的妖,這兩個對象即使在外界已經(jīng)沒有任何指針能夠訪問到它們了烙丛,它們也無法被釋放。實際上羔味,多個對象依次持有對方河咽,形式一個環(huán)狀,也可以造成循環(huán)引用問題赋元,而且在真實編程環(huán)境中忘蟹,環(huán)越大就越難被發(fā)現(xiàn)嫩码。

  • 解決循環(huán)引用的問題的兩個方法
  1. 知道會產(chǎn)生循環(huán)引用滞伟,在合理的位置主動斷開環(huán)中的一個引用屠阻,使得對象得以回收

主動斷開循環(huán)引用這種方式常見于各種與 block 相關(guān)的代碼邏輯中藐握。
但是主動斷開循環(huán)引用這種操作依賴于程序員自己手工顯式地控制,相當(dāng)于回到了以前 “誰申請誰釋放” 的內(nèi)存管理年代褥芒,它依賴于程序員自己有能力發(fā)現(xiàn)循環(huán)引用并且知道在什么時機斷開循環(huán)引用回收內(nèi)存(這通常與具體的業(yè)務(wù)邏輯相關(guān))

  1. 常見的辦法是使用弱引用 (weak reference) 的辦法嚼松,弱引用雖然持有對象,但是并不增加引用計數(shù)锰扶,這樣就避免了循環(huán)引用的產(chǎn)生献酗。在 iOS 開發(fā)中,弱引用通常在 delegate 模式中使用坷牛。
  • 使用 Xcode 檢測循環(huán)引用

Core Foundation 對象的內(nèi)存管理

Core Foundation 對象主要使用在用 C語言編寫的 Core Foundation 框架中,并使用引用計數(shù)的對象罕偎;在 ARC 無效時 ,Core Foundation 框架中的 retain/release 分別是 CFRetain/CFRelease京闰;因為 Core Foundation 對象和 Objective-C 對象沒有什么區(qū)別颜及,所以在 ARC 無效時,可以使用簡單的 C 語言就可以實現(xiàn)互換蹂楣。

在 ARC 下俏站,我們有時需要將一個 Core Foundation 對象轉(zhuǎn)換成一個 Objective-C 對象,這個時候我們需要告訴編譯器痊土,轉(zhuǎn)換過程中的引用計數(shù)需要做如何的調(diào)整肄扎。這就引入了 bridge 相關(guān)的關(guān)鍵字,以下是這些關(guān)鍵字的說明:

  • ==__bridge== : 只做類型轉(zhuǎn)換施戴,不修改相關(guān)對象的引用計數(shù),原來的 Core Foundation 對象在不用時萌丈,需要調(diào)用 CFRelease 方法赞哗。
  • ==__bridge_retained== :類型轉(zhuǎn)換后,將相關(guān)對象的引用計數(shù)加 1辆雾,原來的 Core Foundation 對象在不用時肪笋,需要調(diào)用 CFRelease 方法。
  • ==__bridge_transfer== :類型轉(zhuǎn)換后度迂,將該對象的引用計數(shù)交給 ARC 管理藤乙,Core Foundation 對象在不用時,不再需要調(diào)用 CFRelease 方法惭墓。

總結(jié)

這篇文章并沒有涉及 MRC 以及 ARC 實現(xiàn)的底層坛梁,所涉及到的知識也是個人看完 高級編程第一章的知識以及 唐巧大神的文章后,自己總結(jié)的筆記腊凶。在之后的探索中划咐,也會從底層出發(fā)來剖析內(nèi)存管理的知識拴念。

參考博客:唐巧的理解 iOS 內(nèi)存管理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市褐缠,隨后出現(xiàn)的幾起案子政鼠,更是在濱河造成了極大的恐慌,老刑警劉巖队魏,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件公般,死亡現(xiàn)場離奇詭異,居然都是意外死亡胡桨,警方通過查閱死者的電腦和手機官帘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來登失,“玉大人遏佣,你說我怎么就攤上這事±空悖” “怎么了状婶?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長馅巷。 經(jīng)常有香客問我膛虫,道長,這世上最難降的妖魔是什么钓猬? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任稍刀,我火速辦了婚禮,結(jié)果婚禮上敞曹,老公的妹妹穿的比我還像新娘账月。我一直安慰自己,他們只是感情好澳迫,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布局齿。 她就那樣靜靜地躺著,像睡著了一般橄登。 火紅的嫁衣襯著肌膚如雪抓歼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天拢锹,我揣著相機與錄音谣妻,去河邊找鬼。 笑死卒稳,一個胖子當(dāng)著我的面吹牛蹋半,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播充坑,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼湃窍,長吁一口氣:“原來是場噩夢啊……” “哼闻蛀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起您市,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤觉痛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茵休,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體薪棒,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年榕莺,在試婚紗的時候發(fā)現(xiàn)自己被綠了俐芯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡钉鸯,死狀恐怖吧史,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情唠雕,我是刑警寧澤贸营,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站岩睁,受9級特大地震影響钞脂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捕儒,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一冰啃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刘莹,春花似錦阎毅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蒲拉,卻和暖如春肃拜,著一層夾襖步出監(jiān)牢的瞬間痴腌,已是汗流浹背雌团。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留士聪,地道東北人锦援。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像剥悟,于是被迫代替她去往敵國和親灵寺。 傳聞我的和親對象是個殘疾皇子曼库,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容