什么是內存泄漏,通俗來說就是有一塊內存區域被你占用了,但你又不使用這塊區域也不讓別人用,造成內存浪費,這就是內存泄漏,泄漏嚴重會造成內存吃緊,嚴重的會使程序崩潰;
內存泄漏對于以前MRC開發來說相當痛苦,需要耗費大量精力管理內存,引入ARC機制后,系統自動管理內存,大大減輕了開發工作量,但一些特殊情況仍然會有內存泄漏發生,需要特別注意。
一般易造成泄漏的點
- Retain Cycle,Block強引用
- NSTimer釋放不當
- 第三方提供方法造成的內存泄漏
- CoreFoundation方式申請的內存,忘記釋放
eg:
block循環
[cell setSelectTagCityBlock:^(NSIndexPath *indexPath, NSInteger index){
[self tableView:_tableViewCityList didSelectRowAtIndexPath:indexPath index:index];
}];
一般用weak打破保留環
@WeakObj(self)
[cell setSelectTagCityBlock:^(NSIndexPath *indexPath, NSInteger index){
if (selfWeak)
{
[selfWeak tableView:selfWeak.tableViewCityList didSelectRowAtIndexPath:indexPath index:index];
}
}];
AFNetWorking上的經典代碼,防止循環引用的
//創建__weak弱引用,防止強引用互相持有
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
//創建局部__strong強引用,防止多線程情況下weakSelf被析構
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
weak 本身是可以避免循環引用的問題的,但是其會導致外部對象釋放了之后,block 內部也訪問不到這個對象的問題,我們可以通過在 block 內部聲明一個 strong 的變量來指向 weakObj,使外部對象既能在 block 內部保持住,又能避免循環引用的問題
block 本身無法避免循環引用的問題,但是我們可以通過在 block 內部手動把 blockObj 賦值為 nil 的方式來避免循環引用的問題。另外一點就是 block 修飾的變量在 block 內外都是唯一的,要注意這個特性可能帶來的隱患。
_timer = [NSTimer timerWithTimeInterval:[refreshTime integerValue]
target:self
selector:@selector(doFSearchDoubleBackNumberRequest:)
userInfo:searchResult
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
Timer 添加到 Runloop 的時候,會被 Runloop 強引用。
Timer 又會有一個對 Target 的強引用。
所以說如果不對Timer進行釋放,Timer的targer(self)也一直不會被釋放。
有時候我們我們對某個Timer的targer設置了nil。但沒設置[timer invalidate]。
其實這個對象還是沒被釋放的。timer對應的執行方法也一直會在線程中執行。容易造成內存泄露。
注:repeats:NO不會強引用
常規的監測方法
-
Analyze靜態分析 (command + shift + b)
主要分析以下四種問題:
1、邏輯錯誤:訪問空指針或未初始化的變量等;
2、內存管理錯誤:如內存泄漏等;
3、聲明錯誤:從未使用過的變量;
4、Api調用錯誤:未包含使用的庫和框架。
靜態分析結果會有警告提示
image
-
Instruments中的Leak動態分析內存泄漏
product->profile ->leaks 打開工具主窗口
image點擊暫停,將鼠標移到叉號上面點擊鎖定,點擊下方的“田”字格,選擇callTree,
選擇中間的齒輪,選中選項中的 invert Call Tree 和Hide System Libraries
- Separate by Thread:按線程分開做分析,這樣更容易揪出那些吃資源的問題線程。特別是對于主線程,它要處理和渲染所有的接口數據,一旦受到阻塞,程序必然卡頓或停止響應。
- Invert Call Tree:反向輸出調用樹。把調用層級最深的方法顯示在最上面,更容易找到最耗時的操作。
- Hide System Libraries:隱藏系統庫文件。過濾掉各種系統調用,只顯示自己的代碼調用。
- Flattern Recursion:拼合遞歸。將同一遞歸函數產生的多條堆棧(因為遞歸函數會調用自己)合并為一條。
雙擊左邊 Call Tree 下面的任意一行,查看內存泄漏的代碼位置
-
Allocation工具了解內存的分配情況
每次點擊generations(是兩個時間標記之間所有仍然活著的對象的快照),生成快照,而且 Allocations 會記錄從上回內存快照到這次內存快照這個時間段內,新分配的內存信息,數次 push 跟 pop 之后,內存還不斷增長,則有內存泄露
image開源項目 HeapInspector-for-iOS 可以說是 Allocations 的改進,它通過 hook 掉 alloc,dealloc,retain,release 等方法,來記錄對象的生命周期,親測不太好用
其他工具用途
- Core Data:監測讀取、緩存未命中、保存等操作,能直觀顯示是否保存次數遠超實際需要。
- Cocoa Layout:觀察約束變化,找出布局代碼的問題所在。
- Network:跟蹤 TCP / IP和 UDP / IP 連接。
- Automations:創建和編輯測試腳本來自動化 iOS 應用的用戶界面測試
XCode8后新特性
-
Debug Memory Graph
Debug Memory Graph, 直接以關系圖的形式來告訴你各個對象的持有關系, 泄露時會有紫色的小感嘆號出現,
在開發過程中,因為語法或明顯的代碼錯誤(例如Retain Cycle),編譯器可以發現并報黃色或紅色警告
實時監測內存占用情況
直接選擇一個對象,查看與其相關的內存關系
- 綠色的一般都是 UIKit 控件及其子類
- 藍色一般 NSObject 類及其子類
- 黃色一般都是容器類型及其子類
- 灰色括號是指 block
第三方工具
-
原理還是很簡單的, 它swizzle了NavigationController的Push和Pop相關方法來管理viewController和view的生命周期, 在你Pop掉viewController的時候, 會執行這么一段代碼
- (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 assertNotDealloc]; 如果這個時候view和viewController已經釋放了, 那么weakSelf應該為nil, 所以將不會觸發斷言, 否則將會打印日志, 觸發斷言.
- 關于swizzleSEL
一種簡寫方式
void MethodSwizzle(Class c,SEL origSEL,SEL overrideSEL) {
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod= class_getInstanceMethod(c, overrideSEL);
}
傳入兩個參數,原方法選擇子,新方法選擇子,并通過class_getInstanceMethod()拿到對應的Method
- 有兩種情況要考慮一下。第一種情況是要復寫的方法(overridden)并沒有在目標類中實現,而是在其父類中實現了。第二種情況是這個方法已經存在于目標類中。這兩種情況要區別對待
(它的目的是為了使用一個重寫的方法替換掉原來的方法。但重寫的方法可能是在父類中重寫的,也可能是在子類中重寫的。)
- 對于第一種情況(重寫方法不存在),應當先在目標類增加一個新的實現方法(override),然后將復寫的方法替換為原先的實現
- 對于第二情況(目標類存在重寫的方法)。這時可以通過method_exchangeImplementations來完成交換.
標準方式
+ (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)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);
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
}
class_addMethod:是相對于實現來的說的,將本來不存在于被操作的Class里的newMethod的實現添加在被操作的Class里,并使用origSel作為其選擇子
如果發現方法已經存在,會失敗返回,也可以用來做檢查用,我們這里是為了避免源方法沒有實現的情況;如果方法沒有存在,我們則先嘗試添加被替換的方法的實現
1.如果返回成功:則說明被替換方法沒有存在.也就是被替換的方法沒有被實現,我們需要先把這個方法實現,然后再執行我們想要的效果,用我們自定義的方法去替換被替換的方法. 這里使用到的是class_replaceMethod這個方法. class_replaceMethod本身會嘗試調用class_addMethod和method_setImplementation,所以直接調用class_replaceMethod就可以了)
2.如果返回失敗:則說明被替換方法已經存在.直接將兩個方法的實現交換即可
class_replaceMethod,addMethod成功完成后,從參數可以看出,目的是換掉method_getImplaementation(roiginMethod)的選擇子,將原方法的實現的SEL換成新方法的SEL
-
能夠檢測指定對象的引用情況,并把所存在的引用循環中各對象和引用在終端進行打印
#import <FBRetainCycleDetector/FBRetainCycleDetector.h>
_handlerBlock = ^{
NSLog(@"%@", self);
};
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"%@", retainCycles);
打印結果類似于:
```
{(
(
"-> DetailViewController ",
"-> _handlerBlock -> __NSMallocBlock__ "
)
)}
```
DetailViewController通過_handlerBlock實例變量引用一個Block對象,而該Block又引用了DetailViewController對象。如果不存在引用循環的話,打印的結果將是空的
原理: 循環引用可以包含任何數量的對象。一個壞的連接會導致很多環的時候,這就復雜了
在環中,A→B是一個壞連接,創建了兩個環:A-B-C-D 和 A-B-C-E。
這有兩個問題:
- 不想給一個壞連接導致的兩個循環引用分別標記。
- 不想給可能代表兩個問題的兩個循環引用一起標記,即使它們共享一個連接。
所以需要給循環引用定義簇組(clusters),鑒于這些啟發,我們寫了個算法來找到這些問題。
- 在給定的時間收集所有的環。
- 對于每一個環,提取Facebook特定的類名。
- 對于每一個環,找到包含在環內的被報告的最小的環。
- 依據上面的最小環,將環添加到組中。
- 只報告最小環。
通過Pod安裝后,通過以下代碼激活即可。
[[PLeakSniffer sharedInstance] installLeakSniffer];
[[PLeakSniffer sharedInstance] addIgnoreList:@[@"MySingletonController"]];
addIgnoreList可以添加一些特殊的忽略名單,比如單例這種無法正確預測泄漏的對象
原理: 如果Controller被釋放了,但其曾經持有過的子對象如果還存在,那么這些子對象就是泄漏的可疑目標
子對象(比如view)建立一個對controller的weak引用,如果Controller被釋放,這個weak引用也隨之置為nil。那怎么知道子對象沒有被釋放呢?用一個單例對象每個一小段時間發出一個ping通知去ping這個子對象,如果子對象還活著就會一個pong通知。所以結論就是:如果子對象的controller已不存在,但還能響應這個ping通知,那么這個對象就是可疑的泄漏對象。完整的結構可以用下圖表示
參考資料