iOS ARC下几种导致内存泄露的场景

去年的这个时候临危受命,在没有任何准备的情况下(旁白:准确来讲当时唯一的准备是买了台MacBook pro,本来是用来开发Ruby的~)开始一个人开发公司的某个APP(iOS版),因为知识储备太少,开发的过程中给自己挖了很多的坑,然后从坑中爬出来填坑,接着再挖一个新坑把自己推下去(请叫我挖坑填坑小王子~)…下面我要说说几个导致内存泄露的坑,当然我也会很负责任的告诉你,下面的几个坑我基本都入过,不得不说因为内存没有被及时回收而导致的Bug,会很难定位…不过现在我已经从这些坑里爬出来了~~

1.两个对象互相强引用

好吧,这是最明显的循环引用导致内存泄露的场景了。
演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//AViewController 对象
@interface AViewController : UIViewController
//aVC强引用了bVC
@property (nonatomic, strong) BViewController *bVC;
@end

/**************************************************/

//BViewController 对象
@interface BViewController : UIViewController
//bVC强引用了aVC
@property (nonatomic, strong) aViewController *aVC;
@end

如上的代码,A强引用了B,B也强引用了A,这样就导致了循环引用,ARC无法回收这两个对象,从而导致内存泄露。

解决

那我们该怎么解决呢?我觉得两个对象最好不要互相引用,如果不得不互相引用,我们可以这么写,

1
2
3
4
5
6
7
8
9
10
11
12
@interface AViewController : UIViewController
//aVC强引用了bVC
@property (nonatomic, strong) BViewController *bVC;
@end

/**************************************************/

//BViewController 对象
@interface BViewController : UIViewController
//bVC弱引用了aVC
@property (nonatomic, weak) aViewController *aVC;
@end

如上A对B强引用,那么B对A就弱引用。

2.Block 块

第二种场景是使用Block的时候,在开发的过程中我发现这种情况导致内存泄露的频率会更高。
演示代码:

1
2
3
4
5
6
7
8
9
10
11
@implementation AVC
- (void)request
{
[[HttpEngine sharedEngine] urlPath:url method:POST_TYPE params:params completionHandler:^(id json) {
[self stopRefreshAnimating];
[self.tableView reloadData];
} errorHandler:^(id json) {
[self stopRefreshAnimating];
}];
}
@end

如上的代码,是我在项目中截取的一段网络请求的代码,其中两个block分别是请求成功和失败的block。在这里AVC这个对象对两个block进行了引用,同时在block中,block又对self(AVC这个对象)进行了引用。这样就形成了循环引用导致了内存泄露。

解决

因为block而引起的循环引用,可以通过weak self解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation AVC
- (void)request
{
//weak化self
__weak typeof(self) weakSelf = self;
[[HttpEngine sharedEngine] urlPath:url method:POST_TYPE params:params completionHandler:^(id json) {
//避免weakSelf被回收 将weakSelf strong化
__strong typeof(weakSelf) strongSelf = weakSelf;

[strongSelf stopRefreshAnimating];
[strongSelf.tableView reloadData];
} errorHandler:^(id json) {
[weakSelf stopRefreshAnimating];
}];
}
@end

如上的方式就解决了block与self循环引用的问题,你可能会问为什么第一个block内要strong weakSelf,而第二个block直接使用了weakSelf。因为在第一个block中我们会两次或多次使用到self,如果没有strong,那么可能你调用第一次weakSelf后就被回收了,这样就会出现很多问题。而当你只使用一次的时候可以直接使用weakSelf。然后你可能又会问strong了weakSelf,会不会又导致循环引用,答案是看时机,当在执行block的内容时,确实引用了self,但是当执行完block后,会马上解除引用,就不会导致循环引用了。

3.NSTimer

NSTimer确实是挖坑的重灾区,除了下面想说的内存泄露的问题,还有其它好多坑,如runLoop等。下面我放一段测试代码,看看会不会引起内存泄露,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@interface TwoViewController()

@property (nonatomic, assign) int number;
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation TwoViewController

- (void)viewDidLoad
{
[super viewDidLoad];

self.number = 0;
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(dodd) userInfo:nil repeats:YES];
}

- (void)dodd
{
self.number = self.number + 1;
NSLog(@"======%i", self.number);
}

- (void)dealloc
{
NSLog(@"=====dealloc");
[self.timer invalidate];
self.timer = nil;
}

@end
解决

也许当你写完代码后,很自信快乐的想,我在dealloc中将NSTimer暂停清空,应该不会出现内存泄露了吧。但现实总是那么的悲惨~上面的代码确实引起了内存的泄露。self对timer进行了引用,然后在初始化启动timer的时候timer也引用了self。这样的话- (void)dealloc永远都不会被执行的,所以timer也就无法被清空了。也许你会说那我将代码改成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)dodd
{
if(self.number == 60){
[self.timer invalidate];
self.timer = nil;

return;
}
self.number = self.number + 1;
NSLog(@"======%i", self.number);
}

- (void)dealloc
{
NSLog(@"=====dealloc");
}

这样改确实能解决内存泄露的问题,但是又出现了另一个问题:延长了对象的生命周期。 如上的代码60s后确实能将内存回收,但是假设在10s的时候用户点击了back按钮,理应对象被回收,但是实际上内存没有被回收,而是等到60s后才被回收。所以我一般的做法是除了特定条件下清空计时器外,我还会在viewDidDisappearviewWillDisappear中清空计时器。代码如下:

1
2
3
4
5
6
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}

Update(2015.8.4):还有一种不错的解决方法,你可以参考该文章http://blog.callmewhy.com/2015/07/06/weak-timer-in-ios/

其它(不一定是内存泄露问题)

延时触发方法

不知道大家平时在项目中有没有经常遇到这种情况,延时触发指定的方法,如果你使用- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay就要小心了,它也会和NSTimer一样会引起对象的生命周期延长。假设你延时10s触发某个方法,但是在3s的时候,用户点击了back,这时对象不能马上被回收,而是要等到10s后才有可能被回收。所以在项目中如果要延时触发方法,我不会选择该方法,而是使用GCD,代码如下:

1
2
3
4
5
6
7
__weak typeof(self) weakSelf = self;

double delayInSeconds = 10.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds *NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^{
[weakSelf dodd];
});

代理未清空引起野指针

查看iOS的一些API,发现delegate都是assign的,这样就会引起野指针的问题,可能会引起一些莫名其妙的crash。那么这是怎么引起的,当一个对象被回收时,对应的delegate实体也就被回收,但是delegate的指针确没有被nil,从而就变成了游荡的野指针了。所以在delloc方法中要将对应的assign代理设置为nil,如:

1
2
3
4
5
- (void)dealloc
{
self.myTableView.delegate = nil;
self.myTableView.dataSource = nil;
}

那是不是所有的delegate都要这样做呢?一般自己写的一些delegate,我们会用weak,而不是assign,weak的好处是当对应的对象被回收时,指针也会自动被设置为nil。

消息

不想多说了,记的在dealloc中将注册的消息,kvo remove掉~~