循环引用

1.概念

循环引用可以简单理解为 A 强引用 B,B 又强引用了 A,双方同时保持对方的一个引用,导致引用计数不为0,始终无法释放。

影响?

导致对象无法释放,造成内存泄露。如果是 ViewController 内出现循环引用,在反复 push & pop 后,ViewController 释放不掉,导致内存激增,甚至引发crash。

2.场景

2.1.block

下面是一段关于 block 的描述:

When a block is copied, it creates strong references to object variables used within the block. If you use a block within the implementation of a method:

If you access an instance variable by reference, a strong reference is made to self;

If you access an instance variable by value, a strong reference is made to the variable.

使用copy修饰的 block 会被放到堆中。block 会强引用 block 块中捕获到的对象。

  • 如果在 block 中访问了属性,那么 block 就会强引用 self。
  • 如果在 block 中访问了一个局部变量,那么 block 就会强引用该变量。

block 的循环引用一般表现为,block 作为属性被类的实例(self)强引用,然后实例在block中又引用了实例本身。self -> block -> self。编写代码时,这种循环引用编译器能捕捉到并给出提醒:“Capturing ‘self’ strongly in this block is likely to lead to a retain cycle”。

注意:并不是所有block都会造成循环引用。只有被强引用了的 block 才会产生循环引用,一些系统封装的block就不存在循环引用问题,如:

1
2
3
4
5
dispatch_async(dispatch_get_main_queue(), ^{
});

[UIView animateWithDuration:1 animations:^{
}];
#循环引用示例1

自定义的一个Student类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//.h文件
typedef void(^StudentBlock)();

@interface Student : NSObject

@property (copy , nonatomic) NSString *name;
@property (copy , nonatomic) StudentBlock block;

@end

//.m文件
@implementation Student

- (void)dealloc{
NSLog(@"++%@ dealloced~",[self class]);
}
@end

在某个VC中调用:

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
student.name = @"D";
student.block = ^{
NSLog(@"Hello %@ ~",student.name);
};
student.block();
}

此示例中,student中有个block属性,block内强引用了student对象本身,这就造成循环引用,导致student无法释放。

  • 解决办法

在对象的 block 内访问__block 修饰的变量代替原对象,并在调用完之后将__block变量或 block 本身置为nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
student.name = @"D";
//创建__block修饰的变量
__block Student *st = student;
student.block = ^{
NSLog(@"Hello %@ ~",st.name);//访问__block变量
//case1:置为nil
st = nil;
};
student.block();
//case2:student.block = nil;
}

这种办法的缺点很明显:如果不调用此 block 或没将其置为 nil,那么st就不被置为 nil,依然强引用student,循环引用依然存在。

#循环引用示例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "AppDelegate.h"

@interface AppDelegate()
@property (nonatomic, copy) void (^aBlock)(void);
@property (nonatomic, copy) NSString *aName;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//有循环引用问题
self.aBlock = ^{
self.aName = @"THIS IS A NAME";//有警告
};
//无循环引用问题
dispatch_async(dispatch_get_main_queue(), ^{
self.aName = @"THIS IS A NAME 2";
});
return YES;
}
@end
  • 解决办法1:

通过__weak修饰符,先弱引用self,再通过__strong强引用weakSelf,然后在 block 里使用strongSelf即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"+++self.rfc:%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
__weak typeof(self) weakSelf = self;
NSLog(@"+++self.rfc:%ld,weakSelf.rfc:%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)),CFGetRetainCount((__bridge CFTypeRef)(weakSelf)));
self.aBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"+++weakself.rfc:%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(weakSelf)));
strongSelf.aName = @"THIS IS A NAME";
};
self.aBlock();

return YES;
}

输出日志:

1
2
3
4
+++self.rfc:1
+++self.rfc:1,weakSelf.rfc:2
+++执行block
+++weakself.rfc:3

原理:__weak修饰的weakSelf是一个弱引用,它指向self但不会增加self的引用计数,从而打破selfblock之间的循环引用。

ps: 这里之所以再强引用一下 weakSelf,是因为__weak修饰的对象都是弱引用,随时可能会被系统释放,造成后面调用 weakSelf 时 weakSelf 可能已经是nil了。__strong修饰的strongSelf是一个强引用,保证了weakSelf在 block 声明周期内不会被销毁,而且strongSelf是一个自动变量,在 block 执行完毕后会自动释放。

  • 解决办法2:

将在 block 内要使用到的对象(一般为self对象),以 block 参数的形式传入,block 就不会捕获该对象,而是将其作为参数使用,其生命周期由系统的栈自动管理,也不会造成内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface AppDelegate()
@property (nonatomic, copy) void (^aBlock)(AppDelegate *delSelf);//将本类作为参数
@property (nonatomic, copy) NSString *aName;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.aBlock = ^(AppDelegate *delSelf){
delSelf.aName = @"THIS IS A NAME";
};
return YES;
}
@end

2.2.NSTimer

一般是 NSTimer 被作为类的成员变量,在期望类销毁时,NSTimer 尚处于 validate 状态。此时不管 NSTimer 对象是否被置为nil,类都无法正常销毁。

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
@interface ViewController ()
@property (nonatomic, strong) NSTimer *aTimer;
@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
_aTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(action) userInfo:nil repeats:YES];
}

- (void)dealloc{
NSLog(@"dealloced!!!");
}

- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
_aTimer = nil;
}

- (void)action{
NSLog(@"a Timer action!");
}
@end

上面的示例中, ViewController 出现后过段时间退出,dealloc 不会执行。这是因为 NSTimer 创建后被加入到 NSRunloop 中,失效之前会一直持有 self 作为 target。需要在合适的时机执行 invalidate 来打破这个循环引用即可。如下:

1
2
3
4
5
6
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[_aTimer invalidate];//敲黑板 划重点!!
_aTimer = nil;
}

2.3.delegate

一般是 A 类中声明了一个 strong 的 delegate,B 类强引用 A 类,同时把 A 类的 delegate 指向 B 自己。B -> A -> delegate -> B。解决办法:声明 delegate 时,使用 weak 修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//ClassA
#import <UIKit/UIKit.h>
@protocol CustomDelegate <NSObject>
- (void)onDelegateCallback;
@end

@interface ClassA : NSObject
@property (nonatomic, weak) id <CustomDelegate> delegate;
@end

//ClassB
@interface ClassB ()
@property (nonatomic, strong) ClassA *classA;
@end
@implementation ClassB
- (void)viewDidLoad
{
[super viewDidLoad];
self.classA = [[ClassA alloc] init];
self.classA.delegate = self;
}

循环引用
https://davidlii.cn/2018/03/05/retaincircle.html
作者
Davidli
发布于
2018年3月5日
许可协议