iOS 循环引用深度解析:delegate/block/NSTimer/嵌套闭包

在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 deallocParentVC 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。

五、总结:四大场景核心要点(面试必记)

循环引用的核心是"强引用闭环",四种高频场景的解决方案可总结为"一核心、四场景、多方案",覆盖所有面试考点:

  1. 核心原则:打破强引用闭环,优先使用弱引用(__weak/[weak self]),避免相互强引用;
  2. delegate:用weak修饰delegate属性,避免被代理对象强引用代理;
  3. block:用weak-strong dance,外部弱引用self,内部临时强引用,避免异步操作中self释放;
  4. NSTimer:用Block-based API+weak-strong dance,或自定义中间对象,记得在dealloc中invalidate;
  5. 嵌套闭包:外层弱引用self,内层强引用weakSelf,层级过多可扁平化回调。
相关推荐
MonkeyKing1 小时前
iOS AutoreleasePool 深度解析:原理、Page结构与释放时机
ios
报错小能手2 小时前
Swift经典面试题汇总
开发语言·ios·swift
迷途酱2 小时前
Swift 真的被搞得乱七八糟了吗?写了几年之后说点实话
ios·swift
唐诺2 小时前
iOS UI 框架详解
ui·ios
Zender Han3 小时前
Flutter 轻量存储方案介绍、区别、对比和使用场景
android·flutter·ios
2501_916007473 小时前
XCode 15 IDE新特性:苹果集成开发环境全面升级,提升编程效率与体验
ide·vscode·macos·ios·个人开发·xcode·敏捷流程
MonkeyKing71553 小时前
iOS Tagged Pointer 原理、判断方式、适用场景与避坑指南
ios·objective-c
飞Link16 小时前
iOS 27 开启“AI 开放时代”:Siri 驱动可更换背后的技术范式迁移
人工智能·ios
泉木19 小时前
KVC 详解 —— Key-Value Coding 完全指南
ios·swift