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

接上篇,本篇主要講解通知和 KVO 不移除觀察者、block 循環引用 、NSThread 和 RunLoop一塊兒使用形成的內存泄漏。html

一、通知形成的內存泄漏ios

1.一、ios9 之後,通常的通知,都再也不須要手動移除觀察者,系統會自動在dealloc 的時候調用 [[NSNotificationCenter defaultCenter]removeObserver:self]。ios9之前的須要手動進行移除。git

緣由是:ios9 之前觀察者註冊時,通知中心並不會對觀察者對象作 retain 操做,而是進行了 unsafe_unretained 引用,因此在觀察者被回收的時候,若是不對通知進行手動移除,那麼指針指向被回收的內存區域就會成爲野指針,這時再發送通知,便會形成程序崩潰。github

從 ios9 開始通知中心會對觀察者進行 weak 弱引用,這時即便不對通知進行手動移除,指針也會在觀察者被回收後自動置空,這時再發送通知,向空指針發送消息是不會有問題的。數組

1.二、使用 block 方式進行監聽的通知,仍是須要進行處理,由於使用這個 API 會致使觀察者被系統 retain。oop

請看下面這段代碼:post

[[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
  NSLog(@"11111");
}];
//發個通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];


第一次進來打印一次,第二次進來打印兩次,第三次打印三次。你們能夠在 demo 中進行嘗試,demo 地址見文章底部。測試

解決方法是記錄下通知的接收者,而且在 dealloc 裏面移除這個接收者就行了:atom

@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 形成的內存泄漏spa

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 還有一個重複移除致使崩潰的問題,請參考這篇文章: https://www.cnblogs.com/wengzilin/p/4346775.html。

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

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

demo地址: https://github.com/zmfflying/ZMFBlogProject.git

相關文章
相關標籤/搜索