iOS開發系列以內存泄漏分析(下)

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 了啊");
}
複製代碼

二、KVO 形成的內存泄漏

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 的原理會在以後 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 形成的內存泄漏

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 的單章裏進行講解。

五、webView 形成的內存泄漏

目前 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」方法裏進行移除操做,這樣控制器就能夠獲得釋放了。

本次的內存泄漏分析,就寫到這裏,由於本人水平所限,不少地方仍是沒能講得足夠深刻,歡迎諸位進行指正。

demo地址

相關文章
相關標籤/搜索