在iOS开发中,循环引用(Retain Cycle)是ARC内存管理中最常见、最隐蔽的问题之一。很多开发者在日常开发中,经常会遇到"对象无法释放、内存泄漏"的情况,排查后发现大多是循环引用导致------尤其是 delegate、block、NSTimer、嵌套闭包这四种高频场景,稍有疏忽就会埋下内存隐患。
循环引用的核心本质的是:两个或多个对象之间相互强引用(retain) ,形成一个闭环,导致所有对象的引用计数都无法降为0,最终无法被系统释放,造成内存泄漏。ARC虽然能自动管理引用计数,但无法自动识别和打破循环引用,需要开发者手动干预。
本文将聚焦这四种高频场景,从"循环引用原理→问题复现→解决方案→实战示例"四个维度,逐层拆解,每个场景都搭配可直接在Xcode中运行的代码,清晰易懂,既适合新手入门避坑,也适合开发者查漏补缺,同时覆盖面试中关于循环引用的高频考点(底层原理+解决方案)。
前置说明:本文基于OC环境(Swift闭包循环引用逻辑类似,文末补充说明),适配iOS 13+,所有示例均结合ARC机制,通过dealloc方法打印验证对象是否释放;涉及的底层源码均做简化处理,保留核心逻辑,便于理解。
一、循环引用核心认知(必记)
在讲解具体场景前,先明确两个核心概念,避免理解偏差:
- 强引用(__strong):默认引用修饰符,会使对象的引用计数+1,只要有强引用存在,对象就不会被释放。循环引用的核心就是"强引用闭环"。
- 弱引用(__weak):不会使对象的引用计数+1,当对象没有任何强引用时,会被释放,同时弱引用变量会自动置nil,避免野指针,是打破循环引用的核心手段。
补充:循环引用的判断标准------当对象应该被释放(如页面pop/dismiss),但dealloc方法未被调用,大概率是存在循环引用,可通过Xcode的Memory Graph(内存图)排查。
二、四种高频循环引用场景(附示例+解决方案)
场景1:delegate 循环引用(最基础,易忽略)
1. 循环引用原理
delegate(代理)模式中,循环引用的产生核心是:被代理对象(如ChildVC)强引用代理对象(如ParentVC),同时代理对象也强引用被代理对象,形成"ParentVC → ChildVC → ParentVC"的强引用闭环。
最常见的错误:将delegate属性用strong修饰(默认就是strong),导致被代理对象强引用代理,进而形成循环引用。
2. 问题复现(错误示例)
less
#import <UIKit/UIKit.h>
// 1. 定义代理协议
@protocol ChildVCDelegate <NSObject>
- (void)childVCDidFinish;
@end
// 2. 被代理对象(ChildVC)
@interface ChildVC : UIViewController
// 错误:delegate用strong修饰,强引用代理对象
@property (nonatomic, strong) id<ChildVCDelegate> delegate;
@end
@implementation ChildVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
}
// 析构函数,验证是否释放
- (void)dealloc {
NSLog(@"ChildVC dealloc");
}
@end
// 3. 代理对象(ParentVC)
@interface ParentVC : UIViewController
@end
@implementation ParentVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
// 创建ChildVC,强引用ChildVC
ChildVC *childVC = [[ChildVC alloc] init];
// 代理对象(self)强引用ChildVC
childVC.delegate = self;
// 跳转到ChildVC(此时ParentVC强引用ChildVC,ChildVC强引用ParentVC)
[self presentViewController:childVC animated:YES completion:nil];
}
// 实现代理方法
- (void)childVCDidFinish {
[self dismissViewControllerAnimated:YES completion:nil];
}
// 析构函数,验证是否释放
- (void)dealloc {
NSLog(@"ParentVC dealloc");
}
@end
运行结果:当ChildVC dismiss回到ParentVC,再退出ParentVC时,ChildVC dealloc和ParentVC dealloc均未打印------说明两个对象都未被释放,存在循环引用。
3. 解决方案(核心:delegate用weak修饰)
打破循环引用的关键:将被代理对象的delegate属性改为weak修饰,使被代理对象对代理对象的引用变为弱引用,打破强引用闭环。
修改后代码(仅修改ChildVC的delegate属性):
objectivec
// 正确:delegate用weak修饰,弱引用代理对象
@property (nonatomic, weak) id<ChildVCDelegate> delegate;
运行结果:dismiss ChildVC、退出ParentVC时,两个对象的dealloc方法均会打印------说明循环引用已打破,对象正常释放。
4. 避坑点
- 永远不要用
strong修饰delegate属性,这是delegate循环引用的根源; - 若代理对象可能提前释放,需在代理方法中先判断delegate是否存在(
if (self.delegate && [self.delegate respondsToSelector:@selector(childVCDidFinish)])),避免野指针。
场景2:block 循环引用(高频,面试重点)
1. 循环引用原理
Block循环引用的产生核心是:对象(如ViewController)强引用Block,同时Block内部强引用该对象(如self) ,形成"对象 → Block → 对象"的强引用闭环。
底层补充:ARC环境下,当Block被赋值给strong变量 (或手动copy)时,Block会从栈转移到堆,此时Block会对内部捕获的OC对象进行强引用(retain语义) ,若捕获的对象是self,且self强引用Block,就会形成循环引用。
2. 问题复现(错误示例)
objectivec
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
// strong修饰Block,self强引用Block
@property (nonatomic, strong) void (^myBlock)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 错误:Block内部强引用self,self强引用Block,形成循环引用
self.myBlock = ^{
// Block内部捕获self,强引用self
NSLog(@"Block执行,self地址:%p", self);
};
// 执行Block
self.myBlock();
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
}
@end
运行结果:退出ViewController时,dealloc方法未打印------Block与self相互强引用,对象无法释放。
3. 解决方案(核心:weak-strong dance)
打破Block循环引用的标准方案:在Block外部用__weak修饰self,将弱引用的self捕获到Block内部;在Block内部用__strong临时强引用weakSelf,避免异步操作中self被提前释放(即"弱引用-强引用舞蹈")。
修改后代码:
objectivec
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (nonatomic, strong) void (^myBlock)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 1. 外部用__weak修饰self,弱引用self
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
// 2. 内部用__strong临时强引用weakSelf,避免异步操作中self被释放
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // 判空,防止self已释放
// 用strongSelf访问self的属性/方法
NSLog(@"Block执行,self地址:%p", strongSelf);
};
self.myBlock();
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
}
@end
运行结果:退出ViewController时,dealloc方法正常打印------循环引用已打破。
4. 特殊场景补充(异步Block)
若Block内部有异步操作(如GCD延迟执行),必须在Block内部用__strong临时强引用weakSelf,否则异步操作执行前,self可能已被释放,导致weakSelf置nil,无法执行后续操作:
scss
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
// 异步操作(延迟2秒执行)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"异步Block执行,self:%p", strongSelf);
});
};
self.myBlock();
}
场景3:NSTimer 循环引用(隐蔽,易踩坑)
1. 循环引用原理
NSTimer循环引用的产生核心是:对象(如ViewController)强引用NSTimer,NSTimer的target强引用该对象,同时NSTimer会被加入到RunLoop中,RunLoop也会强引用NSTimer,形成"RunLoop → NSTimer → 对象 → NSTimer"的强引用闭环。
关键点:NSTimer的target默认是强引用,只要NSTimer处于有效状态(未invalidate),target就不会被释放;若对象强引用NSTimer,就会形成循环引用。
2. 问题复现(错误示例)
objectivec
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
// self强引用timer
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 错误:timer的target是self(强引用self),self强引用timer,形成循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick)
userInfo:nil
repeats:YES];
}
// timer回调方法
- (void)timerTick {
NSLog(@"timer执行");
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
// 此处代码无法执行,因为循环引用导致self无法释放,timer无法 invalidate
[self.timer invalidate];
self.timer = nil;
}
@end
运行结果:退出ViewController时,dealloc方法未打印,timer仍在持续打印日志------循环引用导致self无法释放,timer无法被 invalidate,内存持续泄漏。
3. 解决方案(两种常用方案)
方案1:使用Block-based NSTimer API(推荐,简单高效)
iOS 10+ 提供了Block-based的NSTimer API,无需设置target,通过Block访问self时,用weak-strong dance避免循环引用,同时在dealloc中invalidate timer。
objectivec
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 方案1:Block-based API,用weak-strong dance避免循环引用
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
[timer invalidate]; // 若self已释放,停止timer
return;
}
[strongSelf timerTick];
}];
}
- (void)timerTick {
NSLog(@"timer执行");
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
[self.timer invalidate];
self.timer = nil;
}
@end
方案2:自定义中间对象(兼容iOS 10以下)
创建一个中间对象(如TimerTarget),作为NSTimer的target,中间对象用weak引用原对象(self),避免NSTimer直接强引用self,打破循环引用。(代码略,核心逻辑:中间对象转发回调,弱引用原对象)
4. 避坑点
- NSTimer必须在对象释放前调用
invalidate,并将timer置nil,否则即使打破循环引用,timer仍会被RunLoop持有,导致内存泄漏; - 避免在timer回调中强引用self,否则仍会形成循环引用。
场景4:嵌套闭包(Block嵌套,隐蔽性强)
1. 循环引用原理
嵌套闭包(Block嵌套)的循环引用,是Block循环引用的进阶场景,核心是:外层Block被对象强引用,外层Block强引用内层Block,内层Block强引用对象,形成"对象 → 外层Block → 内层Block → 对象"的强引用闭环;或内层Block与外层Block相互强引用,同时引用对象,形成多重闭环。
常见场景:网络请求回调嵌套、动画回调嵌套、异步任务编排(如登录→获取个人信息→获取头像的串行回调)。
2. 问题复现(错误示例)
objectivec
#import <UIKit/UIKit.h>
// 模拟网络请求工具类
@interface NetworkTool : NSObject
+ (void)loginWithCompletion:(void(^)(NSString *token))completion;
+ (void)fetchUserInfoWithToken:(NSString *)token completion:(void(^)(NSString *username))completion;
@end
@implementation NetworkTool
+ (void)loginWithCompletion:(void (^)(NSString *))completion {
// 模拟网络请求延迟1秒
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
completion(@"token123456");
});
}
+ (void)fetchUserInfoWithToken:(NSString *)token completion:(void (^)(NSString *))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
completion(@"iOS开发者");
});
}
@end
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 错误:嵌套Block,内层Block强引用self,外层Block被self隐式强引用,形成循环引用
[NetworkTool loginWithCompletion:^(NSString *token) {
// 外层Block捕获self,强引用self
[NetworkTool fetchUserInfoWithToken:token completion:^(NSString *username) {
// 内层Block捕获self,强引用self,形成闭环
NSLog(@"登录成功,用户名:%@,self地址:%p", username, self);
}];
}];
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
}
@end
运行结果:退出ViewController时,dealloc方法未打印------嵌套Block与self形成循环引用,对象无法释放。
3. 解决方案(核心:外层Block弱引用self,内层Block强引用weakSelf)
打破嵌套闭包循环引用的关键:在最外层Block外部,用__weak修饰self,将weakSelf捕获到外层Block;在内层Block内部,用__strong临时强引用weakSelf,避免异步操作中self被释放,同时确保所有Block内部都不直接强引用self。
修改后代码:
objectivec
#import <UIKit/UIKit.h>
@interface NetworkTool : NSObject
+ (void)loginWithCompletion:(void(^)(NSString *token))completion;
+ (void)fetchUserInfoWithToken:(NSString *)token completion:(void(^)(NSString *username))completion;
@end
@implementation NetworkTool
+ (void)loginWithCompletion:(void (^)(NSString *))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
completion(@"token123456");
});
}
+ (void)fetchUserInfoWithToken:(NSString *)token completion:(void (^)(NSString *))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
completion(@"iOS开发者");
});
}
@end
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 1. 外层Block外部,用__weak修饰self
__weak typeof(self) weakSelf = self;
[NetworkTool loginWithCompletion:^(NSString *token) {
// 2. 外层Block捕获weakSelf(弱引用)
[NetworkTool fetchUserInfoWithToken:token completion:^(NSString *username) {
// 3. 内层Block内部,用__strong临时强引用weakSelf
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSLog(@"登录成功,用户名:%@,self地址:%p", username, strongSelf);
}];
}];
}
- (void)dealloc {
NSLog(@"ViewController dealloc");
}
@end
运行结果:退出ViewController时,dealloc方法正常打印------嵌套Block的循环引用已打破。
4. 进阶优化(嵌套层级过多)
若嵌套Block层级过多(如3层及以上),可考虑引入Promise/async-await机制,扁平化回调,减少嵌套,同时避免循环引用,使代码更简洁易维护。
三、Swift 闭包循环引用补充(简要说明)
Swift中闭包循环引用的原理与OC Block一致,核心是"对象强引用闭包,闭包强引用对象",解决方案如下:
- 用
[weak self]弱引用self(对应OC的__weak),闭包内部可选项绑定(guard let self = self else { return }),对应OC的weak-strong dance; - 若self不会提前释放,可用
[unowned self](对应OC的__unsafe_unretained),但存在野指针风险,慎用; - 嵌套闭包场景:仅需在最外层闭包用
[weak self],内层闭包直接使用绑定后的self即可。
四、循环引用排查技巧(实战必备)
1. 基础排查:dealloc打印
在怀疑存在循环引用的对象中,重写dealloc方法(OC)或deinit方法(Swift),打印日志,若对象应该释放时日志未打印,说明存在循环引用。
2. 工具排查:Xcode Memory Graph(内存图)
步骤:运行项目 → 点击Xcode顶部"Debug Memory Graph"按钮 → 找到未释放的对象 → 查看对象的引用链,红色箭头表示强引用,若形成闭环,即为循环引用,可精准定位到引用源头。
3. 编码规范:提前规避
- delegate属性必须用weak修饰;
- Block/闭包内部访问self时,必用weak-strong dance;
- NSTimer使用Block-based API,且在dealloc中invalidate;
- 嵌套闭包仅在最外层弱引用self,内层强引用weakSelf。
五、总结:四大场景核心要点(面试必记)
循环引用的核心是"强引用闭环",四种高频场景的解决方案可总结为"一核心、四场景、多方案",覆盖所有面试考点:
- 核心原则:打破强引用闭环,优先使用弱引用(__weak/[weak self]),避免相互强引用;
- delegate:用weak修饰delegate属性,避免被代理对象强引用代理;
- block:用weak-strong dance,外部弱引用self,内部临时强引用,避免异步操作中self释放;
- NSTimer:用Block-based API+weak-strong dance,或自定义中间对象,记得在dealloc中invalidate;
- 嵌套闭包:外层弱引用self,内层强引用weakSelf,层级过多可扁平化回调。