1. 程式人生 > >MLeaksFinder檢查洩露

MLeaksFinder檢查洩露

簡介

MLeaksFinder是WeRead團隊開源的一個檢測iOS記憶體洩漏的框架,其使用非常簡單,只需將檔案加入專案中,如果有記憶體洩漏,3秒後自動彈出警告來捕捉迴圈引用。使得可以在開發快速找到80%記憶體洩漏,而使用Xcode Leak工具更適合大範圍的,全部的尋找洩漏點。

特性

通過閱讀MLeaksFinder的介紹可以看出其具有以下幾個特性

  1. 無侵入性
  2. 可以構建洩漏堆疊
  3. 有白名單機制
  4. 擴充套件性
  5. 其他的一些特殊處理

深入原始碼

實現的核心

在MLeaksFinder的部落格介紹中可以清晰的知道其工作原理,引用博文所說

MLeaksFinder一開始從UIViewController入手。我們知道,當一個UIViewController被pop或dismiss後,該UIViewController包括它的檢視,檢視的子檢視等等將很快被釋放(除非你把它設計成單例,或者持有它的強引用,但一般很少這樣做)。在是,我們只需在一個ViewController被彈出或關閉一小段時間後,看看該UIViewController,它的檢視,檢視的子檢視等等是否還存在。

具體的方法是,為基類NSObject新增一個方法-willDealloc方法,該方法的作用是,先用一個弱指標指向self,並在一小段時間(3秒)後,通過這個弱指標呼叫-assertNotDealloc, -assertNotDealloc主要作用是直接中斷言。這樣,當我們認為某個物件應該被釋放了,在釋放前呼叫這個方法,如果3秒後它被釋放成功,weakSelf就指向nil,不會呼叫到-assertNotDealloc方法,也就不會中斷言,如果它沒被釋放(洩露了), - assertNotDealloc就會被呼叫中斷言。這樣,當一個UIViewController被彈出或關閉時(我們認為它應該要被釋放了),我們遍歷該UIViewController上的所有檢視,依次調-willDealloc,若3秒後沒被釋放,就會中斷言。

總結起來一句話就是,當一個物件3秒之後還沒釋放,那麼指向它的弱指標還是存在的,所以可以呼叫其執行時繫結的方法willDealloc從而提示記憶體洩漏。

接下來我們來看看具體實現

尋找釋放點

論無侵入性的最佳實踐還是使用AOP面向切面的程式設計,通過方法Swizzling系統方法來新增
額外的功能。在MLeaksFinder中,其Swizzling了幾個ViewController釋放方法

// UIViewController 的方法

 [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:)];

通過替換viewDidDisappearviewWillAppeardismissViewControllerAnimated:completion:方法來跟蹤一個模態檢視-控制的釋放


//  UINavigationController
[self swizzleSEL:@selector(pushViewController:animated:) withSEL:@selector(swizzled_pushViewController:animated:)];
[self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(swizzled_popViewControllerAnimated:)];
[self swizzleSEL:@selector(popToViewController:animated:) withSEL:@selector(swizzled_popToViewController:animated:)];
[self swizzleSEL:@selector(popToRootViewControllerAnimated:) withSEL:@selector(swizzled_popToRootViewControllerAnimated:)];

而在UINavigationController的方面,勾了pushViewController:animated:popViewControllerAnimated:popToViewController:animated:popToRootViewControllerAnimated:方法來追蹤一個的UINavigationController棧的釋放。

然而程式碼中沒發現UIPageViewControllerUISplitViewController等方法的hook,可以自己實現來擴充套件此。

追蹤洩漏

willDealloc是一個重要的方法。在之前的追蹤到一個頁面需要釋放的時候會呼叫此方法,而這個方法做了什麼呢?,在NSObject的MemoryLeak分類裡面可以找到這個答案。

    NSString *className = NSStringFromClass([self class]);
    if ([[NSObject classNamesInWhiteList] containsObject:className])
        return NO;
    
    NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
    if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
        return NO;
    
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
        [strongSelf assertNotDealloc];
    });
    
    return YES;

程式碼清晰易懂,其原理是首先判斷這個類是不是在白名單之中,如果是則忽略它,這也就是上文提過的白名單機制了,下面一段程式碼很有意思,

NSNumber * senderPtr = objc_getAssociatedObject([UIApplication sharedApplication],kLatestSenderKey);

這個方法是幹嘛的呢,這就得提到UIControl的target-action機制了,具體可以參考這篇文章。此程式碼的意義在與,如果當前的物件在傳送actionize忽略它(因為willDealloc總會先於他們呼叫),之後設定一個弱指標,在呼叫dispatch_after在2秒後呼叫assertNotDealloc方法,如果還沒釋放,那麼會進入這個方法,如果已經釋放了,那麼這個方法是進不去的。

報告洩漏

當進入assertNotDealloc方法,很不幸,這個物件極有可能洩洩了,這個時候應該做的報告此處發生了洩漏

- (void)assertNotDealloc {
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }
    [MLeakedObjectProxy addLeakedObject:self];
    
    NSString *className = NSStringFromClass([self class]);
    NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
}

MLeaksFinder是如何報告的呢,我們一步追蹤,
首先

    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }

這個是什麼東西,

@interface MLeakedObjectProxy : NSObject

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs;
+ (void)addLeakedObject:(id)object;

@end

可以看出這個是洩漏物件的代理,這個物件只有2個類方法,分別是做什麼的呢,先來看isAnyObjectLeakedAtPtrs這個方法


 + (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        leakedObjectPtrs = [[NSMutableSet alloc] init];
    });
    
    if (!ptrs.count) {
        return NO;
    }
    if ([leakedObjectPtrs intersectsSet:ptrs]) {
        return YES;
    } else {
        return NO;
    }
}

結合上文中的assertNotDealloc方法可以看出,這個方法主要是判斷當前這個物件時候已經新增到了洩漏物件名單,如果是,那麼就不在添加了。
還有一個addLeakedObject方法,看名字不難看出是將洩漏物件新增到洩漏物件名單,以下下是其具體實現

+ (void)addLeakedObject:(id)object {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");
    
    MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init];
    proxy.object = object;
    proxy.objectPtr = @((uintptr_t)object);
    proxy.viewStack = [object viewStack];
    static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
    objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
    
    [leakedObjectPtrs addObject:proxy.objectPtr];
    
#if _INTERNAL_MLF_RC_ENABLED
    [MLeaksMessenger alertWithTitle:@"Memory Leak"
                            message:[NSString stringWithFormat:@"%@", proxy.viewStack]
                           delegate:proxy
              additionalButtonTitle:@"Retain Cycle"];
#else
    [MLeaksMessenger alertWithTitle:@"Memory Leak"
                            message:[NSString stringWithFormat:@"%@", proxy.viewStack]];
#endif
}

可以看出,構造了一個MLeakedObjectProxy物件,並將其加入到leakedObjectPtrs集合中,彈出alert框,然後需要找到引用環,將此物件指標傳給FBRetainCycleDetector來確定迴圈引用發生在哪裡(這塊功能是0.2版本新增的)。之後在螢幕上打印出堆疊資訊,看到這裡,還有一個疑問:MLeaksFinder是如何構建堆疊?

構建堆疊資訊

我們可以注意到,在UIViewController的分類中有這樣一段程式碼

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    
    [self willReleaseChildren:self.childViewControllers];
    [self willReleaseChild:self.presentedViewController];
    
    if (self.isViewLoaded) {
        [self willReleaseChild:self.view];
    }
    
    return YES;
}

其中willReleaseChildrenwillReleaseChild就是構造堆疊資訊的祕密所在,我們可以看到NSObject分類中的實現方法

- (void)willReleaseChild:(id)child {
    if (!child) {
        return;
    }
    
    [self willReleaseChildren:@[ child ]];
}

- (void)willReleaseChildren:(NSArray *)children {
    NSArray *viewStack = [self viewStack];
    NSSet *parentPtrs = [self parentPtrs];
    for (id child in children) {
        NSString *className = NSStringFromClass([child class]);
        [child setViewStack:[viewStack arrayByAddingObject:className]];
        [child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
        [child willDealloc];
    }
}

- (NSArray *)viewStack {
    NSArray *viewStack = objc_getAssociatedObject(self, kViewStackKey);
    if (viewStack) {
        return viewStack;
    }
    
    NSString *className = NSStringFromClass([self class]);
    return @[ className ];
}

- (void)setViewStack:(NSArray *)viewStack {
    objc_setAssociatedObject(self, kViewStackKey, viewStack, OBJC_ASSOCIATION_RETAIN);
}

willReleaseChildren和willReleaseChild方法的作用是向這個物件之中的子物件呼叫釋放的方法,如檢視的子檢視,UINavigationController的檢視控制器等等的子物件。構造堆疊資訊的原理就是,遞迴遍歷子物件,然後將父物件class名稱加上子物件類名,一步構造出一個檢視棧。出現洩漏則直接列印此物件的檢視棧即可。

其他

在UIViewController側滑的時候釋放方法需要做特殊的處理,在MLeaksFinder中添加了kHasBeenPoppedKey屬性來判斷是否釋放程式碼如下


//設定側滑的key
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
    UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
    
    if (!poppedViewController) {
        return nil;
    }
    
    // Detail VC in UISplitViewController is not dealloced until another detail VC is shown
 ...
    
    // VC is not dealloced until disappear when popped using a left-edge swipe gesture
    extern const void *const kHasBeenPoppedKey;
    objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
    
    return poppedViewController;
}

//發現是側滑則直接呼叫willDealloc方法
- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];
    
    if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
        [self willDealloc];
    }
}

總結

總得來說MLeaksFinder是一個質量很高的庫,在實用性和便利性上做到了完美結合,日常開發中也為我們的專案尋找到了洩漏點,值得一用,至於FBRetainCycleDetector如何檢測迴圈引用環,那得發一篇文章的篇幅來介紹了

相關推薦

MLeaksFinder檢查洩露

簡介MLeaksFinder是WeRead團隊開源的一個檢測iOS記憶體洩漏的框架,其使用非常簡單,只需將檔案加入專案中,如果有記憶體洩漏,3秒後自動彈出警告來捕捉迴圈引用。使得可以在開發快速找到80%記憶體洩漏,而使用Xcode Leak工具更適合大範圍的,全部的尋找洩漏點。特性通過閱讀MLeaksFind

VC記憶體洩露檢查工具:VisualLeakDetector

轉自 http://www.xdowns.com/article/170/Article_3060.html 初識Visual Leak Detector        靈活自由是C/C++語言的一大特色,而這也為C/C++程式設計師出了一個難題

精準 iOS 記憶體洩露檢測工具----MLeaksFinder

背景 平常我們都會用 Instrument 的 Leaks / Allocations 或其他一些開源庫進行記憶體洩露的排查,但它們都存在各種問題和不便,我們逐個來看這些工具的使用和存在的問題。 Leaks 先看看 Leaks,從蘋果的開發者文件裡可以看到,一個 app 的記憶體分三類:

iOS開發記憶體優化之自動檢測記憶體洩露檢查是否有迴圈引用,檢查記憶體為何如此大,Block迴圈引用的檢查

手機裝置的記憶體是一個共享資源。應用程式可能會不當的耗盡記憶體、崩潰,或者遭遇大幅度的效能降低。 Facebook iOS客戶端有很多功能,並且它們共享同一塊記憶體空間。如果任何特定的功能消耗過多的記憶體,就會影響到整個應用程式。這是可能發生的,比如,這個功能導致了記

C++記憶體洩露檢查工具

Linux下編寫C或者C++程式,有很多工具,但是主要編譯器仍然是gcc和g++。最近用到STL中的List程式設計,為了檢測寫的程式碼是否會發現記憶體洩漏,瞭解了一下相關的知識。 所有使用動態記憶體分配(dynamic memory allocation)的程式都有機

Windows中使用CRT函式檢查記憶體洩露和溢位

C++中可以使用new或malloc等函式分配記憶體,通常與delete和free配合使用,但是如果不小心遺忘而程式在持續new或malloc時就會造成程式所佔用的記憶體越來越大,即為“記憶體洩露”。通常寫資料的時候必須在程式開闢的空間中寫,如果不小心寫到了不是程式請求分配

iOS-記憶體洩露檢測工具(MLeaksFinder)

MLeaksFinder MLeaksFinder helps you find memory leaks in your iOS apps at develop time. It can automatically find leaks in UIVi

golang使用pprof檢查goroutine洩露

有一段時間,我們的推送服務socket佔用很不正常,我們自己統計的同時線上就10w的使用者,但是佔用的socket竟然達到30w,然後檢視goroutine的數量,發現已經60w+。 每個使用者佔用一個socket,而一個socket,有read和write兩個goro

VS2010檢查記憶體洩露

VS2010中的C++程式記憶體洩露檢測     MFC程式是支援記憶體檢測的。對於非MFC程式而言,CRT有一套記憶體洩露的函式,最常用的是 _CrtDumpMemoryLeaks();如下所示: #include <crtdbg.h> int main()

防止記憶體洩露 Linux下用Valgrind做檢查

 用C/C++開發其中最令人頭疼的一個問題就是記憶體管理,有時候為了查詢一個記憶體洩漏或者一個記憶體訪問越界,需要要花上好幾天時間,如果有一款工具能夠幫助我們做這件事情就好了,valgrind正好就是這樣的一款工具。 Valgrind是一款基於模擬linux下的程式偵錯程式

mysql日常檢查

justify mysql style 1) 進入mysql終端 # mysql -uroot -p 2) 查看版本 # mysqladmin -uroot -p version select @@version; 3) 列出數據庫 show databases; 4)

$apply方法(觸發臟檢查機制)

oct -1 alt module img rip ++ bsp area $swatch監聽方法 <!DOCTYPE html> <html><head lang="en"> <meta charset="UTF-8"&

sublime text 2 php 語法錯誤檢查

program windows 系統設置 系統 pro 安裝目錄 control 目錄 sub 使用sublime text 2 編寫php程序的時候,保存代碼的時候,直接檢查出語法錯誤,有利於提高效率。 1.安裝sublime text 2 package menu :

轉:Android檢查設備是否聯網

bool and roi systems lean oid div static andro 1 public static boolean isConnect(Context context) { 2 ConnectivityManager connec

在一個文件中添加刪除檢查用戶腳本

del format nbsp arc 文件中 form err 用戶腳本 $2 #!/bin/bash Path=/etc/user.conf if [ ! -f $Path ];then   touch $Path fi chmod 644 $Path if [

oracle11g dataguard 備庫數據同步的檢查方法

是否 ora grep targe 步驟 sequence rec stat 數據文件 概述: 一、環境 主庫: ip地址:192.168.122.203 oracle根目錄:/data/db/oracle SID:qyq

Memcache 內存分配策略和性能(使用)狀態檢查

asd一直在使用Memcache,但是對其內部的問題,如它內存是怎麽樣被使用的,使用一段時間後想看看一些狀態怎麽樣?一直都不清楚,查了又忘記,現在整理出該篇文章,方便自己查閱。本文不涉及安裝、操作。有興趣的同學可以查看之前寫的文章和Google。1:參數memcached -h memcached 1.4.

Unity Editor 檢查工程Prefab(預設)中的空組件

com game filepath ddr bug str 技術 程序 string 在我們做項目的過程中 經常會有預設中出現空的腳本 例如: 導致的原因是因為 腳本的丟失 現在我們來做一個檢查工程中有空腳本的預設工具 老規矩直接上代碼 放到工程就能用 using

PMD 5.7.0 發布,Java 程序代碼檢查工具

使用 beats odi 每日 代碼 amp 抓取 變量 更多 PMD 5.7.0 發布了。PMD 是一款采用 BSD 協議發布的 Java 程序代碼檢查工具。該工具可以做到檢查 Java 代碼中是否含有未使用的變量、是否含有空的抓取塊、是否含有不必要的對象等。該軟件功能強

idea xml 綠背景色 去掉拼寫檢查

ima src 拼寫檢查 bsp image alt 分享 logs idea 去掉背景色 去掉拼寫檢 idea xml 綠背景色 去掉拼寫檢查