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

Objective-C手动引用计数(MRC)实战指南

2021-04-177.3k 阅读

一、Objective-C 内存管理基础概念

在深入探讨手动引用计数(MRC)之前,我们先来回顾一些 Objective-C 内存管理的基本概念。

Objective-C 使用引用计数(Reference Counting)机制来管理对象的内存。每个对象都有一个与之关联的引用计数,它表示当前有多少个变量正在引用该对象。当一个对象的引用计数降为 0 时,系统会自动释放该对象所占用的内存空间。

1.1 对象的创建与引用计数

在 Objective-C 中,我们通过 alloc 方法来创建一个新的对象。例如:

NSObject *obj = [[NSObject alloc] init];

当执行 alloc 方法时,系统会为 NSObject 分配内存,并将其引用计数初始化为 1。这意味着当前有一个变量 obj 正在引用这个对象。

1.2 引用计数的增减操作

  • 增加引用计数(Retain):当我们希望一个对象的生命周期延长,使其不会过早被释放时,可以调用对象的 retain 方法。例如:
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [obj1 retain];

这里,obj2 通过 retain 方法增加了 obj1 所指向对象的引用计数。此时,该对象的引用计数变为 2,有 obj1obj2 两个变量引用它。

  • 减少引用计数(Release):当我们不再需要对某个对象的引用时,应该调用 release 方法来减少其引用计数。例如:
[obj1 release];

执行此操作后,obj1 所指向对象的引用计数减 1。如果引用计数变为 0,系统会自动释放该对象的内存。

二、手动引用计数(MRC)的规则

在手动引用计数环境下,开发者需要严格遵循一些规则来正确管理对象的内存,以避免内存泄漏和悬空指针等问题。

2.1 谁创建,谁释放

如果通过 allocnewcopy 等方法创建了一个对象,那么就有责任调用 releaseautorelease 来释放该对象。例如:

NSString *str = [[NSString alloc] initWithFormat:@"Hello, MRC"];
// 使用 str
[str release];

这里通过 alloc 方法创建了 NSString 对象,所以在使用完毕后需要调用 release 方法来释放内存。

2.2 谁 retain,谁 release

当对一个对象调用 retain 方法增加其引用计数后,必须在适当的时候调用 release 方法来平衡引用计数。例如:

NSArray *array = [[NSArray alloc] initWithObjects:@"One", @"Two", nil];
NSArray *retainedArray = [array retain];
// 使用 retainedArray
[retainedArray release];
[array release];

在这个例子中,retainedArrayarray 所指向的数组对象调用了 retain 方法,所以在使用完 retainedArray 后,需要调用 release 方法。同时,array 是通过 alloc 创建的,也需要调用 release 方法。

2.3 自动释放池(Autorelease Pool)

自动释放池是 MRC 中一个重要的概念。当一个对象发送 autorelease 消息时,它会被添加到最近的自动释放池中。自动释放池在适当的时候(例如当自动释放池被销毁时)会向池中的所有对象发送 release 消息。

在 iOS 应用程序中,主线程默认会有一个自动释放池,它会在每次事件循环结束时被销毁并重新创建。我们也可以手动创建自动释放池,例如:

@autoreleasepool {
    NSString *str = [[[NSString alloc] initWithFormat:@"Temp String"] autorelease];
    // 在自动释放池销毁时,str 会收到 release 消息
}

在这个代码块中,str 对象发送了 autorelease 消息,被添加到自动释放池中。当自动释放池块结束时,str 的引用计数会减少。

三、MRC 实战场景分析

3.1 类的成员变量(实例变量)管理

在一个类中,成员变量(实例变量)的内存管理需要特别注意。假设我们有一个简单的类 Person,其中包含一个 NSString 类型的成员变量 name

@interface Person : NSObject {
    NSString *name;
}
@property (nonatomic, retain) NSString *name;
- (instancetype)initWithName:(NSString *)aName;
@end

@implementation Person
@synthesize name;

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

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

initWithName: 方法中,我们使用 self.name = aName,由于 name 属性声明为 retain,所以会对传入的 aName 进行 retain 操作,增加其引用计数。在 dealloc 方法中,我们需要手动调用 [name release] 来减少引用计数,以确保对象销毁时内存被正确释放。

3.2 方法返回值的内存管理

当一个方法返回一个对象时,需要遵循 MRC 的规则来确保返回对象的内存管理正确。例如,我们有一个方法 createString

- (NSString *)createString {
    NSString *str = [[NSString alloc] initWithFormat:@"New String"];
    return [str autorelease];
}

在这个方法中,我们通过 alloc 创建了 NSString 对象,然后调用 autorelease 方法,将其添加到自动释放池中。这样,调用该方法的代码不需要手动释放返回的字符串对象,因为自动释放池会在适当的时候处理它。

3.3 集合类(如 NSArray、NSDictionary)的内存管理

在处理集合类时,内存管理也有其特点。当向集合类(如 NSArrayNSDictionary)中添加对象时,集合类会对添加的对象进行 retain 操作。例如:

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSArray *array = [[NSArray alloc] initWithObjects:obj1, obj2, nil];
[obj1 release];
[obj2 release];
// 使用 array
[array release];

在创建 NSArray 时,它会对 obj1obj2 进行 retain 操作。所以在添加完对象后,obj1obj2 可以调用 release 方法,因为数组已经持有了它们的引用。当不再使用 array 时,调用 release 方法释放数组及其内部对象的引用。

四、MRC 中的常见错误及解决方法

4.1 内存泄漏(Memory Leak)

内存泄漏是 MRC 中最常见的问题之一。当一个对象的引用计数没有被正确减少,导致对象无法被释放时,就会发生内存泄漏。例如:

NSObject *obj = [[NSObject alloc] init];
// 忘记调用 release

在这个例子中,obj 是通过 alloc 创建的,但没有调用 release 方法,导致该对象一直占用内存,无法被释放。

解决方法是在使用完对象后,确保调用 releaseautorelease 方法。例如:

NSObject *obj = [[NSObject alloc] init];
// 使用 obj
[obj release];

4.2 悬空指针(Dangling Pointer)

悬空指针是指一个指针指向的内存已经被释放,但指针本身没有被设置为 nil。例如:

NSObject *obj = [[NSObject alloc] init];
[obj release];
// 此时 obj 成为悬空指针
// 错误操作,可能导致程序崩溃
[obj doSomething];

解决方法是在释放对象后,将指针设置为 nil。例如:

NSObject *obj = [[NSObject alloc] init];
[obj release];
obj = nil;

这样,当再次尝试访问 obj 时,不会导致程序崩溃,因为 nil 发送消息是安全的。

4.3 过度释放(Over - release)

过度释放是指对一个对象多次调用 release 方法,导致其引用计数变为负数,从而引发未定义行为。例如:

NSObject *obj = [[NSObject alloc] init];
[obj release];
[obj release]; // 过度释放

解决方法是确保对每个对象的 release 调用次数与 allocretain 的次数相匹配。可以通过仔细跟踪对象的创建、引用和释放过程来避免过度释放。

五、在复杂场景下应用 MRC

5.1 多层对象嵌套的内存管理

当存在多层对象嵌套时,内存管理会变得更加复杂。例如,有一个 Company 类,它包含一个 Department 数组,每个 Department 又包含多个 Employee 对象。

@interface Employee : NSObject {
    NSString *name;
}
@property (nonatomic, retain) NSString *name;
- (instancetype)initWithName:(NSString *)aName;
@end

@implementation Employee
@synthesize name;

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

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

@interface Department : NSObject {
    NSString *departmentName;
    NSMutableArray *employees;
}
@property (nonatomic, retain) NSString *departmentName;
@property (nonatomic, retain) NSMutableArray *employees;
- (instancetype)initWithDepartmentName:(NSString *)aName;
- (void)addEmployee:(Employee *)employee;
@end

@implementation Department
@synthesize departmentName, employees;

- (instancetype)initWithDepartmentName:(NSString *)aName {
    self = [super init];
    if (self) {
        self.departmentName = aName;
        self.employees = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)addEmployee:(Employee *)employee {
    [employees addObject:employee];
}

- (void)dealloc {
    [departmentName release];
    [employees release];
    [super dealloc];
}
@end

@interface Company : NSObject {
    NSString *companyName;
    NSMutableArray *departments;
}
@property (nonatomic, retain) NSString *companyName;
@property (nonatomic, retain) NSMutableArray *departments;
- (instancetype)initWithCompanyName:(NSString *)aName;
- (void)addDepartment:(Department *)department;
@end

@implementation Company
@synthesize companyName, departments;

- (instancetype)initWithCompanyName:(NSString *)aName {
    self = [super init];
    if (self) {
        self.companyName = aName;
        self.departments = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)addDepartment:(Department *)department {
    [departments addObject:department];
}

- (void)dealloc {
    [companyName release];
    [departments release];
    [super dealloc];
}
@end

在这个复杂的对象结构中,每个类都需要正确管理其成员变量的内存。Company 类需要在 dealloc 方法中释放 companyNamedepartmentsDepartment 类需要释放 departmentNameemployees,而 Employee 类需要释放 name。同时,在添加对象到集合类(如 NSMutableArray)时,也要注意引用计数的变化。

5.2 循环引用(Retain Cycle)的处理

循环引用是 MRC 中一个棘手的问题。当两个或多个对象相互持有对方的强引用(通过 retain)时,就会形成循环引用,导致对象无法被释放。例如:

@interface ObjectA : NSObject {
    ObjectB *objectB;
}
@property (nonatomic, retain) ObjectB *objectB;
@end

@interface ObjectB : NSObject {
    ObjectA *objectA;
}
@property (nonatomic, retain) ObjectA *objectA;
@end

@implementation ObjectA
@synthesize objectB;
- (void)dealloc {
    [objectB release];
    [super dealloc];
}
@end

@implementation ObjectB
@synthesize objectA;
- (void)dealloc {
    [objectA release];
    [super dealloc];
}
@end

在这个例子中,ObjectA 持有 ObjectB 的强引用,ObjectB 又持有 ObjectA 的强引用,形成了循环引用。解决循环引用的方法通常是打破其中一个强引用,例如将其中一个属性声明为 assign(在非 ARC 环境下)或 weak(在 ARC 环境下)。在 MRC 中,我们可以将其中一个属性改为 assign

@interface ObjectA : NSObject {
    ObjectB *objectB;
}
@property (nonatomic, assign) ObjectB *objectB;
@end

@interface ObjectB : NSObject {
    ObjectA *objectA;
}
@property (nonatomic, retain) ObjectA *objectA;
@end

这样,ObjectAObjectB 的引用不再增加 ObjectB 的引用计数,从而打破了循环引用。

六、MRC 与 ARC 的对比

6.1 ARC 的优势

自动引用计数(ARC)是 Xcode 4.2 引入的一项重大改进,它大大简化了内存管理。与 MRC 相比,ARC 具有以下优势:

  • 减少错误:ARC 自动处理对象的引用计数增减,开发者无需手动调用 retainreleaseautorelease 方法,从而减少了因手动管理不当导致的内存泄漏、悬空指针和过度释放等错误。
  • 提高开发效率:开发者可以将更多的精力放在业务逻辑上,而不是繁琐的内存管理上,提高了开发效率。

6.2 MRC 的适用场景

虽然 ARC 带来了很多便利,但在某些情况下,MRC 仍然有其适用场景:

  • 旧项目维护:对于一些早期开发的项目,可能没有升级到 ARC 的条件,此时仍然需要在 MRC 环境下进行维护。
  • 深入理解内存管理:学习 MRC 可以帮助开发者深入理解 Objective - C 的内存管理机制,对于理解 ARC 的原理以及在复杂场景下优化内存使用有很大帮助。

七、MRC 调试技巧

7.1 使用 Instruments 工具

Instruments 是 Xcode 提供的一款强大的性能分析工具,在 MRC 环境下,它可以帮助我们检测内存泄漏和分析内存使用情况。

  • 检测内存泄漏:通过 Instruments 中的Leaks模板,可以运行应用程序并实时检测内存泄漏。当发现泄漏时,Leaks 工具会显示泄漏对象的详细信息,包括对象类型、创建位置等,帮助我们定位问题。
  • 分析内存使用:使用 Instruments 中的Allocations模板,可以查看应用程序在运行过程中的内存分配情况,包括对象的创建、销毁时间和数量等,有助于优化内存使用。

7.2 日志输出与断点调试

在代码中添加日志输出语句,记录对象的创建、retainrelease 等操作,可以帮助我们跟踪对象的引用计数变化。例如:

NSObject *obj = [[NSObject alloc] init];
NSLog(@"Object created, retain count: %lu", (unsigned long)[obj retainCount]);
NSObject *retainedObj = [obj retain];
NSLog(@"Object retained, retain count: %lu", (unsigned long)[obj retainCount]);
[retainedObj release];
NSLog(@"Object released, retain count: %lu", (unsigned long)[obj retainCount]);
[obj release];

同时,结合断点调试,可以在关键的内存管理代码处设置断点,观察对象的状态和引用计数变化,以便更准确地找出内存管理问题。

通过以上对 Objective - C 手动引用计数(MRC)的深入探讨和实战指南,希望开发者能够熟练掌握 MRC 的原理和应用,在实际开发中正确管理内存,编写出高效、稳定的 Objective - C 程序。无论是在旧项目维护还是深入学习内存管理机制方面,MRC 的知识都具有重要的价值。同时,了解 MRC 与 ARC 的对比以及 MRC 的调试技巧,也有助于我们更好地适应不同的开发场景和解决开发过程中遇到的问题。