iOS开发基础25-内存管理深度解析:ARC 与 MRC

iOS 内存管理深度解析:ARC 与 MRC

内存管理是 iOS 开发的核心基础。本文深入解析自动引用计数(ARC)与手动引用计数(MRC)的原理、规则、循环引用、autoreleasepool、引用计数存储以及 CoreFoundation 桥接,并补充 Swift 中的 ARC 实践。


一、自动引用计数(ARC)

1. 什么是 ARC

ARC(Automatic Reference Counting)从 iOS 5 引入,是编译器特性,在编译时自动分析对象的生命周期,插入 retainreleaseautorelease 等内存管理代码,开发者无需手动管理。

ARC 以编译时特性为主,但 weak 引用的自动置 nil、引用计数的存储与维护等需要运行时支持(objc_runtime),并非纯编译时。

2. 基本原理

ARC 基于引用计数:对象被强引用时引用计数 +1,强引用释放时 -1,引用计数归 0 时对象销毁(调用 dealloc)。

方法 作用
retain 引用计数 +1
release 引用计数 -1,归 0 时销毁对象
autorelease 放入自动释放池,pool 销毁时 -1

3. 使用规则

  1. 禁止显式调用 retainreleaseautoreleaseretainCount(编译器报错)。
  2. 可以重写 dealloc,但不能调用 [super dealloc](编译器自动插入)。
  3. @property 修饰符:
修饰符 作用 适用场景
strong 强引用,引用计数 +1,类似 MRC 的 retain 大多数对象属性
weak 弱引用,不增加引用计数,对象销毁时自动置 nil delegate、Block 内引用 self、避免循环引用
assign 不增加引用计数,对象销毁后不自动置 nil(野指针风险) 基本数据类型(int/BOOL/CGFloat)
copy 拷贝对象并强引用,类似 MRC 的 copy NSString、NSArray、Block 等可变类型
unsafe_unretained 类似 weak,但对象销毁后不自动置 nil(野指针) 需兼容 iOS 4 等不支持 weak 的场景

weakunsafe_unretained 的核心区别:weak 在对象销毁时自动置 nil(安全),unsafe_unretained 不置 nil(悬垂指针,访问会崩溃)。iOS 5+ 优先使用 weak

4. dealloc 中需要手动处理的内容

ARC 下成员变量的释放由编译器自动完成,但以下资源需在 dealloc 中手动清理:

- (void)dealloc {
    // 移除 KVO 监听(不移除会崩溃)
    [self.obj removeObserver:self forKeyPath:@"property"];
    
    // 移除通知观察者(iOS 9+ selector 方式可自动移除,Block 方式仍需手动)
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    // 停止定时器(NSTimer 会强引用 target,不移除会循环引用)
    [self.timer invalidate];
    
    // 停止网络请求、关闭文件句柄等
}

5. 循环引用

当两个对象互相强引用时,引用计数永远无法归 0,导致内存泄漏:

// 错误:循环引用,两个对象都无法释放
@interface Person : NSObject
@property (nonatomic, strong) Dog *dog; // Person 强引用 Dog
@end

@interface Dog : NSObject
@property (nonatomic, strong) Person *person; // Dog 强引用 Person → 循环引用!
@end

解决方案:将其中一端改为 weak

@interface Dog : NSObject
@property (nonatomic, weak) Person *person; // weak 打破循环引用
@end

常见循环引用场景

场景 原因 解决方案
delegate 控制器强引用 View,View 强引用 delegate=控制器 delegate 用 weak
Block Block 强引用 self,self 强引用 Block Block 内用 __weak typeof(self) weakSelf
NSTimer timer 强引用 target=self,self 强引用 timer 使用 weak target 中间类或 iOS 10+ block-based timer
异步回调 控制器被 pop 后仍被强引用 使用 weak 引用

Block 循环引用的 Weak-Strong Dance

__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return; // self 已释放,直接返回
    [strongSelf doSomething];
};

6. ARC 底层实现

编译器在编译时自动插入内存管理代码。以属性 setter 为例:

// 源码
@interface Person : NSObject
@property (nonatomic, strong) Dog *dog;
@end

@implementation Person
- (void)setDog:(Dog *)dog {
    if (_dog != dog) {
        [_dog release];  // 释放旧值
        _dog = [dog retain]; // 保留新值
    }
}
@end

对象创建与使用的编译后逻辑:

// 源码
Person *person = [[Person alloc] init]; // alloc init 后引用计数为 1
Dog *dog = [[Dog alloc] init];          // 引用计数为 1
person.dog = dog;                        // strong setter 内部 retain dog → dog 引用计数 2
// ... 使用 ...
// 方法结束时,编译器自动插入:
[dog release];    // dog 引用计数 1(仍被 person.dog 持有)
[person release]; // person 引用计数 0 → dealloc → dealloc 中 release _dog → dog 引用计数 0 → dealloc

[[Person alloc] init] 返回的对象引用计数已经是 1,ARC 不会在 alloc 后再插入 retain。方法结束时,编译器根据对象的使用情况自动插入 release


二、手动引用计数(MRC)

1. 什么是 MRC

MRC(Manual Reference Counting)是 iOS 早期的内存管理方式,开发者需手动调用 retainreleaseautorelease 管理对象生命周期。iOS 5 之前是默认方式,现已被 ARC 取代,但理解 MRC 有助于深入理解 ARC。

2. 核心规则(内存管理黄金法则)

你拥有(own)通过 allocnewcopymutableCopy 创建的对象,必须在不需要时 release

通过其他方法获得的对象,你不拥有它;如果需要保持,调用 retain,不需要时 release

操作 所有权 是否需要 release
[[Person alloc] init] 拥有
[Person new] 拥有
[obj copy] 拥有
[NSArray array](便利构造器) 不拥有(autoreleased) 不需要,如需保持则 retain
[dict objectForKey:] 不拥有 不需要,如需保持则 retain

3. MRC 示例

- (void)example {
    // 1. alloc 创建,拥有对象,引用计数 1
    Person *person = [[Person alloc] init];
    
    // 2. 便利构造器,不拥有,autoreleased
    Dog *dog = [Dog dogWithName:@"Wangcai"];
    
    // 3. 属性为 retain 时,setter 内部自动 retain
    //    属性为 assign 时,不 retain
    person.dog = dog; // retain 属性 → dog 引用计数 +1
    
    // 4. person 不再需要,release
    [person release]; // person 引用计数 0 → dealloc → dealloc 中 [_dog release] → dog 引用计数 -1
    // dog 是 autoreleased,由 autoreleasepool 在适当时候 release
}

MRC 下的 @property 与 setter

// .h
@property (nonatomic, retain) Dog *dog; // retain 属性

// .m(MRC 下需手动合成 setter)
- (void)setDog:(Dog *)dog {
    if (_dog != dog) {
        [_dog release];  // 释放旧值
        _dog = [dog retain]; // 保留新值
    }
}

- (void)dealloc {
    [_dog release]; // 必须在 dealloc 中释放 retain 的属性
    [super dealloc]; // MRC 下必须调用 [super dealloc]
}

MRC 下 @property(retain) 的 setter 会自动 retain(如果是合成的 setter),但 dealloc 中必须手动释放属性并调用 [super dealloc]

4. autorelease 与自动释放池

autorelease 将对象放入当前的自动释放池,pool 销毁时对池中所有对象调用 release

// 系统在 main 函数和 RunLoop 每次循环中自动创建 autoreleasepool
// 也可以手动创建,适用于循环中产生大量临时对象的场景
@autoreleasepool {
    for (NSInteger i = 0; i < 10000; i++) {
        // stringWithFormat: 返回 autoreleased 对象
        NSString *str = [NSString stringWithFormat:@"%ld", i];
        // 不手动 release,由 pool 在结束时统一 release
    }
} // pool 销毁,所有 autoreleased 对象 release

手动 @autoreleasepool 常用于循环内产生大量临时对象的场景,避免内存峰值过高。

5. 野指针与内存泄漏

问题 原因 后果
内存泄漏 该 release 的对象没有 release 对象无法销毁,内存持续占用
野指针(悬垂指针) 对象已 release,但指针未置 nil,仍指向已销毁内存 访问时 EXC_BAD_ACCESS 崩溃
过度释放 对对象 release 次数超过 retain 次数 引用计数异常,崩溃

MRC 下对象释放后指针不会自动置 nil,需手动置 nil:

Person *person = [[Person alloc] init];
[person release];
person = nil; // 手动置 nil,避免野指针

三、引用计数的存储

引用计数并非简单地存在对象的某个字段中,其存储方式与架构相关:

1. arm64:nonpointer isa

在 64 位架构下,isa 指针是一个优化过的共用体(nonpointer isa),部分位域存储引用计数:

位域 说明
nonpointer 1 表示优化过的 isa,0 表示原始指针
has_assoc 是否有关联对象
has_cxx_dtor 是否有 C++ 析构函数
shiftcls 类指针(33 位)
magic 调试用魔数
weakly_referenced 是否被 weak 引用
deallocating 是否正在析构
has_sidetable_rc 引用计数是否溢出到 SideTable
extra_rc 额外引用计数(存储引用计数 - 1)

extra_rc 可存储部分引用计数,当超出容量时,has_sidetable_rc 置 1,溢出部分存储在 SideTable 中。

2. SideTable

SideTable 是一个全局的哈希表结构,存储:

  • 引用计数表(RefcountMap):对象地址 → 引用计数
  • weak 引用表(weak_table_t):对象地址 → 所有 weak 指针的地址

对象销毁时,Runtime 遍历 weak 表,将所有指向该对象的 weak 指针置 nil。


四、CoreFoundation 桥接

Objective-C 对象(Foundation)与 CoreFoundation 对象(CF)可以互相桥接,ARC 只管理 Foundation 对象,CF 对象需手动管理:

桥接关键字 所有权转移 说明
__bridge 不转移 仅类型转换,双方各自管理
__bridge_transfer OC → CF OC 对象所有权转移给 CF,ARC 不再管理,CF 端需 CFRelease
__bridge_retained CF → OC CF 对象 retain 一次交给 ARC 管理,CF 端仍需 CFRelease 自己的引用
// __bridge:不转移所有权
NSString *ocStr = @"Hello";
CFStringRef cfStr = (__bridge CFStringRef)ocStr;
// 双方都不 release 对方,各自管理

// __bridge_transfer:OC → CF,CF 端负责释放
CFStringRef cfStr = (__bridge_transfer CFStringRef)[[NSString alloc] initWithString:@"Hello"];
// 使用完后必须 CFRelease(cfStr)

// __bridge_retained:CF → OC,ARC 管理 OC 端
CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, "Hello", kCFStringEncodingUTF8);
NSString *ocStr = (__bridge_retained NSString *)cfStr;
CFRelease(cfStr); // CF 端释放自己的引用,ocStr 由 ARC 管理

五、ARC 与 MRC 对比

维度 ARC MRC
管理方式 编译器自动插入 retain/release 开发者手动调用
引入版本 iOS 5+ iOS 早期
出错概率 低(循环引用仍需手动处理) 高(内存泄漏、野指针、过度释放)
代码量 多(每个对象需跟踪 retain/release)
性能 与 MRC 基本一致(编译器优化后) 手动优化空间大
dealloc 不调用 [super dealloc],成员变量自动释放 必须调用 [super dealloc],手动释放属性
weak 支持 支持(自动置 nil) 不支持(只能用 assign/unsafe_unretained)
与 Swift 兼容 Swift 使用 ARC 不兼容

混合编译

ARC 与 MRC 可以在同一项目中混合使用,通过编译标记控制:

  • -fobjc-arc:该文件使用 ARC
  • -fno-objc-arc:该文件使用 MRC

在 Xcode 的 Build Phases → Compile Sources 中为特定文件添加编译标记即可。


六、Swift 中的 ARC

Swift 使用 ARC,与 Objective-C ARC 兼容。Swift 提供 weakunowned 两种弱引用:

1. weak

class Person {
    let name: String
    weak var dog: Dog? // weak,对象销毁时自动置 nil
    init(name: String) { self.name = name }
    deinit { print("Person deinit") }
}

class Dog {
    let name: String
    weak var owner: Person? // weak 打破循环引用
    init(name: String) { self.name = name }
    deinit { print("Dog deinit") }
}

2. unowned

unowned 类似 unsafe_unretained,但更安全——不自动置 nil,但如果访问已释放的 unowned 引用会触发运行时断言(崩溃信息更明确)。适用于引用对象生命周期一定更长且不会为 nil 的场景:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
}

class CreditCard {
    let number: String
    unowned let customer: Customer // customer 生命周期更长,不会为 nil
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}
特性 weak unowned
是否可选 必须是可选类型(? 可以是非可选
对象销毁时 自动置 nil 不置 nil
访问已释放对象 安全(nil) 运行时断言崩溃
适用场景 引用可能为 nil 引用一定不为 nil 且生命周期更长

3. 闭包循环引用

class ViewController {
    var completion: (() -> Void)?
    
    func setup() {
        // [weak self] 捕获列表,避免循环引用
        completion = { [weak self] in
            guard let self = self else { return } // weak-strong dance
            self.doSomething()
        }
    }
    
    func doSomething() {}
}

七、总结

  • ARC 是编译器特性(辅以运行时支持),自动插入 retain/release,是现代 iOS/Swift 开发的默认方式。仍需手动处理循环引用(delegate 用 weak、Block 用 weak-strong dance、NSTimer 用 weak target)。
  • MRC 需手动管理引用计数,遵循黄金法则(alloc/new/copy 拥有的对象必须 release,其他方法获得的如需保持则 retain)。已被 ARC 取代,但理解 MRC 有助于理解 ARC 底层。
  • 引用计数存储:arm64 下部分存储在 isa 的 extra_rc 位域,溢出部分存储在全局 SideTable;weak 引用表也在 SideTable 中,对象销毁时自动将 weak 指针置 nil。
  • autoreleasepool:管理 autoreleased 对象的生命周期,系统在 RunLoop 循环中自动创建,循环内大量临时对象时可手动创建降低内存峰值。
  • CoreFoundation 桥接__bridge(不转移)、__bridge_transfer(OC→CF)、__bridge_retained(CF→OC),CF 对象需手动 CFRelease
  • Swift ARC:提供 weak(自动置 nil,可选)和 unowned(不置 nil,非可选,生命周期更长),闭包用 [weak self] 捕获列表避免循环引用。

posted @ 2015-07-28 22:45  Mr.陳  阅读(384)  评论(0)    收藏  举报