一:block內部可能存在的self的集中使用情況
(1)什么時候在 block 里面用 self,不需要使用 weak self?
當 block 本身不被 self 持有,而被別的對象持有,同時不產生循環引用的時候,就不需要使用 weak self 了。最常見的代碼就是 UIView 的動畫代碼,我們在使用 UIView 的 animateWithDuration:animations 方法 做動畫的時候,并不需要使用 weak self,因為引用持有關系是:UIView 的某個負責動畫的對象持有了 block
block 持有了 self
因為 self 并不持有 block,所以就沒有循環引用產生,因為就不需要使用 weak self 了。
(2)有沒有這樣一個需求場景,block 會產生循環引用,但是業務又需要你不能使用 weak self? 如果有,請舉一個例子并且解釋這種情況下如何解決循環引用問題。
需要不使用 weak self 的場景是:
例如 : 在使用NSOperation進行異步下載網絡圖片的方法,然后在主線程進行顯示的時候,在將操作添加到隊列的步奏 中,因為操作是由block構成的,在block內部先實現異步下載圖片,然后在主線程中加載圖片,刷新self.tableview的操作,此時因為self.queue 引用操作block,block內部又引用self,構成循環引用;我們只要在將操作block添加到queue之后,將其果斷致為nil,就可以解除循環引用了
總結來說,解決循環引用問題主要有兩個辦法:
代碼如下:
// 已知 : op->self(VC)
self.myblock = ^{
NSLog(@"從網絡中加載...%@",app.name);
// 模擬網絡延遲
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),內存緩存策略
// 字典和數組賦值空對象
[self.imageCache setObject:image forKey:app.icon];
// 刷新對應的行
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
// 移除操作,當圖片為nil不能移除op
}
// 移除操作
[self.operationCache removeObjectForKey:app.icon];
}];
};
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:_myblock];
#pragma attention 此處打破block循環的關鍵,在使用完block后果斷將其指為nil
self.myblock = nil;
// 將操作添加到操作緩存
[self.operationCache setObject:op forKey:app.icon];
// 將操作添加到隊列
[self.queue addOperation:op];
第一個辦法是「事前避免」,我們在會產生循環引用的地方使用 weak 弱引用,以避免產生循環引用。
第二個辦法是「事后補救」,我們明確知道會存在循環引用,但是我們在合理的位置主動斷開環中的一個引用,使得對象得以回收。
二:如何檢測block內部是否存在循環引用
<1>利用第三方框架FBRetainCycleDetector
demo下載地址
看到facebook的一套內存泄漏檢測工具,感覺不錯,想要查看原文可以點擊這里,后續在去分析相關的開源工具FBRetainCycleDetector,源碼如下
在Facebook,許多工程師在不同的代碼倉庫上工作,這不可避免會有內存泄漏的情況發生,當出現這種情況時,我們需要快速的找到并修復它們。
已經有一些工具來輔助我們找到內存泄漏,不過需要大量的人工干預:
傳統辦法:
打開Xcode,選擇build for profiling.
載入Instruments工具
使用app, 嘗試盡可能多的重現場景和行為
查看instrument的leaks/memory
查找內存泄漏的根源
修復問題
這意味著每次都需要大量的手動操作,導致我們可能在開發周期內無法盡早的定位以及修復內存泄漏的問題。
如果該過程能夠自動化,我們就能夠在太多開發者干預的情況下快速找到內存泄漏。為此我們構建一系列的工具來自動化查找以及修復代碼倉庫中的一些問題,這些工具包括:FBRetainCycleDetector, FBAllocationTracker以及FBMemoryProfiler
Retain cycles(循環引用)
Objective-C使用引用計數來管理內存以及釋放不使用的對象,任何一個對象可以持有(retain)其它對象,這樣只要前面的對象需要使用它,該對象就會一直保存在內存,可以認為對象“擁有”其它對象。
大部分情況下這都工作的很好,但是假如兩個對象最后互相“擁有”對方,直接或著更多通過其它對象間接的連接它們,這就會陷入一個僵局。這種持有引用的環就叫做循環引用。
使用第三方框架解決
這一次分享的內容就是用于檢測循環引用的框架 FBRetainCycleDetector 我們會分幾個部分來分析 FBRetainCycleDetector 是如何工作的:
檢測循環引用的基本原理以及過程
檢測設計 NSObject 對象的循環引用問題
檢測涉及 Associated Object 關聯對象的循環引用問題
檢測涉及 Block 的循環引用問題
我們會以類FBRetainCycleDetector
的- findRetainCycles
方法為入口,分析其實現原理以及運行過程。
簡單介紹一下FBRetainCycleDetector
的使用方法:
_RCDTestClass *testObject = [_RCDTestClass new];
testObject.object = testObject;
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:testObject];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"%@", retainCycles);
初始化一個 FBRetainCycleDetector 的實例
調用 - addCandidate: 方法添加潛在的泄露對象
執行 - findRetainCycles 返回 retainCycles
在控制臺中的輸出是這樣的:
2016-07-29 15:26:42.043 xctest[30610:1003493] {(
(
"-> _object -> _RCDTestClass "
)
)}
說明 FBRetainCycleDetector 在代碼中發現了循環引用。
findRetainCycles 的實現
在具體開始分析 FBRetainCycleDetector 代碼之前,我們可以先觀察一下方法 findRetainCycles 的調用棧:
- (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;
調用棧中最上面的兩個簡單方法的實現都是比較容易理解的:
- (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 調用了 - findRetainCyclesWithMaxCycleLength: 傳入了 kFBRetainCycleDetectorDefaultStackDepth 參數來限制查找的深度,如果超過該深度(默認為 10)就不會繼續處理下去了(查找的深度的增加會對性能有非常嚴重的影響)。
在 - findRetainCyclesWithMaxCycleLength: 中,我們會遍歷所有潛在的內存泄露對象 candidate,執行整個框架中最核心的方法 - _findRetainCyclesInObject:stackDepth:,由于這個方法的實現太長,這里會分幾塊對其進行介紹,并會省略其中的注釋:
- (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];
}
其實整個對象的相互引用情況可以看做一個有向圖,對象之間的引用就是圖的 Edge,每一個對象就是 Vertex,查找循環引用的過程就是在整個有向圖中查找環的過程,所以在這里我們使用 DFS 來掃面圖中的環,這些環就是對象之間的循環引用
<2>采用第三方框架MLeaksFinder進行檢測
(1):MLeaksFinder的使用方法
利用cocoa pods引入第三方框架后運行項目(甚至在項目代碼中連頭文件都不用導入),效果圖如下:
就可以根據提示找到造成內存泄漏的位置
(2)MLeaksFinder的簡介
MLeaksFinder 提供了內存泄露檢測更好的解決方案。只需要引入 MLeaksFinder,就可以自動在 App 運行過程檢測到內存泄露的對象并立即提醒,無需打開額外的工具,也無需為了檢測內存泄露而一個個場景去重復地操作。MLeaksFinder 目前能自動檢測 UIViewController 和 UIView 對象的內存泄露,而且也可以擴展以檢測其它類型的對象。
MLeaksFinder 的使用很簡單,參照 https://github.com/Zepo/MLeaksFinder ,基本上就是把 MLeaksFinder 目錄下的文件添加到你的項目中,就可以在運行時(debug 模式下)幫助你檢測項目里的內存泄露了,無需修改任何業務邏輯代碼,而且只在 debug 下開啟,完全不影響你的 release 包。
當發生內存泄露時,MLeaksFinder 會中斷言,并準確的告訴你哪個對象泄露了。這里設計為中斷言而不是打日志讓程序繼續跑,是因為很多人不會去看日志,斷言則能強制開發者注意到并去修改,而不是犯拖延癥。
中斷言時,控制臺會有如下提示,View-ViewController stack 從上往下看,該 stack 告訴你,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 沒被釋放。而且,這里我們可以肯定的是 MyTableViewController,UITableView,UITableViewWrapperView 這三個已經成功釋放了。
從 MLeaksFinder 的使用方法可以看出,MLeaksFinder 具備以下優點:
使用簡單,不侵入業務邏輯代碼,不用打開 Instrument
不需要額外的操作,你只需開發你的業務邏輯,在你運行調試時就能幫你檢測
內存泄露發現及時,更改完代碼后一運行即能發現(這點很重要,你馬上就能意識到哪里寫錯了)
精準,能準確地告訴你哪個對象沒被釋放
(3) MLeaksFinder的實現原理
MLeaksFinder 一開始從 UIViewController 入手。我們知道,當一個 UIViewController 被 pop 或 dismiss 后,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設計成單例,或者持有它的強引用,但一般很少這樣做)。于是,我們只需在一個 ViewController 被 pop 或 dismiss 一小段時間后,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在。
具體的方法是,為基類 NSObject 添加一個方法 -willDealloc 方法,該方法的作用是,先用一個弱指針指向 self,并在一小段時間(3秒)后,通過這個弱指針調用 -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, @“”);
}
這樣,當我們認為某個對象應該要被釋放了,在釋放前調用這個方法,如果3秒后它被釋放成功,weakSelf 就指向 nil,不會調用到 -assertNotDealloc 方法,也就不會中斷言,如果它沒被釋放(泄露了),-assertNotDealloc 就會被調用中斷言。這樣,當一個 UIViewController 被 pop 或 dismiss 時(我們認為它應該要被釋放了),我們遍歷該 UIViewController 上的所有 view,依次調 -willDealloc,若3秒后沒被釋放,就會中斷言。
在這里,有幾個問題需要解決:
不入侵開發代碼
這里使用了 AOP 技術,hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,關于如何 hook,請參考 Method Swizzling。
遍歷相關對象
在實際項目中,我們發現有時候一個 UIViewController 被釋放了,但它的 view 沒被釋放,或者一個 UIView 被釋放了,但它的某個 subview 沒被釋放。這種內存泄露的情況很常見,因此,我們有必要遍歷基于 UIViewController 的整棵 View-ViewController 樹。我們通過 UIViewController 的 presentedViewController 和 view 屬性,UIView 的 subviews 屬性等遞歸遍歷。對于某些 ViewController,如 UINavigationController,UISplitViewController 等,我們還需要遍歷 viewControllers 屬性。
構建堆棧信息
需要構建 View-ViewController stack 信息以告訴開發者是哪個對象沒被釋放。在遞歸遍歷 View-ViewController 樹時,子節點的 stack 信息由父節點的 stack 信息加上子結點信息即可。
例外機制
對于有些 ViewController,在被 pop 或 dismiss 后,不會被釋放(比如單例),因此需要提供機制讓開發者指定哪個對象不會被釋放,這里可以通過重載上面的 -willDealloc 方法,直接 return NO 即可。
特殊情況
對于某些特殊情況,釋放的時機不大一樣(比如系統手勢返回時,在劃到一半時 hold 住,雖然已被 pop,但這時還不會被釋放,ViewController 要等到完全 disappear 后才釋放),需要做特殊處理,具體的特殊處理視具體情況而定。
系統View
某些系統的私有 View,不會被釋放(可能是系統 bug 或者是系統出于某些原因故意這樣做的,這里就不去深究了),因此需要建立白名單
手動擴展
MLeaksFinder目前只檢測 ViewController 跟 View 對象。為此,MLeaksFinder 提供了一個手動擴展的機制,你可以從 UIViewController 跟 UIView 出發,去檢測其它類型的對象的內存泄露。如下所示,我們可以檢測 UIViewController 底下的 View Model:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}
這里的原理跟上面的是一樣的,宏 MLCheck() 做的事就是為傳進來的對象建立 View-ViewController stack 信息,并對傳進來的對象調用 -willDealloc 方法。
未來
MLeaksFinder 目前還在起步階段,它的內存泄露檢測的想法是很簡單,很直接的。雖然目前只能自動地檢測 UIViewController 和 UIView 相關的對象,然而在我們幾個大的項目中,已經起到很大的作用,幫助我們發現很多歷史存在的內存泄露,而且確保新提交的 UI 相關代碼不會引進新的問題。MLeaksFinder 會繼續探索覆蓋更廣的情況,提供更全面的檢測,包括網絡層,數據存儲層等等。
詳細參考:
http://wereadteam.github.io/2016/02/22/MLeaksFinder/
https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/FBRetainCycleDetector/如何在%20iOS%20中解決循環引用的問題.md
http://www.lxweimin.com/p/79d6a3a6a479