强引用与弱引用:循环引用的产生与解决方案
内存管理这事儿,说简单也简单,说复杂也复杂。我见过不少开发者,ARC 用得很溜,但一遇到循环引用就抓瞎。今天咱们就来聊聊这个经典问题——循环引用是怎么产生的,又该怎么解决。
先说说引用计数
在 Objective-C 和 Swift 里,每个对象都有一个引用计数。说白了,就是有多少个地方在「指着」这个对象。当引用计数降到 0 时,对象就会被释放。
举个例子:
// 创建一个对象,引用计数为 1
Person *person = [[Person alloc] init];
// 另一个变量也指向它,引用计数变成 2
Person *anotherPerson = person;
// 第一个引用消失,计数降为 1
person = nil;
// 第二个引用消失,计数降为 0,对象被释放
anotherPerson = nil;
嗯,这里要注意:ARC 帮我们自动管理了 retain 和 release,但引用计数的本质没变。
循环引用是怎么来的?
你想想看,如果 A 对象强引用了 B,B 又强引用了 A,那这两个对象的引用计数永远都不会变成 0。这就是循环引用。
我在项目中遇到过最典型的场景——两个 ViewController 互相持有:
// ViewControllerA.h
@property (nonatomic, strong) ViewControllerB *vcB;
// ViewControllerB.h
@property (nonatomic, strong) ViewControllerA *vcA;
这样写,两个 VC 谁也释放不了。内存泄漏就这么产生了。
常见的循环引用场景
除了上面那种双向持有,还有几种情况我经常见到:
- Block 中的循环引用——对象持有 block,block 又捕获了 self
- Delegate 使用 strong——delegate 本应用 weak,有人用了 strong
- NSTimer 的 target——timer 持有 target,target 又持有 timer
咱们一个个来看。
Block 循环引用
这是最容易被忽视的。我记得有一次排查内存泄漏,查了半天才发现是 block 里用了 self:
// 错误写法
self.callback = ^{
[self doSomething];
};
// 正确写法
__weak typeof(self) weakSelf = self;
self.callback = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
为什么要加个 strongSelf?因为 weakSelf 可能在 block 执行过程中被释放。加个 strongSelf 能保证 block 执行期间 self 是活的。
我的习惯:在 block 开头先声明 weakSelf,然后在 block 内部第一行声明 strongSelf。这样既安全又清晰。
Delegate 用 strong 的问题
这个坑我踩过。曾经有个同事把 delegate 声明成了 strong:
// 错误
@property (nonatomic, strong) id<SomeDelegate> delegate;
// 正确
@property (nonatomic, weak) id<SomeDelegate> delegate;
为什么 delegate 要用 weak?因为 delegate 通常是持有者,比如 ViewController 持有 tableView,tableView 的 delegate 指向 ViewController。如果 delegate 是 strong,就形成了循环引用。
NSTimer 的坑
NSTimer 会强持有它的 target。如果你在 ViewController 里创建了一个 timer,timer 又持有 self,那这个 VC 就永远释放不了。
// 错误
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(tick:)
userInfo:nil
repeats:YES];
// 解决方案:在 dealloc 中 invalidate
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
但这里有个问题:如果 VC 的 dealloc 永远不会被调用呢?嗯,那就死锁了。所以更安全的做法是用 block 版本的 timer,或者用 weakSelf。
如何检测循环引用?
说实话,靠肉眼找循环引用挺累的。我一般用这几个工具:
| 工具 | 用途 | 使用场景 |
|---|---|---|
| Instruments - Leaks | 检测内存泄漏 | 运行时实时监控 |
| Instruments - Allocations | 查看对象存活情况 | 分析对象是否被释放 |
| MLeaksFinder | 自动检测泄漏 | 开发阶段集成 |
| FBRetainCycleDetector | 检测循环引用链 | 定位具体引用路径 |
我个人习惯在开发阶段就集成 MLeaksFinder。它能自动检测 ViewController 和 View 是否被正确释放,一旦发现泄漏就会弹出提示。
避坑指南:我曾经在项目里只依赖 Instruments 来检测循环引用,结果上线后才发现有些场景下泄漏了。后来我学乖了——开发阶段用 MLeaksFinder,上线前再用 Instruments 做一次全面检查。
解决循环引用的核心思路
说白了就一句话:把其中一方的强引用改成弱引用。
具体来说:
- 父子关系:父强引用子,子弱引用父
- Delegate 模式:委托方弱引用 delegate
- Block 捕获:用 weak-strong dance
- Timer:及时 invalidate,或者用 weakSelf
你想想看,只要打破了引用环中的任意一条边,整个环就解开了。
一个完整的例子
咱们来看一个实际项目中的场景。假设有个网络请求工具类:
// NetworkManager.h
typedef void(^CompletionBlock)(NSData *data, NSError *error);
@interface NetworkManager : NSObject
@property (nonatomic, copy) CompletionBlock completion;
- (void)startRequest;
@end
// ViewController.m
- (void)loadData {
__weak typeof(self) weakSelf = self;
self.manager.completion = ^(NSData *data, NSError *error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (error) {
[strongSelf showError:error];
} else {
[strongSelf updateUI:data];
}
};
[self.manager startRequest];
}
这里用了 weak-strong dance,既避免了循环引用,又保证了 block 执行期间 self 不会被释放。
关键点:weakSelf 是为了打破循环引用,strongSelf 是为了保证 block 执行期间对象存活。两者缺一不可。
总结一下
循环引用说白了就是对象之间互相「抱着不放」。解决思路很简单——让其中一方松手。但实际项目中,要找到这个「松手」的点,需要你对对象之间的引用关系有清晰的认识。
我建议你在写代码时养成一个习惯:每次声明属性时,先想想这个属性应该是 strong 还是 weak。如果是 delegate、block、或者父子关系中的子对象,大概率要用 weak。
嗯,今天就聊到这儿。下一章咱们聊聊 autoreleasepool 的优化技巧,那个也挺有意思的。