强引用与弱引用:循环引用的产生与解决方案

内存管理这事儿,说简单也简单,说复杂也复杂。我见过不少开发者,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 的优化技巧,那个也挺有意思的。