內(nèi)存泄漏的檢測(cè)

一:block內(nèi)部可能存在的self的集中使用情況

(1)什么時(shí)候在 block 里面用 self,不需要使用 weak self?

當(dāng) block 本身不被 self 持有,而被別的對(duì)象持有,同時(shí)不產(chǎn)生循環(huán)引用的時(shí)候,就不需要使用 weak self 了。最常見(jiàn)的代碼就是 UIView 的動(dòng)畫(huà)代碼,我們?cè)谑褂?UIView 的 animateWithDuration:animations 方法 做動(dòng)畫(huà)的時(shí)候,并不需要使用 weak self,因?yàn)橐贸钟嘘P(guān)系是:UIView 的某個(gè)負(fù)責(zé)動(dòng)畫(huà)的對(duì)象持有了 block
block 持有了 self
因?yàn)?self 并不持有 block,所以就沒(méi)有循環(huán)引用產(chǎn)生,因?yàn)榫筒恍枰褂?weak self 了。

(2)有沒(méi)有這樣一個(gè)需求場(chǎng)景,block 會(huì)產(chǎn)生循環(huán)引用,但是業(yè)務(wù)又需要你不能使用 weak self? 如果有,請(qǐng)舉一個(gè)例子并且解釋這種情況下如何解決循環(huán)引用問(wèn)題。

需要不使用 weak self 的場(chǎng)景是:
例如 : 在使用NSOperation進(jìn)行異步下載網(wǎng)絡(luò)圖片的方法,然后在主線程進(jìn)行顯示的時(shí)候,在將操作添加到隊(duì)列的步奏 中,因?yàn)椴僮魇怯蒪lock構(gòu)成的,在block內(nèi)部先實(shí)現(xiàn)異步下載圖片,然后在主線程中加載圖片,刷新self.tableview的操作,此時(shí)因?yàn)閟elf.queue 引用操作block,block內(nèi)部又引用self,構(gòu)成循環(huán)引用;我們只要在將操作block添加到queue之后,將其果斷致為nil,就可以解除循環(huán)引用了
總結(jié)來(lái)說(shuō),解決循環(huán)引用問(wèn)題主要有兩個(gè)辦法:
代碼如下:

    // 已知 : op->self(VC)
    self.myblock = ^{
            NSLog(@"從網(wǎng)絡(luò)中加載...%@",app.name);
            // 模擬網(wǎng)絡(luò)延遲
            if (indexPath.row > 9) {
                [NSThread sleepForTimeInterval:10];
            }
            // 同步下載圖片
            NSURL *url = [NSURL URLWithString:app.icon];
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            
            // 圖片下載完成之后,回到主線程更新UI
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                if (image != nil) {
                    // 將圖片保存到圖片緩存中(dict),內(nèi)存緩存策略
                    // 字典和數(shù)組賦值空對(duì)象
                    [self.imageCache setObject:image forKey:app.icon];
                    // 刷新對(duì)應(yīng)的行
                    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                    // 移除操作,當(dāng)圖片為nil不能移除op
                }
                // 移除操作
                [self.operationCache removeObjectForKey:app.icon];
            }];
    };
 
    
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:_myblock];
    
#pragma attention 此處打破block循環(huán)的關(guān)鍵,在使用完block后果斷將其指為nil
    self.myblock = nil;
    // 將操作添加到操作緩存
    [self.operationCache setObject:op forKey:app.icon];
    // 將操作添加到隊(duì)列
    [self.queue addOperation:op];

第一個(gè)辦法是「事前避免」,我們?cè)跁?huì)產(chǎn)生循環(huán)引用的地方使用 weak 弱引用,以避免產(chǎn)生循環(huán)引用。
第二個(gè)辦法是「事后補(bǔ)救」,我們明確知道會(huì)存在循環(huán)引用,但是我們?cè)诤侠淼奈恢弥鲃?dòng)斷開(kāi)環(huán)中的一個(gè)引用,使得對(duì)象得以回收。

二:如何檢測(cè)block內(nèi)部是否存在循環(huán)引用

<1>利用第三方框架FBRetainCycleDetector

demo下載地址
看到facebook的一套內(nèi)存泄漏檢測(cè)工具,感覺(jué)不錯(cuò),想要查看原文可以點(diǎn)擊這里,后續(xù)在去分析相關(guān)的開(kāi)源工具FBRetainCycleDetector,源碼如下

在Facebook,許多工程師在不同的代碼倉(cāng)庫(kù)上工作,這不可避免會(huì)有內(nèi)存泄漏的情況發(fā)生,當(dāng)出現(xiàn)這種情況時(shí),我們需要快速的找到并修復(fù)它們。
已經(jīng)有一些工具來(lái)輔助我們找到內(nèi)存泄漏,不過(guò)需要大量的人工干預(yù):

傳統(tǒng)辦法:

打開(kāi)Xcode,選擇build for profiling.
載入Instruments工具
使用app, 嘗試盡可能多的重現(xiàn)場(chǎng)景和行為
查看instrument的leaks/memory
查找內(nèi)存泄漏的根源
修復(fù)問(wèn)題

這意味著每次都需要大量的手動(dòng)操作,導(dǎo)致我們可能在開(kāi)發(fā)周期內(nèi)無(wú)法盡早的定位以及修復(fù)內(nèi)存泄漏的問(wèn)題。
如果該過(guò)程能夠自動(dòng)化,我們就能夠在太多開(kāi)發(fā)者干預(yù)的情況下快速找到內(nèi)存泄漏。為此我們構(gòu)建一系列的工具來(lái)自動(dòng)化查找以及修復(fù)代碼倉(cāng)庫(kù)中的一些問(wèn)題,這些工具包括:FBRetainCycleDetector, FBAllocationTracker以及FBMemoryProfiler

Retain cycles(循環(huán)引用)
Objective-C使用引用計(jì)數(shù)來(lái)管理內(nèi)存以及釋放不使用的對(duì)象,任何一個(gè)對(duì)象可以持有(retain)其它對(duì)象,這樣只要前面的對(duì)象需要使用它,該對(duì)象就會(huì)一直保存在內(nèi)存,可以認(rèn)為對(duì)象“擁有”其它對(duì)象。

大部分情況下這都工作的很好,但是假如兩個(gè)對(duì)象最后互相“擁有”對(duì)方,直接或著更多通過(guò)其它對(duì)象間接的連接它們,這就會(huì)陷入一個(gè)僵局。這種持有引用的環(huán)就叫做循環(huán)引用。

示例圖

使用第三方框架解決

這一次分享的內(nèi)容就是用于檢測(cè)循環(huán)引用的框架 FBRetainCycleDetector 我們會(huì)分幾個(gè)部分來(lái)分析 FBRetainCycleDetector 是如何工作的:
檢測(cè)循環(huán)引用的基本原理以及過(guò)程
檢測(cè)設(shè)計(jì) NSObject 對(duì)象的循環(huán)引用問(wèn)題
檢測(cè)涉及 Associated Object 關(guān)聯(lián)對(duì)象的循環(huán)引用問(wèn)題
檢測(cè)涉及 Block 的循環(huán)引用問(wèn)題

我們會(huì)以類FBRetainCycleDetector
的- findRetainCycles
方法為入口,分析其實(shí)現(xiàn)原理以及運(yùn)行過(guò)程。
簡(jiǎn)單介紹一下FBRetainCycleDetector
的使用方法:

_RCDTestClass *testObject = [_RCDTestClass new];
testObject.object = testObject;

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:testObject];
NSSet *retainCycles = [detector findRetainCycles];

NSLog(@"%@", retainCycles);

初始化一個(gè) FBRetainCycleDetector 的實(shí)例
調(diào)用 - addCandidate: 方法添加潛在的泄露對(duì)象
執(zhí)行 - findRetainCycles 返回 retainCycles
在控制臺(tái)中的輸出是這樣的:

2016-07-29 15:26:42.043 xctest[30610:1003493] {(
        (
        "-> _object -> _RCDTestClass "
    )
)}

說(shuō)明 FBRetainCycleDetector 在代碼中發(fā)現(xiàn)了循環(huán)引用。
findRetainCycles 的實(shí)現(xiàn)
在具體開(kāi)始分析 FBRetainCycleDetector 代碼之前,我們可以先觀察一下方法 findRetainCycles 的調(diào)用棧:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles
└── - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length
    └── - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement stackDepth:(NSUInteger)stackDepth
        └── - (instancetype)initWithObject:(FBObjectiveCGraphElement *)object
            └── - (FBNodeEnumerator *)nextObject
                ├── - (NSArray<FBObjectiveCGraphElement *> *)_unwrapCycle:(NSArray<FBNodeEnumerator *> *)cycle
                ├── - (NSArray<FBObjectiveCGraphElement *> *)_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement *> *)array
                └── - (void)addObject:(ObjectType)anObject;

調(diào)用棧中最上面的兩個(gè)簡(jiǎn)單方法的實(shí)現(xiàn)都是比較容易理解的:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles {
    return [self findRetainCyclesWithMaxCycleLength:kFBRetainCycleDetectorDefaultStackDepth];
}

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length {
    NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *allRetainCycles = [NSMutableSet new];
    for (FBObjectiveCGraphElement *graphElement in _candidates) {
        NSSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [self _findRetainCyclesInObject:graphElement
                                                                                          stackDepth:length];
        [allRetainCycles unionSet:retainCycles];
    }
    [_candidates removeAllObjects];

    return allRetainCycles;
}
  • findRetainCycles 調(diào)用了 - findRetainCyclesWithMaxCycleLength: 傳入了 kFBRetainCycleDetectorDefaultStackDepth 參數(shù)來(lái)限制查找的深度,如果超過(guò)該深度(默認(rèn)為 10)就不會(huì)繼續(xù)處理下去了(查找的深度的增加會(huì)對(duì)性能有非常嚴(yán)重的影響)。

在 - findRetainCyclesWithMaxCycleLength: 中,我們會(huì)遍歷所有潛在的內(nèi)存泄露對(duì)象 candidate,執(zhí)行整個(gè)框架中最核心的方法 - _findRetainCyclesInObject:stackDepth:,由于這個(gè)方法的實(shí)現(xiàn)太長(zhǎng),這里會(huì)分幾塊對(duì)其進(jìn)行介紹,并會(huì)省略其中的注釋:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
                                                                 stackDepth:(NSUInteger)stackDepth {
    NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [NSMutableSet new];
    FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement];

    NSMutableArray<FBNodeEnumerator *> *stack = [NSMutableArray new];

    NSMutableSet<FBNodeEnumerator *> *objectsOnPath = [NSMutableSet new];

}

其實(shí)整個(gè)對(duì)象的相互引用情況可以看做一個(gè)有向圖,對(duì)象之間的引用就是圖的 Edge,每一個(gè)對(duì)象就是 Vertex,查找循環(huán)引用的過(guò)程就是在整個(gè)有向圖中查找環(huán)的過(guò)程,所以在這里我們使用 DFS 來(lái)掃面圖中的環(huán),這些環(huán)就是對(duì)象之間的循環(huán)引用

<2>采用第三方框架MLeaksFinder進(jìn)行檢測(cè)

(1):MLeaksFinder的使用方法

利用cocoa pods引入第三方框架后運(yùn)行項(xiàng)目(甚至在項(xiàng)目代碼中連頭文件都不用導(dǎo)入),效果圖如下:

屏幕快照 2017-02-09 下午10.40.00.png
屏幕快照 2017-02-09 下午10.39.50.png

就可以根據(jù)提示找到造成內(nèi)存泄漏的位置

(2)MLeaksFinder的簡(jiǎn)介

MLeaksFinder 提供了內(nèi)存泄露檢測(cè)更好的解決方案。只需要引入 MLeaksFinder,就可以自動(dòng)在 App 運(yùn)行過(guò)程檢測(cè)到內(nèi)存泄露的對(duì)象并立即提醒,無(wú)需打開(kāi)額外的工具,也無(wú)需為了檢測(cè)內(nèi)存泄露而一個(gè)個(gè)場(chǎng)景去重復(fù)地操作。MLeaksFinder 目前能自動(dòng)檢測(cè) UIViewController 和 UIView 對(duì)象的內(nèi)存泄露,而且也可以擴(kuò)展以檢測(cè)其它類型的對(duì)象。

MLeaksFinder 的使用很簡(jiǎn)單,參照 https://github.com/Zepo/MLeaksFinder ,基本上就是把 MLeaksFinder 目錄下的文件添加到你的項(xiàng)目中,就可以在運(yùn)行時(shí)(debug 模式下)幫助你檢測(cè)項(xiàng)目里的內(nèi)存泄露了,無(wú)需修改任何業(yè)務(wù)邏輯代碼,而且只在 debug 下開(kāi)啟,完全不影響你的 release 包。

當(dāng)發(fā)生內(nèi)存泄露時(shí),MLeaksFinder 會(huì)中斷言,并準(zhǔn)確的告訴你哪個(gè)對(duì)象泄露了。這里設(shè)計(jì)為中斷言而不是打日志讓程序繼續(xù)跑,是因?yàn)楹芏嗳瞬粫?huì)去看日志,斷言則能強(qiáng)制開(kāi)發(fā)者注意到并去修改,而不是犯拖延癥。

中斷言時(shí),控制臺(tái)會(huì)有如下提示,View-ViewController stack 從上往下看,該 stack 告訴你,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 沒(méi)被釋放。而且,這里我們可以肯定的是 MyTableViewController,UITableView,UITableViewWrapperView 這三個(gè)已經(jīng)成功釋放了。

從 MLeaksFinder 的使用方法可以看出,MLeaksFinder 具備以下優(yōu)點(diǎn):

使用簡(jiǎn)單,不侵入業(yè)務(wù)邏輯代碼,不用打開(kāi) Instrument
不需要額外的操作,你只需開(kāi)發(fā)你的業(yè)務(wù)邏輯,在你運(yùn)行調(diào)試時(shí)就能幫你檢測(cè)
內(nèi)存泄露發(fā)現(xiàn)及時(shí),更改完代碼后一運(yùn)行即能發(fā)現(xiàn)(這點(diǎn)很重要,你馬上就能意識(shí)到哪里寫(xiě)錯(cuò)了)
精準(zhǔn),能準(zhǔn)確地告訴你哪個(gè)對(duì)象沒(méi)被釋放

(3) MLeaksFinder的實(shí)現(xiàn)原理

MLeaksFinder 一開(kāi)始從 UIViewController 入手。我們知道,當(dāng)一個(gè) UIViewController 被 pop 或 dismiss 后,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設(shè)計(jì)成單例,或者持有它的強(qiáng)引用,但一般很少這樣做)。于是,我們只需在一個(gè) ViewController 被 pop 或 dismiss 一小段時(shí)間后,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在。

具體的方法是,為基類 NSObject 添加一個(gè)方法 -willDealloc 方法,該方法的作用是,先用一個(gè)弱指針指向 self,并在一小段時(shí)間(3秒)后,通過(guò)這個(gè)弱指針調(diào)用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中斷言。

- (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, @“”);
}

這樣,當(dāng)我們認(rèn)為某個(gè)對(duì)象應(yīng)該要被釋放了,在釋放前調(diào)用這個(gè)方法,如果3秒后它被釋放成功,weakSelf 就指向 nil,不會(huì)調(diào)用到 -assertNotDealloc 方法,也就不會(huì)中斷言,如果它沒(méi)被釋放(泄露了),-assertNotDealloc 就會(huì)被調(diào)用中斷言。這樣,當(dāng)一個(gè) UIViewController 被 pop 或 dismiss 時(shí)(我們認(rèn)為它應(yīng)該要被釋放了),我們遍歷該 UIViewController 上的所有 view,依次調(diào) -willDealloc,若3秒后沒(méi)被釋放,就會(huì)中斷言。

在這里,有幾個(gè)問(wèn)題需要解決:

不入侵開(kāi)發(fā)代碼

這里使用了 AOP 技術(shù),hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,關(guān)于如何 hook,請(qǐng)參考 Method Swizzling。

遍歷相關(guān)對(duì)象

在實(shí)際項(xiàng)目中,我們發(fā)現(xiàn)有時(shí)候一個(gè) UIViewController 被釋放了,但它的 view 沒(méi)被釋放,或者一個(gè) UIView 被釋放了,但它的某個(gè) subview 沒(méi)被釋放。這種內(nèi)存泄露的情況很常見(jiàn),因此,我們有必要遍歷基于 UIViewController 的整棵 View-ViewController 樹(shù)。我們通過(guò) UIViewController 的 presentedViewController 和 view 屬性,UIView 的 subviews 屬性等遞歸遍歷。對(duì)于某些 ViewController,如 UINavigationController,UISplitViewController 等,我們還需要遍歷 viewControllers 屬性。

構(gòu)建堆棧信息

需要構(gòu)建 View-ViewController stack 信息以告訴開(kāi)發(fā)者是哪個(gè)對(duì)象沒(méi)被釋放。在遞歸遍歷 View-ViewController 樹(shù)時(shí),子節(jié)點(diǎn)的 stack 信息由父節(jié)點(diǎn)的 stack 信息加上子結(jié)點(diǎn)信息即可。

例外機(jī)制

對(duì)于有些 ViewController,在被 pop 或 dismiss 后,不會(huì)被釋放(比如單例),因此需要提供機(jī)制讓開(kāi)發(fā)者指定哪個(gè)對(duì)象不會(huì)被釋放,這里可以通過(guò)重載上面的 -willDealloc 方法,直接 return NO 即可。

特殊情況

對(duì)于某些特殊情況,釋放的時(shí)機(jī)不大一樣(比如系統(tǒng)手勢(shì)返回時(shí),在劃到一半時(shí) hold 住,雖然已被 pop,但這時(shí)還不會(huì)被釋放,ViewController 要等到完全 disappear 后才釋放),需要做特殊處理,具體的特殊處理視具體情況而定。

系統(tǒng)View

某些系統(tǒng)的私有 View,不會(huì)被釋放(可能是系統(tǒng) bug 或者是系統(tǒng)出于某些原因故意這樣做的,這里就不去深究了),因此需要建立白名單

手動(dòng)擴(kuò)展

MLeaksFinder目前只檢測(cè) ViewController 跟 View 對(duì)象。為此,MLeaksFinder 提供了一個(gè)手動(dòng)擴(kuò)展的機(jī)制,你可以從 UIViewController 跟 UIView 出發(fā),去檢測(cè)其它類型的對(duì)象的內(nèi)存泄露。如下所示,我們可以檢測(cè) UIViewController 底下的 View Model:

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    MLCheck(self.viewModel);
    return YES;
}

這里的原理跟上面的是一樣的,宏 MLCheck() 做的事就是為傳進(jìn)來(lái)的對(duì)象建立 View-ViewController stack 信息,并對(duì)傳進(jìn)來(lái)的對(duì)象調(diào)用 -willDealloc 方法。

未來(lái)
MLeaksFinder 目前還在起步階段,它的內(nèi)存泄露檢測(cè)的想法是很簡(jiǎn)單,很直接的。雖然目前只能自動(dòng)地檢測(cè) UIViewController 和 UIView 相關(guān)的對(duì)象,然而在我們幾個(gè)大的項(xiàng)目中,已經(jīng)起到很大的作用,幫助我們發(fā)現(xiàn)很多歷史存在的內(nèi)存泄露,而且確保新提交的 UI 相關(guān)代碼不會(huì)引進(jìn)新的問(wèn)題。MLeaksFinder 會(huì)繼續(xù)探索覆蓋更廣的情況,提供更全面的檢測(cè),包括網(wǎng)絡(luò)層,數(shù)據(jù)存儲(chǔ)層等等。

詳細(xì)參考:
http://wereadteam.github.io/2016/02/22/MLeaksFinder/
https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/FBRetainCycleDetector/如何在%20iOS%20中解決循環(huán)引用的問(wèn)題.md
http://www.itdecent.cn/p/79d6a3a6a479

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

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

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