MLeaksFinder 源碼學習筆記

MLeaksFinder 源碼學習筆記.png

0.前言

項目中集成了 MLeaksFinder 用于平時檢測內(nèi)存泄漏之用,它的基本工作原理也多少了解一些,最近恰好有點空閑時間,決定還是仔細看一下源碼實現(xiàn),畢竟自己查的才比較放心O(∩_∩)O哈哈~,于是就有了本文。

1.用法

MLeaksFinder 的使用非常人性化,直接通過 cocoaPods 導入工程中就行,當檢測到泄露的時候會自動在控制臺打印出相關堆棧信息。

2.原理

2.1 基本原理

一般情況下,當一個 ViewController 或 NavigationController 被 dismiss 或 pop 的時候,它自己、view、view的 subView 等都應該會很快釋放掉。于是,只需要在 dismiss 或 pop 之后檢測這些對象是否還存在,就可以判斷是否存在內(nèi)泄。

2.2 MLeaksFinder 的基本實現(xiàn)

為基類 NSObject 添加一個方法 -willDealloc ,它先用一個弱指針指向 self,并在一小段時間 (2秒) 后,通過這個弱指針調(diào)用 -assertNotDealloc,而 -assertNotDealloc 主要作用是打印堆棧信息 (早期版本是直接中斷言,不過那樣會打斷正常的開發(fā)工作)。

當我們認為某個對象應該要被釋放了,在釋放前調(diào)用 -assertNotDealloc ,如果 2 秒后它被釋放成功,weakSelf 就指向 nil,-assertNotDealloc 方法就不會執(zhí)行(向 nil 發(fā)送消息,實際什么也不會做),如果它沒被釋放,-assertNotDealloc 就會執(zhí)行,從而打印出堆棧信息。

于是,當一個NavigationController 或 UIViewController 被 pop 或 dismiss 時,我們遍歷它的所有 view,依次調(diào) -willDealloc(對 -willDealloc 的調(diào)用是通過 method-swizzle 追加到 pop/dismiss 方法中的),若 2 秒后沒被釋放,就會打印相關堆棧信息。

3.源碼

NSObject+MemoryLeak

先來看看基類 NSObject 的分類 NSObject+MemoryLeak ,這里提供了以下公開方法,詳見注釋,其中第二個方法只有這個宏 #define MLCheck(TARGET) [self willReleaseObject:(TARGET) relationship:@#TARGET]; 會用到,而這個宏是留給我們擴展功能使用的。

/// 入口方法
- (BOOL)willDealloc;

/// 用于擴展,即 MLCheck(TARGET) 中會用到
- (void)willReleaseObject:(id)object relationship:(NSString *)relationship;

// 用于構造堆棧信息
- (void)willReleaseChild:(id)child;
- (void)willReleaseChildren:(NSArray *)children;

/// 堆棧信息數(shù)組,元素是類名
- (NSArray *)viewStack;

/// 添加新類名到白名單
+ (void)addClassNamesToWhitelist:(NSArray *)classNames;

/// 交換方法
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL;

接著查看他的實現(xiàn)文件,首先是 -willDealloc 方法,實現(xiàn)如下,做了三件事:

- (BOOL)willDealloc {

    // 1.檢測白名單
    NSString *className = NSStringFromClass([self class]);
    if ([[NSObject classNamesWhitelist] containsObject:className])
        return NO;

    // 2.fix bug
    NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
    if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
        return NO;

    // 3.核心:延遲 2 秒執(zhí)行 -assertNotDealloc 方法
    __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;
}
  • 檢測白名單

    檢測當前對象是否在白名單中,如果在,就不調(diào)用 -assertNotDealloc 方法,既不檢測內(nèi)泄。構建基礎白名單時,使用了單例,確保只有一個,這個方法是私有的。

    + (NSMutableSet *)classNamesWhitelist {
        static NSMutableSet *whitelist = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            whitelist = [NSMutableSet setWithObjects:
                         @"UIFieldEditor", // UIAlertControllerTextField
                         @"UINavigationBar",
                         @"_UIAlertControllerActionView",
                         @"_UIVisualEffectBackdropView",
                         nil];
    
            // System's bug since iOS 10 and not fixed yet up to this ci.
            NSString *systemVersion = [UIDevice currentDevice].systemVersion;
            if ([systemVersion compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
                [whitelist addObject:@"UISwitch"];
            }
        });
        return whitelist;
    }
    

    另外,用戶也可以自行添加額外的類名,方法如下:

    + (void)addClassNamesToWhitelist:(NSArray *)classNames {
        [[self classNamesWhitelist] addObjectsFromArray:classNames];
    }
    
  • 修復一個 bug

    此處處理一個了bug,即 在button 的點擊事件或者UITableview 的點擊Cell的事件中調(diào)用self.navigationController popViewControllerAnimated:YES 時就報沒有釋放 ,詳見文末的參考。

  • 核心:延遲 2 秒執(zhí)行 -assertNotDealloc 方法

    用弱引用 weakSelf 指向 self,延遲 2 秒執(zhí)行 weakSelf 的 -assertNotDealloc 方法,這個方法如果能夠執(zhí)行,則說明當前對象泄露了。

從下面的代碼可以看出來,-assertNotDealloc 做了2 件事:

- (void)assertNotDealloc {

    // 1.檢測父控件體系中是否有沒被釋放的
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }
    [MLeakedObjectProxy addLeakedObject:self];

    // 2.打印堆棧信息
    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]);
}
  • 判斷當前對象父控件的層級體系中是否有沒被釋放的對象,如果有就不往下執(zhí)行了,否則把自己加進去,并打印堆棧信息。

    因為父對象的 -willDealloc 會先執(zhí)行,所以如果父對象一定會銷毀的話,那么也應該是先銷毀,即先從 MLeakedObjectProxy 中移除,加了這個判斷之后,就不會出現(xiàn)一個堆棧中出現(xiàn)多個未釋放對象的情況。

    這里用到了 2 個 MLeakedObjectProxy 中的方法 +isAnyObjectLeakedAtPtrs:+addLeakedObject:,后邊會講到。

  • 打印 viewStack 這個數(shù)組,數(shù)組里存放的是從父對象到子對象,一直到當前對象的類名。

我們看看 viewStack 的 setter 和 getter,這里用到了運行時機制,即利用關聯(lián)對象給一個類添加屬性信息。viewStack 是一個數(shù)組,存放的是類名,從 getter 可以看出來,初次使用時,直接將當前類名作為第一個元素添加進去了。

- (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);
}

順便看一下前邊用到的 parentPtrs 的 setter 和 getter,從下邊的源碼可看出來,二者的方法實現(xiàn)類似,只不過后者是一個集合 set,前者是 數(shù)組 array。

- (NSSet *)parentPtrs {
    NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
    if (!parentPtrs) {
        parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
    }
    return parentPtrs;
}

- (void)setParentPtrs:(NSSet *)parentPtrs {
    objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN);
}

那么,后續(xù)這個 viewStackparentPtrs 又是什么時候構建的呢?這里提供了 2 個供外界調(diào)用的構建方法,最終是依賴后一個方法 - willReleaseChildren 實現(xiàn)的。

- (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];
    }
}

仔細觀察上邊的 willReleaseChildren: 方法發(fā)現(xiàn),就做了兩件事:

  • 拿到當前對象的 viewStack 和 parentPtrs,然后遍歷 children,為每一個 child 設置 viewStackparentPtrs ,而且是將自己 (child) 加進去了的。

  • 執(zhí)行 [child willDealloc]; ,結合前邊提到的 willDealloc 知道,這就去檢測子類了。

在這個類的最后,提供了一個交換方法的方法:

+ (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);

    // YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name).
    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
}

交換方法代碼就做介紹了,下邊主要講一下用到的兩個宏:_INTERNAL_MLF_ENABLED_INTERNAL_MLF_RC_ENABLED

MLeaksFinder.h

為了弄清楚上邊提到的兩個宏,我們來看看 MLeaksFinder.h 這個文件,分了兩部分:

_INTERNAL_MLF_ENABLED

下邊的條件編譯語句用于確定 _INTERNAL_MLF_ENABLED 的值,即決定是否需要開啟內(nèi)存泄漏的檢測,默認是在 DEBUG 模式下檢測,當然,也可以自己修改這個值。

//#define MEMORY_LEAKS_FINDER_ENABLED 0

#ifdef MEMORY_LEAKS_FINDER_ENABLED

#define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED

#else

#define _INTERNAL_MLF_ENABLED DEBUG // DEBUG 環(huán)境的話,_INTERNAL_MLF_ENABLED == 1

#endif

_INTERNAL_MLF_RC_ENABLED

下邊的條件編譯語句用于確定 _INTERNAL_MLF_RC_ENABLED 的值,即決定是否需要開啟循環(huán)引用的檢測,默認是 如果項目中使用了 CocoaPods,則會通過 FBRetainCycleDetector 進行檢測。實際使用 CocoaPods 導入 MLeaksFinder 的時候,會將 FBRetainCycleDetector 一并導入,對于后者的工作原理,會單獨分一篇介紹的。

//#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1

#ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED

#define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED

#elif COCOAPODS

#define _INTERNAL_MLF_RC_ENABLED COCOAPODS

#endif

MLeakedObjectProxy

現(xiàn)在解決一個遺留問題,就是前邊 -willDealloc 方法中用到的 MLeakedObjectProxy 這個類,查看其 .h 文件發(fā)現(xiàn),對外只提供了兩個類方法:

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

先看第一個方法:

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");

    // 1.初始化 leakedObjectPtrs
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        leakedObjectPtrs = [[NSMutableSet alloc] init];
    });

    if (!ptrs.count) {
        return NO;
    }

    // 2.檢測 `leakedObjectPtrs` 與 `ptrs` 之間是否有交集
    // 當 leakedObjectPtrs 中 至少有一個對象也出現(xiàn)在 ptrs 中時,返回 YES。
    if ([leakedObjectPtrs intersectsSet:ptrs]) {
        return YES;
    } else {
        return NO;
    }
}

這里,首先初始化了 leakedObjectPtrs(為了保證唯一性,使用了單例),他是用來存儲發(fā)生內(nèi)泄的對象地址 (已經(jīng)轉成了數(shù)值,即 uintptr_t)。然后通過 -intersectsSet: 檢測 leakedObjectPtrsptrs 之間是否有交集,即傳入的 ptrs 中是否是泄露的對象。

下面看看第二個重要方法 + 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];

    // 1.給每一個 object 關聯(lián)一個代理即proxy
    static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
    objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);

    // 2.存儲 proxy.objectPtr 到集合 leakedObjectPtrs 里邊
    [leakedObjectPtrs addObject:proxy.objectPtr];

    // 3.彈框
#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
}
  • 給傳入的泄漏對象 object 關聯(lián)一個代理即 proxy
  • 存儲 proxy.objectPtr(實際是對象地址)到集合 leakedObjectPtrs 里邊
  • 彈框 AlertView:若 _INTERNAL_MLF_RC_ENABLED == 1,則彈框會增加檢測循環(huán)引用的選項;若 _INTERNAL_MLF_RC_ENABLED == 0,則僅展示堆棧信息。

當點擊彈框中的檢測循環(huán)引用按鈕時,相關的操作都在下面 AlertView 的代理方法里邊,即異步地通過 FBRetainCycleDetector 檢測循環(huán)引用,然后回到主線程,利用彈框提示用戶檢測結果。

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (!buttonIndex) {
        return;
    }

    id object = self.object;
    if (!object) {
        return;
    }

#if _INTERNAL_MLF_RC_ENABLED
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
        [detector addCandidate:self.object];
        NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:20];

        BOOL hasFound = NO;
        for (NSArray *retainCycle in retainCycles) {
            NSInteger index = 0;
            for (FBObjectiveCGraphElement *element in retainCycle) {
                if (element.object == object) {
                    NSArray *shiftedRetainCycle = [self shiftArray:retainCycle toIndex:index];

                    dispatch_async(dispatch_get_main_queue(), ^{
                        [MLeaksMessenger alertWithTitle:@"Retain Cycle"
                                                message:[NSString stringWithFormat:@"%@", shiftedRetainCycle]];
                    });
                    hasFound = YES;
                    break;
                }

                ++index;
            }
            if (hasFound) {
                break;
            }
        }
        if (!hasFound) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [MLeaksMessenger alertWithTitle:@"Retain Cycle"
                                        message:@"Fail to find a retain cycle"];
            });
        }
    });
#endif
  }
}

UIViewController+MemoryLeak

說了這么多,最后,以 UIViewController 為例,查看一下檢測內(nèi)泄的入口,即如何實現(xiàn)調(diào)用 -willdeallloc 方法,即如何開始構建內(nèi)泄的堆棧信息的。

下面是 +load 方法,就是將幾個系統(tǒng)方法和自定義方法交換,以便給系統(tǒng)方法增加新的操作。

+ (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)swizzled_viewDidDisappear:

先取出了 kHasBeenPoppedKey 對應的值,這個值是在右滑返回上個頁面并觸發(fā) pop 時,設置為 YES 的,說明當前 ViewController 要銷毀了,所以在這個時候調(diào)用了 -willDealloc 方法。

- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];

    if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
        [self willDealloc];
    }
}

- (void)swizzled_viewWillAppear:

與上邊對應,這里是在當前 ViewController 的視圖展示出來的時候,將 kHasBeenPoppedKey 關聯(lián)的值設為 NO,即當前 ViewController 沒有通過右滑返回。

- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];

    objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}

- swizzled_dismissViewControllerAnimated:

前邊兩個方法是針對滑動返回做的處理,這里是針對通過 present 的對象 dismiss 時的操作,即如果當前 ViewController 沒有 presentedViewController,就直接調(diào)用當前 ViewController 的 -willDealloc 方法檢測內(nèi)泄。

- (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    [self swizzled_dismissViewControllerAnimated:flag completion:completion];

    UIViewController *dismissedViewController = self.presentedViewController;
    if (!dismissedViewController && self.presentingViewController) {
        dismissedViewController = self;
    }

    if (!dismissedViewController) return;

    [dismissedViewController willDealloc];
}

最后重寫了 -willDealloc 方法,調(diào)用了 -willReleaseChildren: 方法,由于構建堆棧信息。

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }

    // viewController
    [self willReleaseChildren:self.childViewControllers];
    [self willReleaseChild:self.presentedViewController];

    // view
    if (self.isViewLoaded) {
        [self willReleaseChild:self.view];
    }

    return YES;
}

其它 UI 相關類的內(nèi)泄檢測與 ViewController 類似,這里就不啰嗦了。

4.參考

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容