深入理解Objective-C内存管理和自动引用计数
在Objective-C中,内存管理是程序开发中不可或缺的一部分,而自引用计数(ARC)是一种自动化的内存管理技术。本文将深入探讨自引用计数的原理、内存管理的思考方式以及Objective-C中常用的内存管理方法。
一. 内存管理/引用计数 初理解:
1. 自引用计数的原理
1.1 引用计数概述
自引用计数是一种技术,用于管理对象的引用计数,即对象被引用的次数。当一个对象的引用计数大于0时,表示该对象被持有,不可被释放;当引用计数为0时,表示对象需要被释放。
引用计数的基本原理是,创建一个对象时,其引用计数为1,每当有一个新的指针指向该对象,引用计数加1。当某个指针不再指向该对象时,引用计数减1。当对象的引用计数变为0时,系统将销毁对象,回收内存。
1.2 操作示例
举例来说明引用计数的操作:
// 生成并持有对象
id obj = [[NSObject alloc] init];
// 持有对象
[obj retain];
// 释放对象
[obj release];
// 废弃对象
[obj dealloc];
1.3 内存管理思考方式
在Objective-C中,开发者需要根据对象的生成和使用情况,合理地操作内存。以下是一些常见的内存管理操作:
- 生成并持有对象:使用
alloc/new/copy/mutableCopy
等方法。 - 持有对象:使用
retain
方法。 - 释放对象:使用
release
方法。 - 废弃对象:使用
dealloc
方法。
对于自己生成并持有的对象,需要负责释放;对于非自己生成的对象,也可以通过retain
方法持有,但要确保在不再需要时释放。
2. alloc/retain/release/dealloc实现
Objective-C中的内存管理方法实际上是通过一系列函数和结构体来实现的。以下是简化版的GNUstep源代码,展示了alloc
、retain
、release
和dealloc
的实现方式:
// NSObject.m
+ (id)alloc {
return [self allocWithZone:NSDefaultMallocZone()];
}
+ (id)allocWithZone:(NSZone *)zone {
return NSAllocateObject(self, 0, zone);
}
struct obj_layout {
NSUInteger retained;
};
inline id NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) {
int size = calculateSizeForObject(aClass, extraBytes);
id new = NSZoneMalloc(zone, size);
memset(new, 0, size);
return (id)&((struct obj_layout *)new)[1];
}
- (NSInteger)retainCount {
return NSExtraRefCount(self) + 1;
}
inline NSUInteger NSExtraRefCount(id anObject) {
return ((struct obj_layout *)anObject)[-1].retained;
}
- (id)retain {
NSIncrementExtraRefCount(self);
return self;
}
inline void NSIncrementExtraRefCount(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1)
[NSException raise:NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"];
((struct obj_layout *)anObject)[-1].retained++;
}
- (void)release {
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL NSDecrementExtraRefCountWasZero(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_layout *)anObject)[-1].retained--;
return NO;
}
}
- (void)dealloc {
NSDeallocateObject(self);
}
inline void NSDeallocateObject(id anObject) {
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
free(o);
}
3. autorelease实现
在Objective-C中,autorelease
实际上是通过调用NSAutoreleasePool
对象的addObject
方法实现的。以下是autorelease
的源代码示例:
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
+ (void)addObject:(id)anObj {
NSAutoreleasePool *pool = getCurrentAutoreleasePool();
if (pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"NSAutoreleasePool object called autorelease outside its existence.");
}
}
- (void)drain {
[self dealloc];
}
- (void)dealloc {
[self emptyPool];
[array release];
}
- (void)emptyPool {
for (id obj in array) {
[obj release];
}
}
二. ARC规则详解:
Objective-C作为一门面向对象的语言,其内存管理一直是开发者关注的焦点之一。为了更有效地处理对象,引入了ARC(Automatic Reference Counting)机制。ARC的引入使得内存管理更加方便,但也需要遵守一些规则。在本文中,我们将深入探讨ARC的一些规则和修饰符。
1. 所有权修饰符
在Objective-C中,为了处理对象,可以将变量类型定义为id
类型或各种对象类型。所有权修饰符用于在ARC有效时更精确地管理对象的所有权。共有四种所有权修饰符:
__strong
修饰符__weak
修饰符__unsafe_unretained
修饰符__autoreleasing
修饰符
1.1 __strong
修饰符
__strong
修饰符是id
类型和对象类型默认的所有权修饰符。在没有明确指定所有权修饰符时,默认为__strong
修饰符。示例代码如下:
id obj = [[NSObject alloc] init];
// Equivalent to
id __strong obj = [[NSObject alloc] init];
在ARC无效时,需要手动管理内存,如下所示:
// When ARC is not enabled
id obj = [[NSObject alloc] init];
[obj release];
__strong
修饰符表示对对象的强引用,变量在超出其作用域时,强引用失效,引用对象会随之释放。
1.2 __weak
修饰符
使用__weak
修饰符可以避免循环引用的问题。__weak
提供了弱引用,不持有对象实例。
{
// Generating and holding an object
id __strong obj0 = [[NSObject alloc] init];
// Variable obj1 holds a weak reference to the generated object
id __weak obj1 = obj0;
}
// obj0 goes out of scope, weak reference obj1 becomes nil, and the object is released.
__weak
修饰符的变量在超出其作用域时,对象即被释放。另外,__weak
修饰符的变量在持有的对象被废弃时,弱引用将自动失效并置为nil
。
1.3 __unsafe_unretained
修饰符
与__weak
类似,__unsafe_unretained
修饰符的变量也不持有对象实例。然而,两者之间有差异。
{
// Generating and holding an object
id __strong obj0 = [[NSObject alloc] init];
// Variable obj1 holds an unsafe unretained reference to the generated object
id __unsafe_unretained obj1 = obj0;
}
// obj0 goes out of scope, unsafe unretained reference obj1 still points to the object (dangling pointer).
__unsafe_unretained
修饰符的变量不持有对象,因此在超出其变量作用域时,对象不会被释放,可能导致悬垂指针错误。
1.4 __autoreleasing
修饰符
__autoreleasing
修饰符用于替代NSAutoreleasePool
,在ARC有效时通过@autoreleasepool
块进行对象的自动释放。
@autoreleasepool {
// Generating and holding a non-self-owned object
id __strong obj = [NSMutableArray array];
// obj is automatically registered in the autorelease pool by the compiler
}
不使用__autoreleasing
修饰符也可以实现对象的注册到autorelease pool。
+ (id)array {
id obj = [[NSMutableArray alloc] init];
return obj;
}
// Compiler automatically registers the returned object in the autorelease pool.
2. 规则
在ARC有效的情况下,必须遵守一定的规则,如下所示:
- 不能使用
retain
/release
/retainCount
/autorelease
- 不能使用
NSAllocateObject
/NSDeallocateObject
- 须遵守内存管理的方法命名规则
- 不要显式调用
dealloc
- 使用
@autoreleasepool
块替代NSAutoreleasePool
- 不能使用
NSZone
- 对象型变量不能作为C语言结构体(
struct
/union
)的成员 - 显式转换
id
和void*
2.1 不能使用retain
/release
/retainCount
/autorelease
在ARC有效时,无需再次键入这些方法的代码。以下代码在ARC下会引起编译错误。
for (;;) {
NSUInteger count = [obj retainCount];
[obj release];
if (count == 1)
break;
}
只有在ARC无效时,需要手动进行内存管理时才能使用这些方法。
2.2 不要显式调用dealloc
不论ARC是否有效,只要对象的所有者都不持有该对象,对象就会被废弃。在废弃对象时,无论ARC是否有效,都会调用对象的dealloc
方法。
- (void)dealloc {
free(buffer_);
}
dealloc
方法通常适用于删除已注册的代理或观察者对象等操作。
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
在ARC无效时,需要在dealloc
方法中显式调用[super dealloc]
。
// When ARC is not enabled
- (void)dealloc {
/* Object-specific cleanup */
[super dealloc];
}
2.3 使用@autoreleasepool
块替代NSAutoreleasePool
在ARC有效时,使用@autoreleasepool
块替代NSAutoreleasePool
。
2.4
不能使用NSZone
在ARC有效时,不能使用NSZone
。
id obj = NSAllocateObject([MyClass class], 0, NULL);
2.5 对象型变量不能作为C语言结构体(struct
/union
)的成员
在ARC有效时,对象型变量不能作为C语言结构体的成员。
struct MyStruct {
id obj; // Error: ARC forbids Objective-C objects in struct
};
2.6 显式转换id
和void*
在ARC有效时,不允许直接进行id
和void*
之间的显式转换。
id obj = (__bridge id)someVoidPointer; // Error: ARC forbids explicit message send of 'release'
需要使用__bridge
进行类型转换,以确保正确处理引用计数。
id obj = (__bridge_transfer id)someVoidPointer; // The ownership is transferred to ARC
3. 属性
在使用ARC时,Objective-C类的属性声明中的所有权修饰符会有所改变。
@property (nonatomic, strong) NSString *name;
当ARC有效时,可以使用以下所有权修饰符来声明属性:
assign
: 使用__unsafe_unretained
修饰符copy
: 使用__strong
修饰符(但是赋值的是被复制的对象)retain
: 使用__strong
修饰符strong
: 使用__strong
修饰符unsafe_unretained
: 使用__unsafe_unretained
修饰符weak
: 使用__weak
修饰符
在属性声明中使用这些修饰符相当于将值赋给相应所有权修饰符的变量。
此外,在声明类成员变量时,如果与属性声明中的属性不一致,将引起编译错误。
4. 数组
除了__unsafe_unretained
修饰符之外的__strong
/__weak
/__autoreleasing
修饰符会确保其指定的变量初始化为nil
。
考虑以下例子:
{
id objs[2];
objs[0] = [[NSObject alloc] init];
objs[1] = [NSMutableArray array];
}
当数组超出其变量作用域时,附有__strong
修饰符的变量会失效,其强引用消失,所赋值的对象也随之释放。然而,在C语言的动态数组中,也可以使用附有__strong
修饰符的变量,只是需要遵守一些事项。以下按顺序说明。
首先,声明动态数组的指针:
id __strong *array = nil;
其次,使用calloc
函数确保为附有__strong
修饰符的变量分配的内存块具有正确的容量:
array = (id __strong *)calloc(entries, sizeof(id));
此源代码分配了entries
个所需的内存块。如果不使用calloc
,可以使用malloc
并通过memset
等函数将内存填充为0。
通过使用calloc
分配的动态数组可以像静态数组一样完全使用。
然而,如果只是简单地使用free
函数废弃了数组使用的内存块,在动态数组中释放的对象不会再次释放,从而导致内存泄漏:
free(array);
因为在静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器无法确定数组的生存周期,因此无法处理。
三. ARC实现
笔者个人觉得这部分写博客的意义不大,想具体了解可以自行阅读《Objective - C高级编程》第一章1.4ARC实现部分,相信读者会有更大的收获,与君共勉!
结论
ARC简化了Objective-C的内存管理,但使用ARC并不意味着完全不用关心内存管理。开发者仍需理解ARC的规则和修饰符,以确保代码的正确性和性能。通过遵循ARC规则,可以更轻松地处理对象的所有权,提高代码的可读性和可维护性。