接上篇, 本篇主要讲解通知和 KVO 不移除观察者, block 循环引用 ,NSThread 和 RunLoop 一起使用造成的内存泄漏.
1, 通知造成的内存泄漏
1.1,ios9 以后, 一般的通知, 都不再需要手动移除观察者, 系统会自动在 dealloc 的时候调用 [[NSNotificationCenter defaultCenter]removeObserver:self].ios9 以前的需要手动进行移除.
原因是: ios9 以前观察者注册时, 通知中心并不会对观察者对象做 retain 操作, 而是进行了 unsafe_unretained 引用, 所以在观察者被回收的时候, 如果不对通知进行手动移除, 那么指针指向被回收的内存区域就会成为野指针, 这时再发送通知, 便会造成程序崩溃.
从 ios9 开始通知中心会对观察者进行 weak 弱引用, 这时即使不对通知进行手动移除, 指针也会在观察者被回收后自动置空, 这时再发送通知, 向空指针发送消息是不会有问题的.
1.2, 使用 block 方式进行监听的通知, 还是需要进行处理, 因为使用这个 API 会导致观察者被系统 retain.
请看下面这段代码:
- [[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
- NSLog(@"11111");
- }];
- // 发个通知
- [[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];
第一次进来打印一次, 第二次进来打印两次, 第三次打印三次. 大家可以在 demo 中进行尝试, demo 地址见文章底部.
解决方法是记录下通知的接收者, 并且在 dealloc 里面移除这个接收者就好了:
- @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 造成的内存泄漏
2.1, 现在一般的使用 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.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.1, 在 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.
3,block 造成的内存泄漏
block 造成的内存泄漏一般都是循环引用, 即 block 的拥有者在 block 作用域内部又引用了自己, 因此导致了 block 的拥有者永远无法释放内存.
本文只讲解 block 造成内存泄漏的场景分析和解决方法, 其他 block 的原理会在之后 block 的单章里进行讲解.
3.1,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.2,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 了啊");
- }
4,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
来源: https://www.cnblogs.com/zmfflying/p/11110752.html