MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

掌握Objective-C中手动引用计数(MRC)的使用

2022-05-266.2k 阅读

什么是手动引用计数(MRC)

在Objective-C编程中,内存管理是一个至关重要的方面。手动引用计数(Manual Reference Counting,简称MRC)是Objective-C早期版本中使用的一种内存管理机制。与现代的自动引用计数(ARC,Automatic Reference Counting)不同,MRC要求开发者手动控制对象的内存生命周期,这意味着开发者需要明确地管理对象的创建、引用和释放。

在MRC环境下,每个对象都有一个与之关联的引用计数(reference count)。引用计数是一个整数值,用于记录有多少个变量或指针正在引用该对象。当对象被创建时,其引用计数被设置为1。每当有新的变量或指针开始引用该对象时,引用计数就会增加1;而当某个引用对象的变量或指针不再使用该对象时,引用计数就会减少1。当对象的引用计数减为0时,该对象所占用的内存就会被释放,系统回收这些内存供其他程序使用。

例如,考虑以下简单的Objective-C类定义:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject
@end

@implementation MyObject
@end

当我们在代码中创建这个类的实例时,就开始涉及到引用计数的管理。

MyObject *obj = [[MyObject alloc] init];
// 此时obj所指向的MyObject实例的引用计数为1

MRC中的内存管理方法

1. alloc、new、copy和mutableCopy方法

在MRC中,使用allocnewcopymutableCopy方法创建对象时,新创建的对象引用计数会自动设置为1。

alloc方法用于分配内存并返回一个未初始化的对象,通常紧接着会调用init方法进行初始化。例如:

MyObject *obj1 = [[MyObject alloc] init];
// obj1所指向的对象引用计数为1

new方法是一个便捷方法,它实际上是allocinit的组合。如下:

MyObject *obj2 = [MyObject new];
// obj2所指向的对象引用计数同样为1

copymutableCopy方法用于创建对象的副本。对于不可变对象(如NSStringNSArray等),copy方法会返回一个与原对象具有相同内容的不可变副本,新副本的引用计数为1。而mutableCopy方法会返回一个可变副本,其引用计数也为1。例如:

NSString *immutableString = @"Hello";
NSString *copiedString = [immutableString copy];
NSMutableString *mutableCopiedString = [immutableString mutableCopy];
// copiedString和mutableCopiedString的引用计数都为1

2. retain方法

retain方法用于增加对象的引用计数。当你希望延长某个对象的生命周期,使得在当前作用域结束后该对象仍然存在时,就可以使用retain方法。例如:

MyObject *obj = [[MyObject alloc] init];
MyObject *anotherObj = [obj retain];
// 此时obj和anotherObj指向的对象引用计数变为2

需要注意的是,使用retain方法后,你有责任在适当的时候释放这个对象,以避免内存泄漏。

3. release方法

release方法用于减少对象的引用计数。当你不再需要某个对象时,就应该调用release方法。例如:

MyObject *obj = [[MyObject alloc] init];
[obj release];
// 此时obj所指向的对象引用计数减为0,对象内存被释放

如果在对象引用计数已经为0的情况下再次调用release方法,程序会崩溃,因为此时已经没有对象可供释放,这种情况被称为过度释放(over - release)。

4. autorelease方法

autorelease方法也是用于减少对象的引用计数,但它不是立即减少,而是将对象添加到自动释放池(autorelease pool)中。当自动释放池被销毁时,池中的所有对象的release方法会被调用。例如:

MyObject *obj = [[[MyObject alloc] init] autorelease];
// 此时obj所指向的对象被添加到最近的自动释放池中,引用计数不会立即减1

自动释放池的作用是延迟对象的释放,在一些情况下可以提高性能,特别是在循环中创建大量临时对象时。例如:

for (int i = 0; i < 1000; i++) {
    NSString *tempString = [[NSString alloc] initWithFormat:@"Number %d", i];
    // 假设这里对tempString进行一些操作
    [tempString autorelease];
}
// 这里如果不使用autorelease,在循环中不断创建和释放对象会有较大开销,
// 使用autorelease后,这些对象会在自动释放池销毁时统一释放

MRC中的内存管理原则

1. 谁创建,谁释放

这是MRC中最基本的原则。如果你使用allocnewcopymutableCopy方法创建了一个对象,那么你就有责任释放这个对象。例如:

MyObject *obj = [[MyObject alloc] init];
// 因为使用了alloc方法创建对象,所以需要在适当时候释放
[obj release];

2. 谁retain,谁release

如果你对一个对象调用了retain方法,那么你必须在不再需要该对象时调用release方法,以平衡引用计数。例如:

MyObject *originalObj = [[MyObject alloc] init];
MyObject *retainedObj = [originalObj retain];
// 这里retainedObj对对象进行了retain,所以最后需要release
[retainedObj release];
[originalObj release];

3. 避免过度释放和悬空指针

过度释放如前文所述,是指对一个已经释放(引用计数为0)的对象再次调用release方法。这会导致程序崩溃,因为内存已经被回收,再次操作这块内存是非法的。悬空指针(dangling pointer)是指一个指针指向的对象已经被释放,但指针本身没有被设置为nil。如果后续代码尝试通过这个悬空指针访问对象,也会导致未定义行为。为了避免悬空指针,在释放对象后,应该将指向该对象的指针设置为nil。例如:

MyObject *obj = [[MyObject alloc] init];
[obj release];
obj = nil;
// 这样设置后,如果后续代码意外使用obj,不会导致崩溃,因为obj已经是nil

MRC在实际项目中的应用场景与示例

1. 简单对象的内存管理

假设我们有一个表示员工信息的类Employee

#import <Foundation/Foundation.h>

@interface Employee : NSObject {
    NSString *name;
    NSNumber *age;
}

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *age;

- (instancetype)initWithName:(NSString *)aName age:(NSNumber *)anAge;

@end

@implementation Employee

@synthesize name;
@synthesize age;

- (instancetype)initWithName:(NSString *)aName age:(NSNumber *)anAge {
    self = [super init];
    if (self) {
        self.name = aName;
        self.age = anAge;
    }
    return self;
}

- (void)dealloc {
    [name release];
    [age release];
    [super dealloc];
}

@end

在使用这个类时,我们需要遵循MRC的原则:

Employee *employee = [[Employee alloc] initWithName:@"John" age:@30];
// 这里使用alloc创建了employee对象,所以最后需要release
// 假设对employee进行一些操作
[employee release];

2. 集合类中的内存管理

在Objective-C中,集合类如NSArrayNSDictionary等也涉及到内存管理。当向集合类中添加对象时,集合类会对这些对象调用retain方法,增加其引用计数。当从集合类中移除对象或集合类本身被销毁时,集合类会对其中的对象调用release方法。例如:

NSMutableArray *array = [[NSMutableArray alloc] init];
Employee *employee1 = [[Employee alloc] initWithName:@"Alice" age:@25];
Employee *employee2 = [[Employee alloc] initWithName:@"Bob" age:@35];
[array addObject:employee1];
[array addObject:employee2];
// 此时array对employee1和employee2进行了retain
[employee1 release];
[employee2 release];
// 虽然这里对employee1和employee2调用了release,
// 但由于array持有它们,它们的引用计数不会减为0

// 假设不再需要array
[array release];
// 此时array会对其中的employee1和employee2调用release,
// 如果没有其他地方引用这两个对象,它们的引用计数会减为0并被释放

3. 内存管理与循环引用

循环引用(retain cycle)是MRC中一个常见且棘手的问题。当两个或多个对象相互持有对方的引用时,就会形成循环引用,导致对象无法被释放,造成内存泄漏。例如,假设有两个类ClassAClassB

#import <Foundation/Foundation.h>

@interface ClassB;

@interface ClassA : NSObject {
    ClassB *associatedB;
}

@property (nonatomic, retain) ClassB *associatedB;

@end

@interface ClassB : NSObject {
    ClassA *associatedA;
}

@property (nonatomic, retain) ClassA *associatedA;

@end

@implementation ClassA

@synthesize associatedB;

- (void)dealloc {
    [associatedB release];
    [super dealloc];
}

@end

@implementation ClassB

@synthesize associatedA;

- (void)dealloc {
    [associatedA release];
    [super dealloc];
}

@end

如果我们在代码中这样使用:

ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
a.associatedB = b;
b.associatedA = a;
// 此时a和b相互持有对方的引用,形成循环引用
[a release];
[b release];
// 由于循环引用,a和b的引用计数都不会减为0,导致内存泄漏

为了打破这种循环引用,我们可以将其中一个属性的内存管理策略改为assign(对于非对象类型,可以使用unsafe_unretained)。例如,修改ClassB的定义:

@interface ClassB : NSObject {
    ClassA *associatedA;
}

@property (nonatomic, assign) ClassA *associatedA;

@end

这样,当ab不再被其他地方引用时,a会释放b,而b不会持有a的强引用,从而打破循环引用,使内存得以正确释放。

MRC与ARC的对比

1. 开发效率

ARC(自动引用计数)大大提高了开发效率。在ARC环境下,编译器会自动插入必要的内存管理代码,开发者无需手动编写retainreleaseautorelease等方法。这减少了因手动管理不当而导致的内存泄漏和过度释放等错误,开发者可以将更多的精力放在业务逻辑的实现上。例如,在ARC环境下,前面的Employee类的使用代码可以简化为:

Employee *employee = [[Employee alloc] initWithName:@"John" age:@30];
// 这里无需手动调用release,ARC会自动处理内存释放

而在MRC中,开发者需要时刻关注对象的创建和释放,代码编写相对繁琐。

2. 性能

在性能方面,MRC和ARC各有优劣。MRC在某些特定场景下,开发者可以根据具体需求精细地控制内存释放时机,从而优化性能。例如,在对性能要求极高的游戏开发或底层库开发中,手动管理内存可以避免不必要的内存开销。然而,ARC也在不断优化,现代编译器在插入内存管理代码时能够进行一些优化,减少性能损失。并且,ARC减少了因手动管理不当导致的性能问题,如内存泄漏造成的内存碎片化等。

3. 代码可读性和维护性

ARC使代码更加简洁,提高了代码的可读性和维护性。因为无需手动编写大量的内存管理代码,代码逻辑更加清晰,易于理解和修改。而MRC代码中充斥着大量的retainrelease等方法调用,增加了代码的复杂性,使得代码阅读和维护难度加大。例如,在MRC中处理复杂对象关系时,追踪对象的引用计数变化和内存释放逻辑可能会非常困难。

虽然ARC已经成为Objective-C开发中的主流内存管理方式,但了解MRC对于深入理解Objective-C的内存管理机制仍然具有重要意义。它帮助开发者更好地掌握内存管理的本质,在一些特殊场景下,如与旧代码库集成或对性能有极高要求的场景中,能够做出更合适的决策。同时,理解MRC也有助于在遇到ARC无法处理的内存问题时,能够从底层原理出发进行调试和解决。