接上篇,本篇主要講解通知和 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