Objective-C内存管理(MRC/ARC)看这篇就懂了 – wiki基地


Objective-C内存管理(MRC/ARC)看这篇就懂了

引言:为何内存管理如此重要?

在软件开发的世界里,内存管理是一个永恒的话题。它如同一个国家的财政系统,管理不善会导致资源枯竭(内存泄漏),或系统崩溃(野指针访问)。对于 Objective-C 这门建立在 C 语言之上的语言而言,内存管理更是其核心精髓之一。从早期的手动管理(MRC)到后来的自动管理(ARC),苹果公司不断努力简化开发者的负担,但其底层的思想——“引用计数”(Reference Counting)——却从未改变。

理解 Objective-C 的内存管理,不仅是面试中的高频考点,更是编写出健壮、高效、无泄漏 iOS 或 macOS 应用的基石。许多开发者可能直接从 ARC 时代入门,对 MRC 感到陌生,但这恰恰是理解 ARC 工作原理的钥匙。本文将带您穿越时空,从最基本的内存分区讲起,深入剖析 MRC 的黄金法则,最终揭开 ARC 的神秘面纱,让您真正做到“知其然,并知其所以然”。


第一部分:内存管理的基石——栈与堆

要谈内存管理,首先必须理解程序运行时内存是如何划分的。在 Objective-C(以及大多数编程语言)中,内存主要分为两个区域:栈(Stack)堆(Heap)

  1. 栈(Stack)

    • 管理者:由编译器自动管理,无需我们操心。
    • 存储内容:主要存放局部变量、函数参数、方法调用的地址等。例如,int a = 10;,这个变量 a 就存放在栈上。
    • 特点
      • 高效:栈的内存分配和回收速度极快,因为它是一个“后进先出”(LIFO)的数据结构,分配和释放只是移动栈顶指针。
      • 自动:当一个函数或方法执行完毕,其在栈上创建的所有局部变量都会被自动销毁。
      • 空间有限:栈的可用空间相对较小,不适合存储大型数据或生命周期需要跨越多个方法作用域的数据。
  2. 堆(Heap)

    • 管理者:由开发者“手动”或“间接”管理。这正是我们内存管理的主战场。
    • 存储内容:所有通过 allocnewcopy 等方法创建的 Objective-C 对象都存放在堆上。
    • 特点
      • 灵活:堆空间远大于栈,可以动态地申请和释放任意大小的内存。
      • 生命周期长:对象的生命周期不局限于某个方法或作用域,只要有“人”还在使用它,它就会一直存在。
      • 管理复杂:正是因为其灵活性,堆内存的管理也变得复杂。如果申请了内存而忘记释放,就会造成内存泄漏(Memory Leak);如果内存已经被释放,但仍然试图去访问它,就会导致野指针(Dangling Pointer)问题,引发程序崩溃。

核心问题:我们如何知道一个堆上的对象何时不再被需要,可以安全地释放它呢?Objective-C 的答案是:引用计数(Reference Counting)

引用计数:一个生动的比喻

想象一下,堆中的一个对象是一本图书馆里的珍贵图书。
* alloc/new:图书馆购入一本新书,这本书至少有一个“馆藏”引用,所以它的“被借阅数”(引用计数)为 1。
* retain(持有):你(一个指针)想借阅这本书,于是办理了借阅手续。这本书的“被借阅数”加 1。
* release(释放):你读完了,把书还给图书馆。这本书的“被借阅数”减 1。
* dealloc(销毁):当这本书的“被借阅数”变为 0 时,意味着没有任何人(包括馆藏)在借阅它了。图书馆管理员就会将这本书下架销毁,腾出空间。

这个“被借阅数”就是对象的引用计数(Retain Count)。Objective-C 的运行时系统通过追踪每个对象的引用计数,来决定其何时应该被销毁。


第二部分:手动挡的时代——MRC (Manual Retain-Release)

在 ARC 出现之前,开发者需要像开手动挡汽车一样,精确地控制每一个对象的“离合”与“挂挡”——也就是手动调用 retain, release, autorelease。这套规则的核心思想是“谁创建,谁持有;谁持有,谁释放”

MRC 的黄金法则

苹果官方文档总结了四条黄金法则,是所有 MRC 操作的根本依据:

  1. 自己生成的对象,自己持有 (You own any object you create)。

    • 当你使用以 alloc, new, copy, mutableCopy 开头的方法创建了一个对象时,你就拥有了这个对象,其引用计数为 1。你必须在未来某个时刻负责释放它。

    “`objc
    // 你通过 alloc 创建了 str,所以你拥有它。
    // str 的引用计数为 1。
    NSString *str = [[NSString alloc] initWithFormat:@”Hello”];

    // … 使用 str …

    // 你不再需要 str,你有责任释放它。
    [str release];
    “`

  2. 通过 retain 持有对象 (You take ownership of an object by retaining it)。

    • 当你从其他地方(如方法返回值、参数等)获得一个对象,而你希望在自己的作用域内长期使用它时,你需要调用 retain 来声明你对它的所有权。这会将对象的引用计数加 1。

    “`objc
    // 假设 otherObject 是别人传给我们的,我们不拥有它。
    // 为了确保在我们的方法执行期间它不被释放,我们 retain 它。
    [otherObject retain];
    // 现在 otherObject 的引用计数 +1,我们也拥有了它。

    // … 使用 otherObject …

    // 当我们用完后,同样需要释放我们的所有权。
    [otherObject release];
    “`

  3. 当你不再需要你持有的对象时,必须释放它 (You must relinquish ownership of objects you own)。

    • 对于你所拥有的每一个对象(无论是通过创建还是 retain 获得),都必须在不再需要它时调用 releaseautorelease 来放弃所有权。
  4. 不要释放你不持有的对象 (You must not relinquish ownership of an object you do not own)。

    • 如果你没有通过上述方式持有某个对象,那么你绝对不能 release 它。这样做会导致过度释放,当引用计数减到负数时,程序会崩溃。

MRC 实践:dealloc 与属性

  • dealloc 方法:这是对象生命周期的终点。当一个对象的引用计数降为 0 时,系统会自动调用其 dealloc 方法。在 MRC 中,dealloc 的核心任务是释放该对象所持有的其他对象。

    “`objc
    – (void)dealloc {
    // 释放你所持有的实例变量
    [_myString release];
    [_myArray release];

    // 必须调用父类的 dealloc,这是规定!
    [super dealloc];
    

    }
    “`

  • 属性(Property):在 MRC 时代,属性的 setter 方法是体现内存管理逻辑的关键场所。

    • assign:用于基本数据类型(int, float, BOOL)和非对象类型的指针。它只是简单的赋值,不涉及任何引用计数操作。
    • retain:用于对象类型。setter 方法会自动为你处理“先 release 旧值,再 retain 新值”的逻辑,以保证所有权正确转移且不发生泄漏。

    “`objc
    @property (nonatomic, retain) NSString *name;

    // 上面的声明,编译器会自动生成(或我们可以手动实现)如下的 setter 方法:
    – (void)setName:(NSString *)newName {
    if (_name != newName) {
    [_name release]; // 释放旧值的所有权
    _name = [newName retain]; // 持有新值的所有权
    }
    }
    “`

autoreleaseNSAutoreleasePool

autorelease 是 MRC 中一个巧妙但略显复杂的机制。它表示“我暂时不想释放这个对象,但请在未来的某个时刻帮我释放它”。

  • 使用场景:当一个方法需要创建一个对象并将其返回给调用者时,它不能在方法内部 release 这个对象(否则调用者会收到一个被释放的野指针),但它又不希望调用者必须记住去 release

    “`objc
    // 一个典型的工厂方法
    + (NSString )createMyString {
    NSString
    str = [[NSString alloc] initWithFormat:@”I am a temporary string”];

    // 我们创建了 str,但要把它返回给别人。
    // 如果直接返回,调用者就得负责 release。
    // 如果在这里 release,返回的就是野指针。
    // 正确做法是 autorelease。
    return [str autorelease]; 
    // 这会将 str 放入当前的“自动释放池”。
    // str 的所有权被转移给了池子,它的引用计数暂时不变。
    

    }
    “`

  • NSAutoreleasePool:自动释放池。所有被标记为 autorelease 的对象,都会被添加到离它最近的 NSAutoreleasePool 中。当这个池子被“抽干”(drain)或销毁时,它会向池中所有的对象发送一条 release 消息。

    • 在 iOS 应用中,主线程的 RunLoop 会在每个事件循环的开始创建一个 NSAutoreleasePool,并在事件循环结束时将其抽干。这就是为什么大部分情况下我们感觉不到池子的存在。
    • 在需要大量创建临时对象的循环中,手动创建 NSAutoreleasePool 可以有效防止内存峰值过高。

第三部分:自动挡的革命——ARC (Automatic Reference Counting)

MRC 功能强大,但完全依赖于开发者的严谨和细心。稍有不慎,retainrelease 的不匹配就会导致内存泄漏或崩溃。为了解放生产力,苹果在 iOS 5 和 OS X 10.7 中引入了 ARC。

重要认知:ARC 不是垃圾回收(GC)!

ARC 不是像 Java 或 C# 那样的垃圾回收器,它没有一个后台线程在扫描和回收垃圾。ARC 的本质是,编译器在编译阶段,自动为你插入了所有必要的 retain, release, autorelease 调用。 换句话说,ARC 只是把 MRC 的那些黄金法则自动化了。你写的代码表面上干净了,但编译后的代码和精心编写的 MRC 代码几乎一样。

ARC 下的变化

  1. 禁止手动调用:在 ARC 环境下,你不能再调用 retain, release, autorelease, retainCount,也不能调用 dealloc(但可以重写 dealloc 来释放非对象资源,如 Core Foundation 对象、文件句柄等,只是不能调用 [super dealloc])。
  2. 新的属性关键字
    • strong:替代了 MRC 的 retain。表示一个“强引用”,只要有强引用指向一个对象,这个对象就不会被销毁。这是对象属性的默认值。
    • weak:表示一个“弱引用”。它指向一个对象,但不会增加其引用计数。最关键的是,当弱引用指向的对象被销毁后,该弱引用会自动被置为 nil。这是 ARC 解决“循环引用”问题的核心武器。
    • unsafe_unretained:类似 weak,也是弱引用,不增加引用计数。但区别在于,当它指向的对象被销毁后,它不会被置为 nil,而是变成一个野指针。除非为了兼容老版本系统或特殊性能要求,否则应优先使用 weak
    • assign: 依然用于基本数据类型。

ARC 的核心任务:解决循环引用(Retain Cycle)

这是 ARC 时代开发者最需要关注的内存问题。

  • 什么是循环引用?
    两个或多个对象互相强引用对方,导致它们的引用计数都无法降为 0,从而谁也无法被释放,造成内存泄漏。

  • 典型场景:Delegate 模式
    一个 ViewController 持有一个 UITableViewUITableView 又需要一个 delegatedataSource 来提供数据和处理事件,通常这个 delegate 就是 ViewController 自己。

    “`objc
    // ViewController.h
    @interface MyViewController : UIViewController
    @property (nonatomic, strong) UITableView *tableView; // VC 强引用 tableView
    @end

    // ViewController.m
    – (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView = [[UITableView alloc] init];
    self.tableView.delegate = self; // tableView 强引用了 VC
    // 此时,VC -> tableView, tableView -> VC,循环引用形成!
    }
    ``
    在这个例子中,
    MyViewControllertableView属性是strong的,它强引用了tableView对象。同时,tableViewdelegate属性在系统实现中也是strong的(在 ARC 之前是retain),它强引用了MyViewController` 实例。这样一来,两个对象互相“抓住”对方,谁也无法在对方被释放前释放,导致双双泄漏。

  • 解决方案:weak
    打破循环引用的关键在于,将循环链条中的一环从强引用(strong)改为弱引用(weak)。通常遵循“谁拥有谁,谁就强引用;谁只是使用谁,谁就弱引用”的原则。

    在 Delegate 模式中,ViewController “拥有” tableView,所以它对 tableView 的引用是 strong 是合理的。而 tableView 只是“使用” ViewController 作为其代理,不应该拥有它。因此,delegate 属性的设计就应该是 weak 的。

    事实上,苹果 SDK 中所有的 delegatedataSource 属性都已经被声明为 weak(或在 MRC 下的 assign)。

    objc
    // UIKit 中 UITableView 的 delegate 属性声明
    @property (nonatomic, weak, nullable) id<UITableViewDelegate> delegate;

    因此,我们日常开发中遇到的 delegate 循环引用问题,通常是我们自定义类时忘记将 delegate 属性声明为 weak

    “`objc
    // 正确的自定义 delegate 声明
    @protocol MyCustomClassDelegate
    – (void)customClassDidFinishTask:(MyCustomClass *)sender;
    @end

    @interface MyCustomClass : NSObject
    // 代理属性必须声明为 weak,以防循环引用
    @property (nonatomic, weak) id delegate;
    @end
    ``
    * **另一个常见场景:Block**
    Block 内部如果使用了
    self,会导致 Block 捕获并强引用self。如果此时self` 也强引用了这个 Block(比如把 Block 存为一个属性),循环引用就产生了。

    objc
    // 在一个 ViewController 内部
    self.myBlock = ^{
    // Block 强引用了 self
    NSLog(@"My name is %@", self.name);
    };
    // self.myBlock 强引用了 Block
    // self -> Block -> self 循环引用

    解决方案:使用 weak-strong dance

    objc
    __weak typeof(self) weakSelf = self; // 创建一个 self 的弱引用
    self.myBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf; // 在 Block 内部转为强引用
    if (strongSelf) {
    // Block 内部使用 strongSelf,确保在 Block 执行期间 self 不会被释放
    NSLog(@"My name is %@", strongSelf.name);
    }
    };

    这样,self 对 Block 是强引用,但 Block 对 self 只是弱引用 (weakSelf),打破了循环。Block 内部的 strongSelf 只是一个局部变量,当 Block 执行完毕后就会释放,它保证了在 Block 执行的瞬间 self 是存在的,防止了多线程环境下可能出现的问题。


第四部分:总结与展望

特性 MRC (Manual Retain-Release) ARC (Automatic Reference Counting)
核心思想 开发者手动管理对象所有权 编译器自动管理对象所有权
关键操作 retain, release, autorelease 无,由编译器自动插入
属性关键字 assign, retain, copy assign, strong, weak, unsafe_unretained, copy
dealloc 必须调用 [super dealloc],释放持有的对象 不能调用 [super dealloc],用于释放非 OC 资源
主要挑战 忘记 release 导致泄漏,过度 release 导致崩溃 循环引用(Retain Cycle)导致的内存泄漏
解决方案 遵循黄金法则,细心编码 使用 weak 关键字打破强引用环,尤其在 delegateblock

从 MRC 到 ARC,是 Objective-C 开发的一次巨大飞跃。它极大地降低了内存管理的门槛,让开发者能更专注于业务逻辑。然而,这并不意味着我们可以完全忽视内存管理。ARC 只是将繁琐的手动操作自动化,但其背后的“引用计数”和“所有权”思想依然是根基。

要成为一名优秀的 iOS/macOS 开发者,深刻理解 MRC 的工作原理是必不可少的。它能让你在面对 ARC 下的循环引用问题时,能从根源上分析并解决问题,而不是仅仅记住“delegate 要用 weak”这样的表层规则。希望通过本文的梳理,您已经对 Objective-C 的内存管理有了一个清晰而立体的认识,无论是面对老旧的 MRC 项目,还是在现代的 ARC 开发中,都能游刃有余。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部