1、iOS内存管理基础:引用计数与ARC原理

内存管理,说白了就是管好对象的生与死。

我刚入行那会儿,用的还是MRC(手动引用计数)。那时候写代码,retain、release、autorelease满天飞。稍不留神,要么内存泄漏,要么野指针崩溃。嗯,那段日子,确实挺痛苦的。

后来ARC来了,大家都说「解放了」。但你真的理解它吗?

1.1 引用计数:对象存活的「计数器」

每个OC对象内部,都有一个retainCount。它就像一张入场券。

  • 对象创建时,retainCount = 1
  • 别人想用这个对象,就retain一下,计数+1
  • 用完了,就release一下,计数-1
  • 当计数归0,对象就被销毁,内存回收

举个例子:

// MRC时代的手动管理
Person *p = [[Person alloc] init];  // retainCount = 1
[p retain];                          // retainCount = 2
[p release];                         // retainCount = 1
[p release];                         // retainCount = 0 → 对象销毁

你想想看,如果少写了一个release,对象就永远活着,内存泄漏。如果多写了一个release,对象提前挂了,后面再访问就是野指针崩溃。

⚠️ 我曾经踩过的坑: 有一次在tableView的cell里,我忘了release一个临时对象。结果用户滑动列表时,内存蹭蹭往上涨。排查了半天,才发现是引用计数没归零。从那以后,我养成了一个习惯:每次alloc之后,立刻想好对应的release在哪。

1.2 ARC:编译器替你写retain/release

ARC不是垃圾回收,它是在编译阶段,自动插入retain、release、autorelease。

说白了,ARC就是帮你干苦力活。但它的规则,你必须懂。

ARC的核心规则就三条:

  1. 强引用(strong):持有对象,引用计数+1。默认就是strong。
  2. 弱引用(weak):不持有对象,引用计数不变。对象销毁时,weak指针自动置nil。
  3. 无主引用(unowned):类似weak,但不自动置nil。对象销毁后,访问它会崩溃。

💡 我个人习惯:

能用weak,就别用unowned。除非你能100%确定,对象的生命周期比你长。我在项目中遇到过几次unowned导致的崩溃,后来全改成weak了,省心。

1.3 自动释放池:延迟释放的「垃圾桶」

有些对象,你不想立刻释放,想让它多活一会儿。这时候就用autorelease。

// ARC下,你不需要手动写autorelease
// 但你可以看到它的效果
- (NSString *)createString {
    NSString *str = [NSString stringWithFormat:@"Hello %d", i];
    // 这个str是autorelease的
    return str;
}

为什么要有autorelease?

因为方法返回的对象,调用者需要用它。如果方法内部直接release了,调用者拿到的是个野指针。所以,把它放进自动释放池,等调用者用完再释放。

自动释放池的机制是这样的:

  • 每个RunLoop循环结束时,会销毁当前的自动释放池
  • 池里的所有对象,都会收到一次release消息
  • 如果对象的引用计数归0,就销毁

🔧 避坑指南:

我曾经在for循环里创建了大量临时字符串,没有加@autoreleasepool。结果循环跑完之前,内存暴涨。后来加了个池子,内存就稳了。

// 错误写法:大量临时对象堆积
for (int i = 0; i < 100000; i++) {
    NSString *str = [NSString stringWithFormat:@"%d", i];
    // 处理str...
}

// 正确写法:及时释放
for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"%d", i];
        // 处理str...
    }
}

1.4 循环引用:最常见的「内存泄漏」元凶

ARC虽然帮你管理了引用计数,但它管不了循环引用。

什么叫循环引用?A强引用B,B强引用A。两个对象互相持有,引用计数永远不为0。它们就永远活着,泄漏了。

最常见的场景:block和delegate。

// 循环引用示例
self.myBlock = ^{
    [self doSomething];  // block强引用self,self强引用block
};

// 正确写法:使用weakSelf
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
};

为什么block里要用weakSelf?

因为block会捕获外部变量。如果block被self持有,block内部又强引用self,就形成了闭环。

📌 记住这个口诀:

「block里用weak,delegate用weak,NSTimer用weak」

这三个地方,是循环引用的高发区。我每次写代码,都会下意识检查一遍。

1.5 总结:ARC不是万能的

ARC帮我们省去了手动管理引用计数的麻烦。但它不是银弹。

  • 你仍然需要理解引用计数的原理
  • 你仍然需要小心循环引用
  • 你仍然需要关注自动释放池的时机

我个人觉得,理解ARC的最好方式,就是把它当成「一个帮你写retain/release的编译器插件」。它的规则是固定的,你只要遵守规则,它就不会坑你。

下一章,我们会深入讲「内存泄漏的检测与定位」。到时候我会分享一些Xcode的调试技巧,还有我在线上环境排查内存问题的实战经验。

嗯,先消化这些吧。