掌握Objective-C中手动引用计数(MRC)的使用
什么是手动引用计数(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中,使用alloc
、new
、copy
和mutableCopy
方法创建对象时,新创建的对象引用计数会自动设置为1。
alloc
方法用于分配内存并返回一个未初始化的对象,通常紧接着会调用init
方法进行初始化。例如:
MyObject *obj1 = [[MyObject alloc] init];
// obj1所指向的对象引用计数为1
new
方法是一个便捷方法,它实际上是alloc
和init
的组合。如下:
MyObject *obj2 = [MyObject new];
// obj2所指向的对象引用计数同样为1
copy
和mutableCopy
方法用于创建对象的副本。对于不可变对象(如NSString
、NSArray
等),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中最基本的原则。如果你使用alloc
、new
、copy
或mutableCopy
方法创建了一个对象,那么你就有责任释放这个对象。例如:
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中,集合类如NSArray
、NSDictionary
等也涉及到内存管理。当向集合类中添加对象时,集合类会对这些对象调用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中一个常见且棘手的问题。当两个或多个对象相互持有对方的引用时,就会形成循环引用,导致对象无法被释放,造成内存泄漏。例如,假设有两个类ClassA
和ClassB
:
#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
这样,当a
和b
不再被其他地方引用时,a
会释放b
,而b
不会持有a
的强引用,从而打破循环引用,使内存得以正确释放。
MRC与ARC的对比
1. 开发效率
ARC(自动引用计数)大大提高了开发效率。在ARC环境下,编译器会自动插入必要的内存管理代码,开发者无需手动编写retain
、release
和autorelease
等方法。这减少了因手动管理不当而导致的内存泄漏和过度释放等错误,开发者可以将更多的精力放在业务逻辑的实现上。例如,在ARC环境下,前面的Employee
类的使用代码可以简化为:
Employee *employee = [[Employee alloc] initWithName:@"John" age:@30];
// 这里无需手动调用release,ARC会自动处理内存释放
而在MRC中,开发者需要时刻关注对象的创建和释放,代码编写相对繁琐。
2. 性能
在性能方面,MRC和ARC各有优劣。MRC在某些特定场景下,开发者可以根据具体需求精细地控制内存释放时机,从而优化性能。例如,在对性能要求极高的游戏开发或底层库开发中,手动管理内存可以避免不必要的内存开销。然而,ARC也在不断优化,现代编译器在插入内存管理代码时能够进行一些优化,减少性能损失。并且,ARC减少了因手动管理不当导致的性能问题,如内存泄漏造成的内存碎片化等。
3. 代码可读性和维护性
ARC使代码更加简洁,提高了代码的可读性和维护性。因为无需手动编写大量的内存管理代码,代码逻辑更加清晰,易于理解和修改。而MRC代码中充斥着大量的retain
、release
等方法调用,增加了代码的复杂性,使得代码阅读和维护难度加大。例如,在MRC中处理复杂对象关系时,追踪对象的引用计数变化和内存释放逻辑可能会非常困难。
虽然ARC已经成为Objective-C开发中的主流内存管理方式,但了解MRC对于深入理解Objective-C的内存管理机制仍然具有重要意义。它帮助开发者更好地掌握内存管理的本质,在一些特殊场景下,如与旧代码库集成或对性能有极高要求的场景中,能够做出更合适的决策。同时,理解MRC也有助于在遇到ARC无法处理的内存问题时,能够从底层原理出发进行调试和解决。