接上篇,本篇主要讲解通知和 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 的单章里进行讲解。

5、webView 造成的内存泄漏

目前 iOS 的 webView 有UIWebView 和 WKWebView 两种。

5.1、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.2、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地址: https://github.com/zmfflying/ZMFBlogProject.git

ios开发系列之内存泄漏分析(下)的更多相关文章

  1. ios开发系列之内存泄漏分析(上)

    ios自从引入ARC机制后,一般的内存管理就可以不用我们码农来负责了,但是一些操作如果不注意,还是会引起内存泄漏. 本文主要介绍一下内存泄漏的原理.常规的检测方法以及出现的常用场景和修改方法. 1.  ...

  2. iOS开发 如何检查内存泄漏

    本文转载至 http://mobile.51cto.com/iphone-423391.htm 在开发的时候内存泄漏是不可避免的,但是也是我们需要尽量减少的,因为内存泄漏可能会很大程度的影响程序的稳定 ...

  3. iOS开发系列-打印内存地址

    打印内存地址 基本数据类型 定义一个基本数据类型,会根据变量类型分配对应的内存空间.比如定义一个int类型的变量a. int a = 10; 内存如下 输入变量a在内存中内存地址 NSLog(@&qu ...

  4. iOS开发系列之app的一天

    本文主要讲述我对 iOS 开发的一些理解,希望能通过 app 从启动到退出,将一些的知识整合起来,形成一条知识链,目前涉及到的知识点有 runloop.runtime.文件存储.界面布局.离线推送.内 ...

  5. Java内存泄漏分析系列之五:常见的Thread Dump日志案例分析

    原文地址:http://www.javatang.com 症状及解决方案 下面列出几种常见的症状即对应的解决方案: CPU占用率很高,响应很慢 按照<Java内存泄漏分析系列之一:使用jstac ...

  6. Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析

    原文地址:http://www.javatang.com 一个典型的thread dump文件主要由一下几个部分组成: 上图将JVM上的线程堆栈信息和线程信息做了详细的拆解. 第一部分:Full th ...

  7. iOS开发系列--数据存取

    概览 在iOS开发中数据存储的方式可以归纳为两类:一类是存储为文件,另一类是存储到数据库.例如前面IOS开发系列-Objective-C之Foundation框架的文章中提到归档.plist文件存储, ...

  8. iOS开发系列--网络开发

    概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博.微信等,这些应用本身可能采用iOS开发,但是所有的数据支撑都是基于后台网络服务器的.如今,网络编程越来越普遍,孤立的应用通常是没有生命力 ...

  9. iOS开发系列--并行开发其实很容易

    --多线程开发 概览 大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的, ...

随机推荐

  1. Android GPS获取当前位置信息

    package com.example.gpstest; import org.apache.http.util.LangUtils; import android.content.Context; ...

  2. QComboBox实现复选功能(三种方法:嵌套QListWidget, 设置QStandardItemModel, 设置Delegate)

    今天介绍一下一个小东西 — 如何让QComboBox实现复选功能?   需求: 下拉列表有复选功能 不可编辑 显示所有选中项   关于QComboBox的复选功能有几种方案: QStandardIte ...

  3. 从hadoop 要删除字符串匹配指定的任务

    我们都知道,假设 hadoop job -list 获取当前正在执行的hadoop 任务,返回的结果例如以下: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQ ...

  4. DELPHI下多线程编程的几个思维误区(QDAC)

    有几个网友私下问我一些有关线程的事情.过节写个东西上来大家交流. 思维误区1,自己新建的THREAD是线程,自己的主程序不是线程. 很多人在多线程编程没有把主线程也当作线程.其实主线程也是线程.看起来 ...

  5. JAVASCRIPT高程笔记-------第十章 DOM对象

    10.1.1 node类型 --除IE外 所有浏览器都可以访问到这个类型 :JS中所有的节点类型都继承自Node类型 nodeName 与nodeValue  如果是一个元素 那么nodeName中保 ...

  6. WPF ObjectDataProvider的使用-只能检索用

    <Window x:Class="CollectionBinding.MainWindow"        xmlns="http://schemas.micros ...

  7. VS2012中使用CEGUI项目发布到XP平台的问题(核心方法就一句话。“你项目使用的所有外部依赖库都用/MT编译。”)

    接着上一篇文章,详细说说如何把一个带CEGUI的项目发布到XP平台. 这个问题纠缠了我好几天.这里把详细解决思路记下来.有同样问题的朋友可以少走很多弯路. 核心方法就一句话.“你项目使用的所有外部依赖 ...

  8. Lambda表达式的参数捕获

    以常用的Action委托为例: 有如下3个无参数的方法: public void Function() { //Do something } public void Function2() { //D ...

  9. Win8 Metro(C#)数字图像处理--2.59 P分位法图像二值化

    原文:Win8 Metro(C#)数字图像处理--2.59 P分位法图像二值化  [函数名称]   P分位法图像二值化 [算法说明]   所谓P分位法图像分割,就是在知道图像中目标所占的比率Rat ...

  10. no identifier specified for entity错误

    未给entity类添加主键造成. 之前出现这个错误是因为忘记给id添加@Id标签.