循环引用
# 1.什么是循环引用问题?
上篇文章说到循环引用的问题,其实引用计数这种管理内存 (opens new window)的方式虽然简单,但是有一个瑕疵,它不能很好的解决循环引用的问题。如图展示:
对象A和对象B,互相引用了对方作为自己的成员变量,只有当自己销毁的时候,才会将成员变量的引用计数减1。因为对象A的摧毁依赖于对象B的销毁,而对象B的销毁依赖与对象A的销毁,这样就造成了循环引用问题。即使在外界已经没有任何指针能访问它们了,它们这种互相依赖关系也无法被释放。
不止两个对象可以产生循环引用问题,多个对象间依次持有,形成一个环路造成循环引用,这是最恶心的,因为在实际开发中,环大起来简直要命了,很难找出来。
# 2.解决循环引用
解决办法有两个,第一个办法就是我明确知道这里会存在循环引用,在合理的位置主动的断开环中的一个引用,使得对象得以回收。但是这种方法并不是很好用,依赖于程序员对具体业务逻辑相当的熟悉。现在,更常见的是第二种方法:使用弱引用(weak reference)的办法。
弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在iOS开发中,弱引用通常在delegate模式中使用。这个之前的文章有说过的。传送门:iOS - Delegate代理为什么要用weak修饰(面试官钟爱)_ios delegate为什么weak-CSDN博客 (opens new window)
# 二、循环引用类型(哪些场景会有循环引用问题)
# 自循环引用
假如有一个对象,内部强持有它的成员变量 obj,若此时我们 给 o bj 赋值为原对象时,就是自循环引用。
# 相互循环引用
对象 A 内部强 持有 obj , 对象 B 内部强持有 obj , 若 此时对 象 A 的 obj 指 向对 象 B ,同时对 象 B 中的 obj 指向对象 A,就是相互引用。
# 多循环引用
假如类中有对象 1...对象 N,每个对象中都强持有一 个 obj,若每个对 象的 obj 都指向下个对象,就产生了多循环引用。
**总之,识别循环引用的核心就是:**两个或多个对象之间是否存在保留环。
# 三、常见循环用及原因
# 代理
VC页面强持有UITableView,UITableView强持有Cell,在设置Cell的delegate为VC时,如果使用strong,这样就会形成循环引用。
VC->UITableView->Cell->VC
这种循环引用,比较简单,把Cell的delegate属性使用weak修饰,进行弱引用VC即可。
@interface ViewController ()
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, copy) void (^myBlock)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^{
NSLog(@"%@", self.array); // Capturing 'self' strongly in this block is likely to lead to a retain cycle
NSLog(@"%@", _array); // Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.因为self强引用了myblock,此时myblock在堆上,myblock捕获self并在其中强制持有,会导致保留环。
出现以下情况时,ARC 会自动对 block 执行一次 copy 操作,将其从栈区移动到堆区:
当 block 作为函数返回值时。
当 block 被强指针引用时。
当 Cocoa API 方法名包含usingBlock,且 block 作为参数时,或 block 作为 GCD API 方法参数
2.myblock中使用了self的实例变量 _array ,因此myblock会隐式的retain住self。(因为访问成员变量本质上是在调用self->instance,即仍然访问了self。)
解决方案:
在block外部使用__weak生成一个对self的弱引用weakSelf,block捕获weakSelf时,会连同他的修饰符进行捕获,在block对象内部产生一个指向真实内存区的__weak引用。从而解决了block对真实内存区的强引用问题
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
NSLog(@"%@", weakSelf.array);
NSLog(@"%@", weakSelf->_array);
};
2
3
4
5
那self和weakSelf一样么?
NSLog(@"self === %p", &self);
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
NSLog(@"weakSelf === %p", &weakSelf);
NSLog(@"%@", weakSelf.array);
};
self.myBlock();
// 打印
self === 0x16aff39b8
weakSelf === 0x600003bac050
2
3
4
5
6
7
8
9
10
11
进入block外和block里面的weakSelf是不一样的。
我们使用:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 ViewController.m 命令,将ViewController.m转成C++代码
// @implementation ViewController
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
ViewController *const __weak weakSelf; // 对捕获的weakSelf进行弱引用。
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
通过源码可以看出,block内部对捕获的weakSelf进行了弱引用,从而打破了block强引用self的这一层关系。
为什么我们在项目中,block体里有时候又会使用strongSelf呢?
__strong typeof(weakSelf) strongSelf = weakSelf;
假设有这样的场景:block里面有延迟操作self的持有的对象时,如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.car = [[Car alloc] init];
self.car.name = @"xiaomi su7";
__weak typeof(self.car) weakCar = self.car;
self.car.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"car.name2 === %@", weakCar.name);
});
NSLog(@"car.name1 === %@", weakCar.name);
};
self.car.block();
self.car = nil;
}
// 打印结果
car.name1 === xiaomi su7
car.name2 === (null)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以看出self强持有car对象,在执行blcok之后,就把car置为nil。block里又有延迟函数,来读取self持有的car信息。
通过打印结果来看:这通常不是我们想要的结果,故,我们需要在block体内进行一个“强引用”来延迟car的释放
- (void)viewDidLoad {
[super viewDidLoad];
self.car = [[Car alloc] init];
self.car.name = @"xiaomi su7";
__weak typeof(self.car) weakCar = self.car;
self.car.block = ^{
__strong typeof(weakCar) strongCar = weakCar;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"car.name2 === %@", strongCar.name);
});
NSLog(@"car.name1 === %@", weakCar.name);
};
self.car.block();
self.car = nil;
}
// 打印结果
car.name1 === xiaomi su7
car.name2 === xiaomi su7
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
原理:
这就是我们要引入strongSelf的原因:用来稍微延长真实内存区的生存期,确保block体内操作对象不被释放。
strongSelf是个局部变量,它的生存期与方法体同在,也就是说你在方法体开始时,保证了strongSelf不为空,那在方法体结束时,它依旧能提供这样的保证。当方法体结束时,strongSelf局部变量的生存期到期,strongSelf被释放,strongSelf对真实内存区的引用也被释放。
使用案例:
例如:在SDWebimage的下载回调中,就是用了strongSelf来延迟block体操作对象的释放
早期的AFN请求完成完成回调
- (void)setCompletionBlock:(void (^)(void))block {
[self.lock lock];
if (!block) {
[super setCompletionBlock:nil];
} else {
__weak __typeof(self)weakSelf = self;
[super setCompletionBlock:^ {
__strong __typeof(weakSelf)strongSelf = weakSelf;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop
dispatch_group_async(group, queue, ^{
block();
});
dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
[strongSelf setCompletionBlock:nil];
});
}];
}
[self.lock unlock];
}
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
# NSTimer
@property (nonatomic, strong) NSTimer *timer;
// Creates a timer and schedules it on the current run loop in the default mode.
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(doSomething) userInfo:nil repeats:YES];
2
3
4
苹果官方文档对NSTimer的target参数解释:
target
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
当timer fire的时候会对`target`进行强持有,直到Timer是 invalidated
2
3
4
如上,我们在创建NSTimer实例时,就会在默认的runloop中持有了一个timer,time在fire时,就会持有它的target(即self)
如果把对timer的引用改成弱引用可以不?
答案:不行,因为当NSTimer被分配之后,会被当前线程的Runloop进行强引用,如果对象以及NSTimer是在主线程创建的,就会被主线程的Runloop持有这个NSTimer,所以即使我们通过弱引用来指向NSTimer,但是由于主线程中Runloop常驻内存通过对NSTimer的强引用,再通过NSTimer对对象的强引用,仍然对这个对象产生了强引用,此时即使VC页面退出,去掉VC对对象的引用,当前VC仍然有被Runloop的间接强引用持有,这个对象也不会被释放,此时就产生了内存泄漏。
左侧是Runloop对NSTimer的强引用,在NSTimer和VC对象中间添加一个中间对象,然后由NSTimer对中间对象进行强引用,同时中间对象分别对NSTimer和VC对象进行弱引用,当当前VC退出之后,VC就释放了,变为nil,当下次定时器的回调事件回来的时候,可以在中间对象当中,判断当前中间对象所持有的弱引用VC对象是否被释放了,实际上就是判断中间对象当中所持有的weak变量是否为nil,如果是nil,然后调用[NSTimer invalid]以及将NSTimer置为nil,就打破了Runloop对NSTimer的强引用以及NSTimer对中间对象的强引用。
这个解决方案是利用了:当一个对象被释放后,它的weak指针会自动置为nil
实现参考:内存管理-循环引用 - 简书 (opens new window)
解决方案2:
自定义一个作为消息传递中间者的Proxy(继承自NSProxy的类),并让这个Proxy弱持有真正的target,再把这个Proxy设为timer的target。Proxy会把消息转发给真正的target,而又因为是弱持有的,所以不出现循环引用(保留环)的问题。
@interface YYTextWeakProxy : NSProxy
/// The proxy target.
@property (nullable, nonatomic, weak, readonly) id target;
/// Creates a new weak proxy for target.
- (instancetype)initWithTarget:(id)target;
/// Creates a new weak proxy for target.
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation YYTextWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYTextWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
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
37
38
39
40
这是 Apple 官方文档给 NSProxy 的定义,NSProxy 和 NSObject 一样都是根类,它是一个抽象类,你可以通过继承它,并重写 -forwardInvocation: 和 -methodSignatureForSelector: 方法以实现消息转发到另一个实例。综上,NSProxy 的目的就是负责将消息转发到真正的 target 的代理类。
动态方法解析
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
- (BOOL)resolveInstanceMethod:(SEL)selector
使用这种方法的前提是:相关方法的实现代码已经写好了,只等着运行的时候动态插入在类里面就可以了。
备援接受者
当前接受者还有第二次机会能处理未知的选择子,在这一步中,运行期的系统会问它,能不能把这条消息转给其他接受者来处理。
- (id)forwardingTargetForSelector:(SEL)selector
在一个对象内部,可能还有一些列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回。如果没有则返回nil。
完整的消息转发机制
如果forwardingTargetForSelector:方法没有处理,会来到methodSignatureForSelector:方法,该方法可以返回一个方法签名,返回后,程序会继续调用forwardInvocation:方法。如果forwardInvocation:方法也没处理,程序就抛出异常
(NSMethodSignature *)methodSignatureForSelector:(SEL)selector
(void)forwardInvocation:(NSInvocation *)invocation
问题:
当不能识别方法的时候,就会调用forwardingTargetForSelector这个方法,在这个方法中,我们可以将不能识别的传递给其它对象处理,由于这里对所有的不能处理方法的都传递给_target了,所以methodSignatureForSelector和forwardInvocation不可能被执行的,所以不用再重载了吧?
其实还是需要重载methodSignatureForSelector和forwardInvocation的,为什么呢?因为_target是弱引用的,所以当_target可能释放了,当它被释放了的情况下,那么forwardingTargetForSelector就是返回nil了。然后methodSignatureForSelector和forwardInvocation没实现的话,就直接crash了!!! 这也是为什么这两个方法中随便写的!!!
参考链接:
NSTimer和实现弱引用的timer的方式_vc弱引用nstimer-CSDN博客 (opens new window)
[iOS进阶]iOS消息机制-CSDN博客 (opens new window)
使用案例:
self.tipViewTimer = [NSTimer scheduledTimerWithTimeInterval:time
target:[YYTextWeakProxy proxyWithTarget:self]
selector:@selector(tipViewTimerAction)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.tipViewTimer forMode:NSRunLoopCommonModes];
2
3
4
5
6
# 四、不会产生循环引用的场景
# AFN
我们使用[AFHTTPSessionManager manager]发起网络请求,在函数中,AFHTTPSessionManager * manager是一个局部变量,随着函数栈的调用结束,这个局部变量也就被回收了,故self并没有持
// AFN GET请求
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:url parameters:params headers:header progress:^(NSProgress * _Nonnull downloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
+ (instancetype)manager {
return [[[self class] alloc] initWithBaseURL:nil];
}
2
3
4
5
6
7
8
9
10
11
12
13
在GET的调用过程中,对success,failure并没有做什么操作。
// GET请求源码1
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress * _Nonnull))downloadProgress
success:(nullable void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
URLString:URLString
parameters:parameters
headers:headers
uploadProgress:nil
downloadProgress:downloadProgress
success:success
failure:failure];
[dataTask resume];
return dataTask;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
来到dataTaskWithHTTPMethod里,发现31-40行,发现dataTask也没有持有success,failure,只是当成一个普通的block,然后调用了这个block而已。
// 源码2
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
for (NSString *headerField in headers.keyEnumerator) {
[request setValue:headers[headerField] forHTTPHeaderField:headerField];
}
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request
uploadProgress:uploadProgress
downloadProgress:downloadProgress
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
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
37
38
39
40
41
42
43
# Masonry
由源码可以看出:Masonry中设置布局的方法中的block对象并没有被View所引用,而是直接在方法内部同步执行,执行完以后block将释放,其中捕捉的外部变量的引用计数也将还原到之前。
/// 调用
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(0);
make.top.mas_equalTo(TBCPadding(M_H_X002));
make.left.mas_equalTo(TBCPadding(M_W_X004));
make.right.mas_equalTo(-TBCPadding(M_W_X004));
}];
/// Masonry源码
#import "View+MASAdditions.h"
#import <objc/runtime.h>
@implementation MAS_VIEW (MASAdditions)
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.updateExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.removeExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
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
# 系统UIView类动画
UIView动画block不会造成循环引用是因为这是类方法,不可能强引用一个类,所以不会造成循环引用。
UIView中的block持有当前控制器,但是当前控制器中是没有持有UIView类的,没有形成循环。当动画结束时,UIView会结束持有这个block,如果没有别的对象持有block的话,block对象就会被释放掉,从而block会释放掉对self的持有,整个内存引用关系被解除。
[UIView animateWithDuration:0.25 animations:^{
[self.refreshHeaderView setPullToRefreshState:TBCPullToRefreshHeaderViewStateLoading];
self.contentComponent.top = self.segmentComponent.height + kTBCHybridFrsRefreshHeaderViewHeight;
}];
2
3
4
# GCD相关的一些代码块
没有对象持有dispatch_after,所以在dispatch_after里面使用self是不构成循环引用。