一:block內(nèi)部可能存在的self的集中使用情況
(1)什么時(shí)候在 block 里面用 self胀茵,不需要使用 weak self更卒?
當(dāng) block 本身不被 self 持有拼岳,而被別的對(duì)象持有抬闯,同時(shí)不產(chǎn)生循環(huán)引用的時(shí)候晾浴,就不需要使用 weak self 了涌献。最常見的代碼就是 UIView 的動(dòng)畫代碼星岗,我們?cè)谑褂?UIView 的 animateWithDuration:animations 方法 做動(dòng)畫的時(shí)候填大,并不需要使用 weak self,因?yàn)橐贸钟嘘P(guān)系是:UIView 的某個(gè)負(fù)責(zé)動(dòng)畫的對(duì)象持有了 block
block 持有了 self
因?yàn)?self 并不持有 block俏橘,所以就沒有循環(huán)引用產(chǎn)生允华,因?yàn)榫筒恍枰褂?weak self 了。
(2)有沒有這樣一個(gè)需求場(chǎng)景寥掐,block 會(huì)產(chǎn)生循環(huán)引用靴寂,但是業(yè)務(wù)又需要你不能使用 weak self? 如果有,請(qǐng)舉一個(gè)例子并且解釋這種情況下如何解決循環(huá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é)來說百炬,解決循環(huá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)斷開環(huán)中的一個(gè)引用德澈,使得對(duì)象得以回收虫埂。
二:如何檢測(cè)block內(nèi)部是否存在循環(huán)引用
<1>利用第三方框架FBRetainCycleDetector
demo下載地址
看到facebook的一套內(nèi)存泄漏檢測(cè)工具,感覺不錯(cuò)圃验,想要查看原文可以點(diǎn)擊這里掉伏,后續(xù)在去分析相關(guān)的開源工具FBRetainCycleDetector,源碼如下
在Facebook,許多工程師在不同的代碼倉(cāng)庫(kù)上工作澳窑,這不可避免會(huì)有內(nèi)存泄漏的情況發(fā)生斧散,當(dāng)出現(xiàn)這種情況時(shí),我們需要快速的找到并修復(fù)它們摊聋。
已經(jīng)有一些工具來輔助我們找到內(nèi)存泄漏鸡捐,不過需要大量的人工干預(yù):
傳統(tǒng)辦法:
打開Xcode,選擇build for profiling.
載入Instruments工具
使用app, 嘗試盡可能多的重現(xiàn)場(chǎng)景和行為
查看instrument的leaks/memory
查找內(nèi)存泄漏的根源
修復(fù)問題
這意味著每次都需要大量的手動(dòng)操作麻裁,導(dǎo)致我們可能在開發(fā)周期內(nèi)無法盡早的定位以及修復(fù)內(nèi)存泄漏的問題箍镜。
如果該過程能夠自動(dòng)化,我們就能夠在太多開發(fā)者干預(yù)的情況下快速找到內(nèi)存泄漏煎源。為此我們構(gòu)建一系列的工具來自動(dòng)化查找以及修復(fù)代碼倉(cāng)庫(kù)中的一些問題色迂,這些工具包括:FBRetainCycleDetector, FBAllocationTracker以及FBMemoryProfiler
Retain cycles(循環(huán)引用)
Objective-C使用引用計(jì)數(shù)來管理內(nèi)存以及釋放不使用的對(duì)象,任何一個(gè)對(duì)象可以持有(retain)其它對(duì)象手销,這樣只要前面的對(duì)象需要使用它歇僧,該對(duì)象就會(huì)一直保存在內(nèi)存,可以認(rèn)為對(duì)象“擁有”其它對(duì)象锋拖。
大部分情況下這都工作的很好诈悍,但是假如兩個(gè)對(duì)象最后互相“擁有”對(duì)方,直接或著更多通過其它對(duì)象間接的連接它們兽埃,這就會(huì)陷入一個(gè)僵局侥钳。這種持有引用的環(huán)就叫做循環(huán)引用。
使用第三方框架解決
這一次分享的內(nèi)容就是用于檢測(cè)循環(huán)引用的框架 FBRetainCycleDetector 我們會(huì)分幾個(gè)部分來分析 FBRetainCycleDetector 是如何工作的:
檢測(cè)循環(huán)引用的基本原理以及過程
檢測(cè)設(shè)計(jì) NSObject 對(duì)象的循環(huán)引用問題
檢測(cè)涉及 Associated Object 關(guān)聯(lián)對(duì)象的循環(huán)引用問題
檢測(cè)涉及 Block 的循環(huán)引用問題
我們會(huì)以類FBRetainCycleDetector
的- findRetainCycles
方法為入口柄错,分析其實(shí)現(xiàn)原理以及運(yùn)行過程舷夺。
簡(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 "
)
)}
說明 FBRetainCycleDetector 在代碼中發(fā)現(xiàn)了循環(huán)引用。
findRetainCycles 的實(shí)現(xiàn)
在具體開始分析 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ù)來限制查找的深度冕房,如果超過該深度(默認(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)引用的過程就是在整個(gè)有向圖中查找環(huán)的過程,所以在這里我們使用 DFS 來掃面圖中的環(huán)斑粱,這些環(huán)就是對(duì)象之間的循環(huán)引用
<2>采用第三方框架MLeaksFinder進(jìn)行檢測(cè)
(1):MLeaksFinder的使用方法
利用cocoa pods引入第三方框架后運(yùn)行項(xiàng)目(甚至在項(xiàng)目代碼中連頭文件都不用導(dǎo)入),效果圖如下:
就可以根據(jù)提示找到造成內(nèi)存泄漏的位置
(2)MLeaksFinder的簡(jiǎn)介
MLeaksFinder 提供了內(nèi)存泄露檢測(cè)更好的解決方案弃揽。只需要引入 MLeaksFinder,就可以自動(dòng)在 App 運(yùn)行過程檢測(cè)到內(nèi)存泄露的對(duì)象并立即提醒则北,無需打開額外的工具矿微,也無需為了檢測(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)存泄露了,無需修改任何業(yè)務(wù)邏輯代碼方篮,而且只在 debug 下開啟名秀,完全不影響你的 release 包。
當(dāng)發(fā)生內(nèi)存泄露時(shí)藕溅,MLeaksFinder 會(huì)中斷言泰偿,并準(zhǔn)確的告訴你哪個(gè)對(duì)象泄露了。這里設(shè)計(jì)為中斷言而不是打日志讓程序繼續(xù)跑蜈垮,是因?yàn)楹芏嗳瞬粫?huì)去看日志耗跛,斷言則能強(qiáng)制開發(fā)者注意到并去修改,而不是犯拖延癥攒发。
中斷言時(shí)调塌,控制臺(tái)會(huì)有如下提示,View-ViewController stack 從上往下看惠猿,該 stack 告訴你羔砾,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 沒被釋放。而且偶妖,這里我們可以肯定的是 MyTableViewController姜凄,UITableView,UITableViewWrapperView 這三個(gè)已經(jīng)成功釋放了趾访。
從 MLeaksFinder 的使用方法可以看出态秧,MLeaksFinder 具備以下優(yōu)點(diǎn):
使用簡(jiǎn)單,不侵入業(yè)務(wù)邏輯代碼扼鞋,不用打開 Instrument
不需要額外的操作申鱼,你只需開發(fā)你的業(yè)務(wù)邏輯愤诱,在你運(yùn)行調(diào)試時(shí)就能幫你檢測(cè)
內(nèi)存泄露發(fā)現(xiàn)及時(shí),更改完代碼后一運(yùn)行即能發(fā)現(xiàn)(這點(diǎn)很重要捐友,你馬上就能意識(shí)到哪里寫錯(cuò)了)
精準(zhǔn)淫半,能準(zhǔn)確地告訴你哪個(gè)對(duì)象沒被釋放
(3) MLeaksFinder的實(shí)現(xiàn)原理
MLeaksFinder 一開始從 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秒)后柑爸,通過這個(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ì)中斷言厘熟,如果它沒被釋放(泄露了)屯蹦,-assertNotDealloc 就會(huì)被調(diào)用中斷言。這樣绳姨,當(dāng)一個(gè) UIViewController 被 pop 或 dismiss 時(shí)(我們認(rèn)為它應(yīng)該要被釋放了)登澜,我們遍歷該 UIViewController 上的所有 view,依次調(diào) -willDealloc飘庄,若3秒后沒被釋放脑蠕,就會(huì)中斷言。
在這里竭宰,有幾個(gè)問題需要解決:
不入侵開發(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 沒被釋放哼审,或者一個(gè) UIView 被釋放了,但它的某個(gè) subview 沒被釋放孕豹。這種內(nèi)存泄露的情況很常見涩盾,因此,我們有必要遍歷基于 UIViewController 的整棵 View-ViewController 樹励背。我們通過 UIViewController 的 presentedViewController 和 view 屬性春霍,UIView 的 subviews 屬性等遞歸遍歷。對(duì)于某些 ViewController叶眉,如 UINavigationController址儒,UISplitViewController 等,我們還需要遍歷 viewControllers 屬性衅疙。
構(gòu)建堆棧信息
需要構(gòu)建 View-ViewController stack 信息以告訴開發(fā)者是哪個(gè)對(duì)象沒被釋放莲趣。在遞歸遍歷 View-ViewController 樹時(shí),子節(jié)點(diǎn)的 stack 信息由父節(jié)點(diǎn)的 stack 信息加上子結(jié)點(diǎn)信息即可饱溢。
例外機(jī)制
對(duì)于有些 ViewController喧伞,在被 pop 或 dismiss 后,不會(huì)被釋放(比如單例)绩郎,因此需要提供機(jī)制讓開發(fā)者指定哪個(gè)對(duì)象不會(huì)被釋放潘鲫,這里可以通過重載上面的 -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)來的對(duì)象建立 View-ViewController stack 信息抱环,并對(duì)傳進(jìn)來的對(duì)象調(diào)用 -willDealloc 方法壳快。
未來
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)新的問題因宇。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)引用的問題.md
http://www.reibang.com/p/79d6a3a6a479