20190702 更新,新增 webView 的內存泄漏分析,歡迎你們評論補充。html
接上篇,本篇主要講解通知和 KVO 不移除觀察者、block 循環引用 、NSThread 和 RunLoop一塊兒使用形成的內存泄漏。git
1.一、iOS9 之後,通常的通知,都再也不須要手動移除觀察者,系統會自動在dealloc 的時候調用 [[NSNotificationCenter defaultCenter]removeObserver:self]。iOS9之前的須要手動進行移除。github
緣由是:iOS9 之前觀察者註冊時,通知中心並不會對觀察者對象作 retain 操做,而是進行了 unsafe_unretained 引用,因此在觀察者被回收的時候,若是不對通知進行手動移除,那麼指針指向被回收的內存區域就會成爲野指針,這時再發送通知,便會形成程序崩潰。web
從 iOS9 開始通知中心會對觀察者進行 weak 弱引用,這時即便不對通知進行手動移除,指針也會在觀察者被回收後自動置空,這時再發送通知,向空指針發送消息是不會有問題的。數組
1.二、使用 block 方式進行監聽的通知,仍是須要進行處理,由於使用這個 API 會致使觀察者被系統 retain。bash
請看下面這段代碼:app
[[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"11111");
}];
//發個通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];
複製代碼
第一次進來打印一次,第二次進來打印兩次,第三次打印三次。你們能夠在 demo 中進行嘗試,demo 地址見文章底部。oop
解決方法是記錄下通知的接收者,而且在 dealloc 裏面移除這個接收者就行了:post
@property(nonatomic, strong) id observer;
複製代碼
self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"11111");
}];
//發個通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];
複製代碼
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self.observer name:@"notiMemoryLeak" object:nil];
NSLog(@"hi,我 dealloc 了啊");
}
複製代碼
2.一、如今通常的使用 KVO,就算不移除觀察者,也不會有問題了性能
請看下面這段代碼:
- (void)kvoMemoryLeak {
MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:view];
[view addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
//調用這兩句主動激發kvo 具體的原理會有後期的kvo詳解中解釋
[view willChangeValueForKey:@"frame"];
[view didChangeValueForKey:@"frame"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"frame"]) {
NSLog(@"view = %@",object);
}
}
複製代碼
這種狀況不移除也不會有問題,我猜想是由於 view 在控制器銷燬的時候也銷燬了,因此 view 的 frame 不會再發生改變,不移除觀察者也沒問題,因此我作了一個猜測,要是觀察的是一個不會銷燬的對象會怎麼樣?當觀察者已經銷燬,被觀察的對象還在發生改變,會有問題嗎?
2.二、觀察一個不會銷燬的對象,不移除觀察者,會發生不肯定的崩潰。
接上面的猜想,首先建立一個單例對象 MFMemoryLeakObject,有一個屬性title:
@interface MFMemoryLeakObject : NSObject
@property (nonatomic, copy) NSString *title;
+ (MFMemoryLeakObject *)sharedInstance;
@end
#import "MFMemoryLeakObject.h"
@implementation MFMemoryLeakObject
+ (MFMemoryLeakObject *)sharedInstance {
static MFMemoryLeakObject *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
sharedInstance.title = @"1";
});
return sharedInstance;
}
@end
複製代碼
而後在 MFMemoryLeakView 對 MFMemoryLeakObject 的 title 屬性進行監聽:
#import "MFMemoryLeakView.h"
#import "MFMemoryLeakObject.h"
@implementation MFMemoryLeakView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor whiteColor];
[self viewKvoMemoryLeak];
}
return self;
}
#pragma mark - 6.KVO形成的內存泄漏
- (void)viewKvoMemoryLeak {
[[MFMemoryLeakObject sharedInstance] addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"title"]) {
NSLog(@"[MFMemoryLeakObject sharedInstance].title = %@",[MFMemoryLeakObject sharedInstance].title);
}
}
複製代碼
最後在控制器中改變 title 的值,view 銷燬前改變一次,銷燬後改變一次:
//6.一、在MFMemoryLeakView監聽一個單例對象
MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:view];
[MFMemoryLeakObject sharedInstance].title = @"2";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[view removeFromSuperview];
[MFMemoryLeakObject sharedInstance].title = @"3";
});
複製代碼
通過嘗試,第一次沒有問題,第二次就發生崩潰,報錯野指針,具體的你們能夠用 demo 作測試,demo 地址見底部。
解決方法也很簡單,在view 的 dealloc 方法裏移除觀察者就好:
- (void)dealloc {
[[MFMemoryLeakObject sharedInstance] removeObserver:self forKeyPath:@"title"];
NSLog(@"hi,我MFMemoryLeakView dealloc 了啊");
}
複製代碼
總的來講,寫代碼仍是規範一點,有觀察就要有移除,否則項目裏容易產生各類欲仙欲死的 bug。KVO 還有一個重複移除致使崩潰的問題,請參考這篇文章。
block 形成的內存泄漏通常都是循環引用,即 block 的擁有者在 block 做用域內部又引用了本身,所以致使了 block 的擁有者永遠沒法釋放內存。
本文只講解 block 形成內存泄漏的場景分析和解決方法,其餘 block 的原理會在以後 block 的單章裏進行講解。
3.一、block 做爲屬性,在內部調用了 self 或者成員變量形成了循環引用。
請看下面這段代碼,先定義一個 block 屬性:
typedef void (^BlockType)(void);
@interface MFMemoryLeakViewController ()
@property (nonatomic, copy) BlockType block;
@property (nonatomic, assign) NSInteger timerCount;
@end
複製代碼
而後進行調用:
#pragma mark - 7.block 形成的內存泄漏
- (void)blockMemoryLeak {
// 7.1 正常block循環引用
self.block = ^(){
NSLog(@"MFMemoryLeakViewController = %@",self);
NSLog(@"MFMemoryLeakViewController = %zd",_timerCount);
};
self.block();
}
複製代碼
這就形成了 block 和控制器的循環引用,解決方法也很簡單, MRC 下使用 __block、ARC 下使用 __weak 切斷閉環,實例變量使用 -> 的方式訪問就能夠解決了。
須要注意的是,僅用 __weak 所修飾的對象,若是被釋放,那麼這個對象在 block 執行的過程當中就會變成 nil,這就可能會帶來一些問題,好比數組和字典的插入。
因此建議在 block 內部對__weak所修飾的對象再進行一次強引用,這樣在 Block 執行的過程當中,這個對象就不會被置爲nil,而在Block執行完畢後,ARC 下這個對象也會被自動釋放,不會形成循環引用:
__weak typeof(self) weakSelf = self;
self.block = ^(){
//建議加一下強引用,避免weakSelf被釋放掉
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"MFMemoryLeakViewController = %@",strongSelf);
NSLog(@"MFMemoryLeakViewController = %zd",strongSelf->_timerCount);
};
self.block();
複製代碼
3.二、NSTimer 使用 block 建立的時候,要注意循環引用
請看這段代碼:
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"MFMemoryLeakViewController = %@",self);
}];
複製代碼
從 block 的角度來看,這裏是沒有循環引用的,其實在這個類方法的內部,有一個 timer 對 self 的強引用,因此也要使用 __weak 切斷閉環,另外,這種方式建立的 timer,repeats 爲 YES 的時候,也須要進行invalidate 處理,否則定時器仍是停不下來。
@property(nonatomic,strong) NSTimer *timer;
複製代碼
__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"MFMemoryLeakViewController = %@",weakSelf);
}];
複製代碼
- (void)dealloc {
[_timer invalidate];
NSLog(@"hi,我MFMemoryLeakViewController dealloc 了啊");
}
複製代碼
NSThread 和 RunLoop 結合使用的時候,要注意循環引用問題,請看下面代碼:
- (void)threadMemoryLeak {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
[thread start];
}
- (void)threadRun {
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
複製代碼
致使問題的就是 「[[NSRunLoop currentRunLoop] run];」 這一行代碼。緣由是 NSRunLoop 的 run 方法是沒法中止的,它專門用於開啓一個永不銷燬的線程,而線程建立的時候也對當前當前控制器(self)進行了強引用,因此形成了循環引用。
解決方法是建立的時候使用block方式建立:
- (void)threadMemoryLeak {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
}
複製代碼
這樣控制器是能夠獲得釋放了,但其實這個線程仍是沒有銷燬,就算調用 「CFRunLoopStop(CFRunLoopGetCurrent());」 也沒法中止這個線程,由於這個只能中止這一次的 RunLoop,下次循環依然能夠繼續進行下去。具體的解決方法我會在 RunLoop 的單章裏進行講解。
目前 iOS 的 webView 有UIWebView 和 WKWebView 兩種。
5.一、UIWebView
UIWebView 內存問題應該是衆所周知了吧,Apple官方也認可了內存泄露確實存在,因此在 iOS8 推出了功能和性能都更增強大WKWebView。
你們能夠看看下面這段代碼:
UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.backgroundColor = [UIColor whiteColor];
NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
[webView loadRequest:requset];
[self.view addSubview:webView];
複製代碼
就這麼一段簡單的代碼,打開網頁的時候,內存暴漲 200M,就算返回到上級頁面,webView 銷燬,內存也依然比原來高了 50M 左右,就算在控制器的 dealloc 里加載一個空的 url 也沒有做用,這個你們能夠用demo進行嘗試。
5.二、WKWebView
總的來講,WKWebView 不論是性能仍是功能,都要比 UIWebView 強大不少,自己也不存在內存泄漏問題,可是,若是開發者使用不當,仍是會形成內存泄漏。請看下面這段代碼:
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
[config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"];
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
wkWebView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:wkWebView];
NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
[wkWebView loadRequest:requset];
複製代碼
這樣看起來沒有問題,可是其實 「addScriptMessageHandler」 這個操做,致使了 wkWebView 對 self 進行了強引用,而後 「addSubview」這個操做,也讓 self 對 wkWebView 進行了強引用,這就形成了循環引用。
解決方法就是在合適的機會裏對 「MessageHandler」 進行移除操做,好比:
@property (nonatomic, strong) WKWebView *wkWebView;
複製代碼
- (void)webviewMemoryLeak {
// 9.2 WKWebView
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
[config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"];
_wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
_wkWebView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:_wkWebView];
NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
[_wkWebView loadRequest:requset];
}
複製代碼
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"WKWebViewHandler"];
}
複製代碼
在 demo 裏我選擇了在 「 viewDidDisappear」方法裏進行移除操做,這樣控制器就能夠獲得釋放了。
本次的內存泄漏分析,就寫到這裏,由於本人水平所限,不少地方仍是沒能講得足夠深刻,歡迎諸位進行指正。