iOS - 記憶體管理之超級大坑記憶體流失QAQ

來源:互聯網
上載者:User

標籤:

??前段時間被分配到查記憶體流失這種大坑,不勝惶恐!!!結果還真的跳進去了,爬了好長一段時間都沒爬出來QAQ。每天開著Leaks各種搗鼓愛啪啪,然後看到一大波“神奇”的記憶體泄露資訊,頭都大了。

??不過這雖然是個大坑,不過趁著這次機會可以把記憶體管理知識好好實踐了一遍。或許現在大多數的新項目都是ARC的了,然而在一些實際的大項目中,會重用很多諾幹年前(其實也就幾年前)的代碼,QAQ,而這些代碼會有很多實現採用的是MRC的方式,所以項目中就經常可以見到混合著ARC和MRC這兩種不同記憶體管理員模式的代碼。然後ARC檔案的代碼可能會調用MRC模式實現的代碼,當然也還有反過來的情況。

??除了ARC和MRC混合,有時候還有很多底層庫是使用C/C++實現的,而且這些庫中也涉及一些OC方面的庫類調用,這樣的情況就又複雜一點了。 = =,所以C/C++的記憶體管理也是需要有所瞭解滴~~~

??QAQ,接下來在最後部分會簡單列舉一些碰到過的發生記憶體流失的情況吧。最後大家會發現這些問題都很簡單,但熟話說的好,潛水淹死人啊!

一、偵查工具介紹1.1 Instrument — Leaks,Allocations,Analyze

??我用到的檢測記憶體泄露的工具主要是Xcode中整合的Leaks組件,這個組件的檢測準確率還是比較高的(畢竟水果家親兒子),可以查看到很多比如說是泄露大小,泄露產生的地方及其堆棧資訊等。但是這裡的“泄露產生的地方”並不一定可以定位到具體發生泄漏的某一句代碼,而是會標出發生泄漏的對象初始化分配記憶體的地方,然後需要具體去分析該對象來查處泄漏的原因。

參考資料:
1. 【使用Instruments定位iOS應用的Memory Leaks】
2. 【Leaks Instrument】

??Allocations工具是一個跟蹤由應用程式分配的對象記憶體的工具。一般就是用來在疑似記憶體泄露的地方,通過反覆操作,查看某些對象記憶體是否有被正常的釋放,從而得知是否發生記憶體泄露。(= =。這裡我並沒使用到這個,這算是以前比較古老的檢測記憶體流失的方式了,不過某些情況下也還是挺有用。)

參考資料:
1. 【iOS效能最佳化:Instruments 工具的救命三招】
2. 【IOS效能調優系列:使用Allocation動態分析記憶體使用量情況】

??Analyze是一款靜態分析代碼的工具。它可以發現一些邏輯錯誤,記憶體流失和聲明錯誤(未使用變數)等。這裡可以發現的一些記憶體流失問題主要是一些常見的循環參考,CF庫對象未release等相對簡單的問題,通常是在進行其他方式檢測之前就使用的方式,把一些簡單的問題先發現並處理了。

參考資料:
1. 【IOS效能調優系列:Analyze靜態分析】
2. 【IPhone開發工具篇-利用xcode profile和analyze進行效能最佳化】

1.2 記憶體檢測組件

??此外還有一些“植入”項目中的記憶體檢測組件,比如說Facebook iOS 記憶體檢測三劍客(FBAllocationTracker/FBMemoryProfiler/FBRetainCycleDetector),MSLeakHunter,MLeaksFinder,PLeakSniffer等等。

??這些組件的實現原理都是大同小異的。主要就是靈活運用了OC中的Rumtime機制,以及各種OC對象生命週期管理相關的特性。這些組件為了實現對OC對象的記憶體監控,其本質就是在這些對象被分配和釋放的時機進行監測,結合系統對這些對象生命週期管理方法實現是否發生記憶體泄露檢測的目的。

??比如說需要監測一個UIViewController類型的對象,就可以聯想到iOS中VC的生命週期管理和UINavigationController有很大關係,因為後者在iOS應用者常常被用來管理大量VC的跳轉控制。所以就可以考慮通過監控UINavigationController的navigation stack來達到檢測VC是否發生記憶體泄露的目的。(一般以下這些方法都會被hook)

??再比如說常見的NSObject對象,其alloc和dealloc方法就是對象生命週期中很重要的兩個方法,分別是分配記憶體資源和釋放記憶體資源時會被調用的方法。然後就可以考慮通過method swizzing方法替換alloc和dealloc這兩個方法的實現,這樣就可以獲得對象記憶體配置的一些資訊。

??以下再給出個檢測VC記憶體流失的原理:

  1. 如何判斷VC是否還在記憶體駐留?
    Tips:利用ARC中weak指標指向的對象在對象釋放時會自動置為nil的特性來檢測VC是否在記憶體駐留。

  2. 在什麼時機檢測VC是否發生記憶體泄露?
    Tips:通過監控UINavigationController的navigation stack,可以判斷一個VC的生命週期的開始和結束。就是當VC從navigation stack移除且VC的viewDidDisappear方法執行時,可以認為一個VC的生命週期即將結束。這時候就可以建立一個指向該VC的weak指標,並初始化一個定時器對VC進行延時掃描,最後通過1中的方法判斷VC是否還駐留在記憶體從而得出VC是否發生記憶體泄露的結論。


二、應用記憶體

從蘋果的開發人員文檔裡可以看到,一個 app 的記憶體分三類:

  1. Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

  2. Abandoned memory: Memory still referenced by your application that has no useful purpose.

  3. Cached memory: Memory still referenced by your application that might be used again for better performance.

??其中 Leaked memory 和 Abandoned memory 都屬於應該釋放而沒釋放的記憶體,都是記憶體泄露,而 Leaks 工具只負責檢測 Leaked memory,而不管 Abandoned memory。在 MRC 時代 Leaked memory 很常見,因為很容易忘了調用 release,但在 ARC 時代更常見的記憶體泄露是循環參考導致的 Abandoned memory,Leaks 工具查不出這類記憶體泄露,應用有限。(引用出處【傳送門】)


三、實踐3.1 對象記憶體管理


1. 在MRC模式下,通過new, copy, alloc方式建立的對象,記得release。一般在delloc中進行釋放操作。當然局部內產生的也要在局部內進行釋放。

點評:呵呵,在實踐中發現最多的問題就是這個。尤其是在ARC和MRC都有的項目中。= =。我猜測原因之一可能是後面的代碼修改者沒意識到當前修改的檔案是MRC模式的,所以在新增一些屬性或成員變數後,沒有在dealloc方法或對象使用完畢後及時的釋放資源。


2. 在MRC模式下,發送了retain訊息,記得也要發送release訊息。並且在一個對象發送retain訊息之前,也要考慮是否要release原來的對象。

碰到的一個栗子:

@interface classA{    NSString *_str;}- (void)functionA{    //正確的方式是這裡要有: [_str release];    _str = [[NSString stringWithFormat:@"%d", @(213)] retain];    //後續代碼...}- (void)functionB{    //正確的方式是這裡要有: [_str release];    _str = [[NSString stringWithFormat:@"%d", @(213)] retain];//後續代碼...}@end

Tips:這裡存在一個問題就是functionA中對一個對象發送了retain訊息,如果這時候又調用了functionB方法,str變數被重新賦值。此時如果沒有先對str發送release訊息的話,則會導致functionA中引用的對象發生記憶體泄露。
對於一般情況下使用的局部變數都會記得發送retain後發送release,然而在栗子中那種情況下,成員變數可能在不同方法中被重新賦值的時候,就要注意了!


3. 不論是MRC還是ARC情況下,使用Core Foundation架構(C語言實現的架構,其可以和Cocoa Foundation庫中的對象進行類型轉換)建立的對象需要手動進行記憶體管理。即需要手動調用CFRetain和CFRelease來管理對象記憶體。

Tips:這種情況沒啥好說的了,就是記得CFRetain、CFRelease和retain、release一樣要成對出現~

??再多說一點就是Core Foundation架構和Cocoa Foundation對象指標轉換的內容。Cocoa Foundation指標與Core Foundation指標轉換,需要考慮的是所指向對象所有權的歸屬。ARC提供了3個修飾符來管理。【參考資料: IOS之Core Foundation架構和Cocoa Foundation架構區別 、Core Foundation Framework Reference】

  1. __bridge,什麼也不做,僅僅是轉換。此種情況下:
    (1). 從Cocoa轉換到Core,需要人工CFRetain,否則,Cocoa指標釋放後, 傳出去的指標則無效。

    (2). 從Core轉換到Cocoa,需要人工CFRelease,否則,Cocoa指標釋放後,對象引用計數仍為1,不會被銷毀。

  2. __bridge_retained,轉換後自動調用CFRetain,即協助自動解決上述(1)的情形。

  3. __bridge_transfer,轉換後自動調用CFRelease,即協助自動解決上述(2)的情形。


4. 使用NSAutoreleasePool建立的自動釋放池,一定要確保其發送drain或release訊息。這樣建立的自動釋放池對象才會被釋放,同時被加入自動釋放池的對象才能收到release訊息,避免記憶體泄露。

碰到的栗子:

- (void)functionA{    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];    NSString *str = [[NSString alloc] initWithFormat:@"%d", @(213)];    [str release];    //執行各種代碼...    if (...){        //執行各種代碼...        //問題就在這裡:return 之前沒有釋放自動釋放池!!!        //正確的做法,加上: [pool release];        return;    }    [pool release];}

Tips:栗子中的案例雖然看起來是個很逗比的錯誤,不過在實戰中已經發現兩處了…所以如果是MRC方式下這樣使用自動釋放池時,記得也要對自動釋放池發送drain或release操作。如果是使用ARC的話,則不推薦栗子中使用自動釋放池的方式,而是下面這種方式了。

@autoreleasepool {    // Code benefitting from a local autorelease pool.}

??既然說到自動釋放池,那就順便簡單瞭解一下其實現原理,使用情境和一些注意事項吧。上面也有提到NSAutoreleasePool有兩個方法drain和release,關於這兩者的區別可以參考這些資料:【NSAutoReleasePool使用中drain和release的區別】【NSAutoreleasePool】。此外,還發現了一篇講解AutoReleasePool的比較好的文章,裡面也有解釋了AutoReleasePool釋放時間,原理等等:【黑幕背後的Autorelease】。


5. 函數返回的對象,是否加入自動釋放池(延遲釋放)。從記憶體管理的規範上來講,如果一個函數需要返回一個對象,這個對象應該加入自動釋放池中(”誰建立,誰釋放”)?雖然說從某種角度來說,不加進自動釋放池,而是由函數調用者負責該對象的釋放也是可行的。如果函數返回的對象沒有加入自動釋放池,而函數調用者在外部又沒有釋放該對象,則就有可能造成記憶體泄露的現象。

(1)OC中有一些對象有多種建立的方法,比如說NSString, NSArray, NSDictionary之類的(還有它們的可變類型)。這些類都提供了兩種類型的建立方式,一種是成員函數initWithXXX,另一種則是類函數stringWithXXX, arrayWithXXX(或array), dictionaryWithXXX(或dictionary)這些。
這些方法都是有區別的,第一種方式產生的對象需要手動release來釋放記憶體,第二種方式產生的對象已經被加到autoreleasepool中,不需要手動release來釋放記憶體。所以在項目中也要注意這些對象使用不同建立方式時所採用的不同的對象管理方法,針對這兩種對象產生方式,也有很多討論,大家自己看看吧哈哈哈哈哈。

參考資料:
1. stringWithFormat vs. initWithFormat on NSString
2. objective-C: NSString應該用initWithFormat? 還是 stringWithFormat?
3.Difference between [NSMutableArray array] vs [[NSMutableArray alloc] init]

(2)其中就碰到過Runtime方法中的class_copyIvarListclass_copyMethodList這些方法返回的對象沒有被手動釋放導致的記憶體流失。因為這些是C實現的函數,是需要手動對函數傳回值進行free的,不然則會導致記憶體泄露。= =。這裡也順便提醒平時需要注意對於C/C++的實現,當見到malloc/new分配的對象,就應該檢查該對象有沒有對應的free/delete操作,這些地方往往也是記憶體流失產生的地方。


3.2 引用迴圈

??這是無論在MRC還是ARC下都存在的一種導致記憶體泄露的情況,尤其是在ARC中,如果發生記憶體流失,其一般都會是罪魁禍首。= =。而且個人覺得引用迴圈這種問題是最難發現和分析的!項目很大的時候,模組間會很複雜,相互間依賴就很多,一不小心就很容易產生強引用迴圈這種現象。尤其是在使用到block的時候,更要注意適當處理以避免強引用迴圈的發生。

後續有待更新。。。。

iOS - 記憶體管理之超級大坑記憶體流失QAQ

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.