iOS內存泄漏檢測方法

什么是內存泄漏,通俗來說就是有一塊內存區域被你占用了,但你又不使用這塊區域也不讓別人用,造成內存浪費,這就是內存泄漏,泄漏嚴重會造成內存吃緊,嚴重的會使程序崩潰;
內存泄漏對于以前MRC開發來說相當痛苦,需要耗費大量精力管理內存,引入ARC機制后,系統自動管理內存,大大減輕了開發工作量,但一些特殊情況仍然會有內存泄漏發生,需要特別注意。

一般易造成泄漏的點
  • Retain Cycle,Block強引用
  • NSTimer釋放不當
  • 第三方提供方法造成的內存泄漏
  • CoreFoundation方式申請的內存,忘記釋放

eg:

block循環

 [cell setSelectTagCityBlock:^(NSIndexPath *indexPath, NSInteger index){
        [self tableView:_tableViewCityList didSelectRowAtIndexPath:indexPath index:index];
    }];
    

一般用weak打破保留環

@WeakObj(self)
    [cell setSelectTagCityBlock:^(NSIndexPath *indexPath, NSInteger index){
        if (selfWeak)
        {
            [selfWeak tableView:selfWeak.tableViewCityList didSelectRowAtIndexPath:indexPath index:index];
        }
    }];
    

AFNetWorking上的經典代碼,防止循環引用的

//創建__weak弱引用,防止強引用互相持有
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    //創建局部__strong強引用,防止多線程情況下weakSelf被析構
     __strong __typeof(weakSelf)strongSelf = weakSelf;
    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {
         strongSelf.networkReachabilityStatusBlock(status);
    }
};

weak 本身是可以避免循環引用的問題的,但是其會導致外部對象釋放了之后,block 內部也訪問不到這個對象的問題,我們可以通過在 block 內部聲明一個 strong 的變量來指向 weakObj,使外部對象既能在 block 內部保持住,又能避免循環引用的問題

block 本身無法避免循環引用的問題,但是我們可以通過在 block 內部手動把 blockObj 賦值為 nil 的方式來避免循環引用的問題。另外一點就是 block 修飾的變量在 block 內外都是唯一的,要注意這個特性可能帶來的隱患。



 _timer = [NSTimer timerWithTimeInterval:[refreshTime integerValue]
                                             target:self
                                           selector:@selector(doFSearchDoubleBackNumberRequest:)
                                           userInfo:searchResult
                                            repeats:NO];
            [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
            

Timer 添加到 Runloop 的時候,會被 Runloop 強引用。
Timer 又會有一個對 Target 的強引用。
所以說如果不對Timer進行釋放,Timer的targer(self)也一直不會被釋放。
有時候我們我們對某個Timer的targer設置了nil。但沒設置[timer invalidate]。
其實這個對象還是沒被釋放的。timer對應的執行方法也一直會在線程中執行。容易造成內存泄露。

注:repeats:NO不會強引用
常規的監測方法
  • Analyze靜態分析 (command + shift + b)

    主要分析以下四種問題:

    1、邏輯錯誤:訪問空指針或未初始化的變量等;

    2、內存管理錯誤:如內存泄漏等;

    3、聲明錯誤:從未使用過的變量;

    4、Api調用錯誤:未包含使用的庫和框架。

    靜態分析結果會有警告提示

    image
  • Instruments中的Leak動態分析內存泄漏

    product->profile ->leaks 打開工具主窗口


    image
    image

    點擊暫停,將鼠標移到叉號上面點擊鎖定,點擊下方的“田”字格,選擇callTree,

    選擇中間的齒輪,選中選項中的 invert Call Tree 和Hide System Libraries

  • Separate by Thread:按線程分開做分析,這樣更容易揪出那些吃資源的問題線程。特別是對于主線程,它要處理和渲染所有的接口數據,一旦受到阻塞,程序必然卡頓或停止響應。
  • Invert Call Tree:反向輸出調用樹。把調用層級最深的方法顯示在最上面,更容易找到最耗時的操作。
  • Hide System Libraries:隱藏系統庫文件。過濾掉各種系統調用,只顯示自己的代碼調用。
  • Flattern Recursion:拼合遞歸。將同一遞歸函數產生的多條堆棧(因為遞歸函數會調用自己)合并為一條。

雙擊左邊 Call Tree 下面的任意一行,查看內存泄漏的代碼位置

image
  • Allocation工具了解內存的分配情況

    每次點擊generations(是兩個時間標記之間所有仍然活著的對象的快照),生成快照,而且 Allocations 會記錄從上回內存快照到這次內存快照這個時間段內,新分配的內存信息,數次 push 跟 pop 之后,內存還不斷增長,則有內存泄露

    image

    開源項目 HeapInspector-for-iOS 可以說是 Allocations 的改進,它通過 hook 掉 alloc,dealloc,retain,release 等方法,來記錄對象的生命周期,親測不太好用

    其他工具用途

    • Core Data:監測讀取、緩存未命中、保存等操作,能直觀顯示是否保存次數遠超實際需要。
    • Cocoa Layout:觀察約束變化,找出布局代碼的問題所在。
    • Network:跟蹤 TCP / IP和 UDP / IP 連接。
    • Automations:創建和編輯測試腳本來自動化 iOS 應用的用戶界面測試
XCode8后新特性
  • Debug Memory Graph

    Debug Memory Graph, 直接以關系圖的形式來告訴你各個對象的持有關系, 泄露時會有紫色的小感嘆號出現,
    在開發過程中,因為語法或明顯的代碼錯誤(例如Retain Cycle),編譯器可以發現并報黃色或紅色警告

image

實時監測內存占用情況

image

直接選擇一個對象,查看與其相關的內存關系

image
- 綠色的一般都是 UIKit 控件及其子類
- 藍色一般 NSObject 類及其子類
- 黃色一般都是容器類型及其子類
- 灰色括號是指 block
第三方工具
  • MLeaksFinder

    原理還是很簡單的, 它swizzle了NavigationController的Push和Pop相關方法來管理viewController和view的生命周期, 在你Pop掉viewController的時候, 會執行這么一段代碼


- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

3秒后執行 [weakSelf assertNotDealloc]; 如果這個時候view和viewController已經釋放了, 那么weakSelf應該為nil, 所以將不會觸發斷言, 否則將會打印日志, 觸發斷言.

  • 關于swizzleSEL

一種簡寫方式


void MethodSwizzle(Class c,SEL origSEL,SEL overrideSEL)  {
        Method origMethod = class_getInstanceMethod(c, origSEL);
        Method overrideMethod= class_getInstanceMethod(c, overrideSEL);
}

傳入兩個參數,原方法選擇子,新方法選擇子,并通過class_getInstanceMethod()拿到對應的Method

  • 有兩種情況要考慮一下。第一種情況是要復寫的方法(overridden)并沒有在目標類中實現,而是在其父類中實現了。第二種情況是這個方法已經存在于目標類中。這兩種情況要區別對待
    (它的目的是為了使用一個重寫的方法替換掉原來的方法。但重寫的方法可能是在父類中重寫的,也可能是在子類中重寫的。)
  • 對于第一種情況(重寫方法不存在),應當先在目標類增加一個新的實現方法(override),然后將復寫的方法替換為原先的實現
  • 對于第二情況(目標類存在重寫的方法)。這時可以通過method_exchangeImplementations來完成交換.

標準方式

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
        [self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
        [self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];
    });
}


+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
#if _INTERNAL_MLF_ENABLED
    
#if _INTERNAL_MLF_RC_ENABLED
    // Just find a place to set up FBRetainCycleDetector.
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [FBAssociationManager hook];
        });
    });
#endif
    
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
    
    BOOL didAddMethod =
    class_addMethod(class,
                    originalSEL,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSEL,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
#endif
}

class_addMethod:是相對于實現來的說的,將本來不存在于被操作的Class里的newMethod的實現添加在被操作的Class里,并使用origSel作為其選擇子

如果發現方法已經存在,會失敗返回,也可以用來做檢查用,我們這里是為了避免源方法沒有實現的情況;如果方法沒有存在,我們則先嘗試添加被替換的方法的實現

1.如果返回成功:則說明被替換方法沒有存在.也就是被替換的方法沒有被實現,我們需要先把這個方法實現,然后再執行我們想要的效果,用我們自定義的方法去替換被替換的方法. 這里使用到的是class_replaceMethod這個方法. class_replaceMethod本身會嘗試調用class_addMethod和method_setImplementation,所以直接調用class_replaceMethod就可以了)

2.如果返回失敗:則說明被替換方法已經存在.直接將兩個方法的實現交換即可

class_replaceMethod,addMethod成功完成后,從參數可以看出,目的是換掉method_getImplaementation(roiginMethod)的選擇子,將原方法的實現的SEL換成新方法的SEL

  • FBRetainCycleDetector

    能夠檢測指定對象的引用情況,并把所存在的引用循環中各對象和引用在終端進行打印

    #import <FBRetainCycleDetector/FBRetainCycleDetector.h>
    
    _handlerBlock = ^{
        NSLog(@"%@", self);
    };
    
    FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
    [detector addCandidate:self];
    NSSet *retainCycles = [detector findRetainCycles];
    NSLog(@"%@", retainCycles);
    
打印結果類似于:


```

{(
    (
    "-> DetailViewController ",
    "-> _handlerBlock -> __NSMallocBlock__ "
)
)}

 
 
 
 ```

DetailViewController通過_handlerBlock實例變量引用一個Block對象,而該Block又引用了DetailViewController對象。如果不存在引用循環的話,打印的結果將是空的

原理: 循環引用可以包含任何數量的對象。一個壞的連接會導致很多環的時候,這就復雜了

image

在環中,A→B是一個壞連接,創建了兩個環:A-B-C-D 和 A-B-C-E。
這有兩個問題:

  • 不想給一個壞連接導致的兩個循環引用分別標記。
  • 不想給可能代表兩個問題的兩個循環引用一起標記,即使它們共享一個連接。

所以需要給循環引用定義簇組(clusters),鑒于這些啟發,我們寫了個算法來找到這些問題。

  • 在給定的時間收集所有的環。
  • 對于每一個環,提取Facebook特定的類名。
  • 對于每一個環,找到包含在環內的被報告的最小的環。
  • 依據上面的最小環,將環添加到組中。
  • 只報告最小環。

通過Pod安裝后,通過以下代碼激活即可。

[[PLeakSniffer sharedInstance] installLeakSniffer];
[[PLeakSniffer sharedInstance] addIgnoreList:@[@"MySingletonController"]];

addIgnoreList可以添加一些特殊的忽略名單,比如單例這種無法正確預測泄漏的對象

原理: 如果Controller被釋放了,但其曾經持有過的子對象如果還存在,那么這些子對象就是泄漏的可疑目標

子對象(比如view)建立一個對controller的weak引用,如果Controller被釋放,這個weak引用也隨之置為nil。那怎么知道子對象沒有被釋放呢?用一個單例對象每個一小段時間發出一個ping通知去ping這個子對象,如果子對象還活著就會一個pong通知。所以結論就是:如果子對象的controller已不存在,但還能響應這個ping通知,那么這個對象就是可疑的泄漏對象。完整的結構可以用下圖表示

image

參考資料

iOS 中的 NSTimer

__weak與__block區別

iOS內存泄漏自動檢測工具PLeakSniffer

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 在 Java 中,內存的分配是由程序完成的,而內存的釋放則是由 Garbage Collecation(GC) 完...
    Shawn_Dut閱讀 5,919評論 3 28
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,717評論 18 399
  • 心情曾一度跌到谷底。就像是生命沒有了繼續前行的勇氣,沒了方向~~就像是無意識的木偶麻木的看著周遭一切,機械...
    沉默的惜惜閱讀 220評論 0 1
  • 前段時間,老爺子感覺渾身無力,坐在哪里就不想動彈,盡打瞌覺想睡覺,看情形不對,老公趕緊帶他到醫院去檢查,原...
    夏所珍創作室閱讀 752評論 0 1