【iOS】自动引用计数(一)
自动引用计数
自动引用计数原理
Objective-C中的内存管理就是引用计数,而自动引用计数则是指内存管理中对引用采取自动计数的技术。自动引用计数技术用于管理对象的引用计数,也就是对象被引用的次数。
当一个对象的引用计数大于0时,表示该对象被持有,不可被释放;当某个指针不再指向该对象时,引用计数减1;当对象的引用计数变为0时,系统将销毁对象,回收内存。

正如苹果的官方说明,在新一代Apple LLVM编译器中,编译器将自动进行内存管理。
内存管理
Cocoa框架中Foundation框架类库的NSObject类担负内存管理的职责,这里先使用一个图直观的感受如何使用引用计数管理对象的内存:

简而言之,引用计数仅仅只是"生成"、"持有"、"释放"、"废弃"这四个词的操作:

自己生成的对象自己持有
编程人员自己生成对象的方法有:
- alloc类方法:指向生成并持有对象的指针被赋给变量。
- new类方法:与alloc类方法完全一致。
- copy方法:基于NSCopying方法约定,由实现 copyWithZone: 方法生成并持有不可变对象的副本。
- mutableCopying方法:基于NSMutableCopying方法约定,由实现 mutableCopyWithZone: 方法生成并持有可变对象的副本。
非自己生成的对象自己也能持有
上述方法外的方法取得的对象,编程人员自己不是该对象的持有者,变量可以使用retain方法来持有对象。
objc
id obj = [NSMutableArray array];
[obj retain];
不过现在的 Objective-C 已经引入了自动引用计数(ARC)。在启动ARC的情况下,我们已不再需要手动管理内存,不在使用 retain 和 release 方法了,因此我们输入时是找不到这个方法的。
不需要自己持有的对象时释放
自己持有的对象,一旦不再需要,持有者有义务使用 release 方法释放该对象。
objc
id obj = [NSMutableArray array];
[obj release];
对象一经释放绝对不可访问。因为ARC的启动,release 方法我们同样找不到。
但是,我们想使得获取的对象存在,但自己不持有,需要使用autorelease方法。autorelease方法使得对象在超出指定的生存范围时能够自动并正确地释放。
objc
id obj = [[NSObject alloc] init];
[obj autorelease];
这里我们通过一个图直观地对比一下 release 和 autorelease 方法的区别:

无法释放非自己持有的对象
对于持有者是自己的对象,在不需要时需要将其释放。而除此之外的对象绝不能释放,倘若在程序中释放了非自己所持有的对象就会造成崩溃。
例如:释放之后再次释放已非自己持有的对象
objc
id obj = [[NSObject alloc] init];
[obj autorelease];
[obj autorelease];
alloc/retain/release/dealloc实现
介于包含NSObject类的Foudation框架并没有公开,部分源代码没有公开。为此,我们首先使用开源软件GNUstep来说明。
- alloc
我们直接来看一下去掉NSZone后简化了的源代码:

alloc类方法用struct obj_layout中的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0后返回。

- retain
这里先认识一下retainCount,通过retainCount实例方法可获得对象的引用计数。
执行alloc后的对象的retainCount是"1"。因为分配是全部置0,所以retained为0 。由NSExtraRefCount(self) + 1得出retainCount为1。

由对象寻址找到对象内存头部,从而访问其中的retained变量。
我们再通过源代码就看出retain方法会使retained变量加1 。
这里虽然写入了retained变量超出最大值时发生异常的代码,但实际上只运行了使retained加1的retained++的代码。
- release
- 当retained变量大于0时,release实例方法使retained变量减1 。

- 当retained变量等于0时调用dealloc实例方法,废弃对象。

值得注意的是:上述代码仅废弃由alloc分配的内存块。
总结一下:
- 在Objective-C的对象中存有引用计数这一整数值。
- 调用alloc或retain方法后,引用计数值加1。
- 调用release后,引用计数值减1。
- 引用计数值为0时,调用dealloc方法废弃对象。
苹果是采用散列表(引用计数表)来管理引用计数的。
autorelease
认识autorelease
首先复习一下C语言的自动变量。当程序执行时,若某自动变量超出其作用域,该自动变量将被自动废弃。

autorelease会像C语言的自动变量那样来对待对象实例。当超出变量作用域时,对象实例的release实例方法被调用。初次之外,与C语言的自动变量不同的是,编程人员可以设定变量的作用域。
使用autorelease
autorelease具体使用方法:
- 生成并持有NSAutoreleasePool对象。
- 调用已分配对象的autorelease实例对象。
- 废弃NSAutoreleasePool对象。
这里首先认识一下NSAutoreleasePool对象的生存周期。

NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。

在Cocoa框架中,Cocoa程序都有一个主事件循环(RunLoop),程序会一直在循环处理事件,而不是一运行就退出。在每次循环中,系统就会自动管理(创建、使用、销毁)一个自动释放池,以释放autorelease的对象。

但在大量产生autorelease的对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。
例如读入大量图像的同时改变其尺寸,这种情况下会大量产生autorelease的对象,外面只有一个大的autoreleasepool,那么在循环中产生的所有对象要等到循环结束后才释放,这样每张图片的内存都暂时保留在内存里,很容易爆内存。

在此情况下,有必要在适当的地方生成、持有或废弃NSAutoreleasePool对象。在这个例子中,也就是在循环中手动创建小的@autoreleasepool块。

这样。每次循环结束时,池子就被清空,该释放的对象马上释放,内存使用量会保持稳定。
另外,Cocoa框架中也有很多类方法用于返回autorelease的对象。
objc
id array = [NSMutableArray arrayWithCapacity:1];
这个代码等同于源代码:

实现autorelease
autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法,addObject类方法调用正在使用的NSAutoreleasePool对象的addObject实例方法。
如果嵌套生成或持有NSAutoreleasePool对象,调用NSObject类的autorelease实例方法时,该对象将被追加到正在使用的NSAutoreleasePool对象中的数组里。
追加一个问题:如果autorelease了NSAutoreleasePool对象会如何?
答案是发生异常。这是因为无论调用哪一个对象的autorelease实例方法,实际上调用的都是NSObject类的autorelease实例方法,但是对于NSAutoreleasePool类,autorelease实例方法已被该类重载,也就是说,NSAutoreleasePool本身就是管理autorelease对象的池子,而现在它要把自己放进自己管理的池子里,这样就会造成严重的内存错误或死循环,因此会报错。
ARC规则
所有权修饰符
ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符。
所有权修饰符:用于修饰指针类型的关键字,用来告诉编译器对象的内存所有权、引用关系和生命周期管理方式。
所有权修饰符共4种:
- __strong修饰符
- __weak修饰符
- __unsafe_unretained修饰符
- __autoreleasing修饰符
__strong修饰符
是id类型和对象类型默认的所有权修饰符。
objc
id obj = [[NSObject alloc] init];
id __strong obj1 = [[NSObject alloc] init];
上述两个代码是相同的。
管理成员变量的对象所有者
作用域中管理
__strong修饰符表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃,随着强引用的实效,引用的对象会随之释放。
- 自己生成并持有的对象
objc
{
//因为变量是强引用,所以自己持有对象
id __strong obj = [[NSObject alloc] init];
}
因为变量obj超出其作用域,强引用失效,所以自动释放自己持有的对象,对象的所有者不存在,因此废弃对象。
- 非自己生成并持有的对象
objc
{
//强引用,自己持有
id __strong obj = [NSMutableArray array];
}
//超出作用域,强引用实效,自动释放自己持有的对象
与自己生成并持有的对象的生命周期是一样明确的。
赋值上管理
附有__strong修饰符的变量之间还可以相互赋值,下面具体展示:
obj1持有对象A的强引用。
objc
id __strong obj1 = [[NSObject alloc] init];//对象A
obj2持有对象B的强引用。
objc
id __strong obj2 = [[NSObject alloc] init];//对象B
obj3不持有任何对象。
objc
id __strong obj3 = nil;
objc
obj1 = obj2;
obj3 = obj1;
- obj1持有obj2赋值的对象的B的强引用,因为obj1被赋值,所以原先持有的对对象A得强引用实效。对象A的所有者不存在,因此废弃对象A。此时,持有对象B的强引用的变量为obj1和obj2。
- obj3持有obj1赋值的对象的B的强引用。此时,持有对象B的强引用的变量为obj1、obj2和obj3。
objc
obj2 = nil;
obj1 = nil;
obj3 = nil;
- 因为nil被赋予了obj2,所以对对象B的强引用实效。此时,持有对象B强引用的变量为obj1和obj3。
- 因为nil被赋予了obj1,所以对对象B的强引用实效。此时,持有对象B强引用的变量为obj3。
- 因为nil被赋予了obj3,所以对对象B的强引用实效。此时,对象B的所有者不存在,因此废弃对象B。
这里我们使用dealloc函数来查看一下对象被持有和释放的时机:
objc
-(void)dealloc {
NSLog(@"%@被释放", self.name);
}

管理方法参数的对象所有者
objc
-(void)setObject:(id __strong)obj;
objc
{
id __strong test = [[Classes alloc] init];
[test setObject:[[NSObject alloc] init]];
}
test持有Classes对象的强引用,同时Classes对象的obj成员持有NSObject对象的强引用。
在上述代码中,因为test变量超出其作用域,强引用实效,所以自动释放Classes对象。Classes对象的所有者不存在,因此废弃该对象。与此同时,Classes对象的obj成员也被废弃,NSObject对象的强引用实效,自动释放NSObject对象。同样,NSObject对象的所有者不存在,因此也废弃了该对象。
另外,_ _ strong 、_ _ weak和_ _autoreleasing一起时可以保证将附有这些修饰符的自动变量初始化为nil。
objc
id __strong obj1;
id __weak obj2;
id __autoreleasing obj3;

这样,就可以通过__strong修饰符,不必再次输入retain或者release,且满足了"引用计数式内存管理的思考方式"。
id类型和对象类型的所有权修饰符默认为__strong修饰符。
__weak修饰符
__strong修饰符不能解决引用计数式内存管理中"循环引用"的问题。

objc
{
//test1持有Classes对象A的强引用
//test2持有Classes对象B的强引用
id test1 = [[Classes alloc] init];//A
id test2 = [[Classes alloc] init];//B
[test1 setObject:test2];//A.obj_ = test2
//Classes对象A的obj_成员变量持有Classes对象B的强引用,此时持有Classes对象B的强引用的变量为对象A的obj_和test2
[test2 setObject:test1];//B.obj_ = test1
//Classes对象B的obj_成员变量持有Classes对象A的强引用,此时持有Classes对象B的强引用的变量为对象B的obj_和test1
}
因为test1变量超出其作用域,强引用实效,所以自动释放Classes对象A。
因为test2变量超出其作用域,强引用实效,所以自动释放Classes对象B。
此时,持有Classes对象A的强引用的变量为Classes对象B的obj_。
此时,持有Classes对象B的强引用的变量为Classes对象A的obj_。
也就是,A和B在超出作用域后依然不能自动释放,因为还互相强引用着,这样就发生了内存泄漏!
内存泄漏:应当废弃的对象在超出其生存周期后继续存在。
对自身强引用时也会发生循环引用。
objc
id test = [[Classes alloc] init];
[test setObject:test];

那么怎样才能避免循环引用呢?这时候就需要__weak修饰符了。
弱引用不能持有对象实例。
objc
id __weak obj = [[NSObject alloc] init];
上述代码会引起警告:

这是因为代码将自己生成并持有的对象赋值给附有__weak修饰符的变量obj,也就是变量obj持有对持有对象的弱引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会立即释放,编译器就会给出警告。
下面代码(将对象先赋值给附有_ _strong修饰符的变量后再赋值给附有 _ _weak修饰符的变量)可以解决这个问题:
objc
id __strong obj1 = [[NSObject alloc] init];
//因为obj1变量为强引用,所以自己持有对象
id __weak obj2 = obj1;
//obj2变量持有生成对象的弱引用
超出obj1变量作用域,强引用实效,自动释放自己持有的对象。对象所有者不存在,废弃该对象。因为弱引用的变量不持有对象,所以超出变量作用域时,对象即被释放。
再看上面"循环引用"的问题,这时我们将成员变量弱引用,就可以避免该问题了。
objc
@interface Classes : NSObject {
id __weak obj_;
}

__weak修饰符的另一个优点:在持有某对象的弱引用时,若该对象被废弃,弱引用将自动失效且处于nil被赋值的状态(空弱引用)。

objc
id __weak obj1 = nil;
{
id __strong obj2 = [[NSObject alloc] init];
obj1 = obj2;
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
将obj2指向的对象赋给obj1,obj1是弱引用,不增加引用计数,只弱引用,因此作用域中obj1变量持有弱引用的对象。
obj2变量超出作用域后,强引用实效,自动释放自己持有的对象,废弃对象。同时,obj1持有该对象的弱引用也实效,nil赋值给obj1,因此输出nil。

__unsafe_unretained修饰符
不安全的所有权修饰符。附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。
与附有_ _weak修饰符的变量一样,附有 __unsafe_unretained修饰符的变量因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会被立即释放。
__unsafe_unretained也不能持有对象实例。
objc
id __unsafe_unretained obj = [[NSObject alloc] init];

那么,我们对比一下区别:
objc
id __unsafe_unretained obj1 = nil;
{
id __strong obj2 = [[NSObject alloc] init];
obj1 = obj2;
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);

我们可以看到输出结果已经不一样了,第一次可以输出,而第二次出现了典型的"访问已释放对象""的报错。
对比_ _weak,附有__unsafe_unretained的对象是一个不安全的弱引用。它不会增加引用计数,也不会在对象销毁时自动置为nil。
因此,在作用域中和__weak无异,正常输出持有的对象,而超出作用域后,对象销毁且不会自动置为nil,因此程序崩溃,且此时的obj1就是悬垂指针。
悬垂指针:指向一块已经被释放或无效的内存的指针。
换句话说,这个指针曾经指向一个合法的对象,但那个对象后来被销毁了,指针变量本身还保留着那块旧地址。程序若再访问,就会访问到不可用的内存区域,导致程序崩溃。
__autoreleasing修饰符
__autoreleasing使用机制
实际上,不能使用autorelease方法,也不能使用NSAutoreleasePool类。虽然autorelease无法直接使用,但实际上,ARC有效时autorelease功能使起作用的。
- ARC无效时

- ARC有效时
objc
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
ARC有效和无效时,有一部分是等价的:
- 指定@autoreleasepool块来代替无效时NSAutoreleasePool类对象的生成、持有及废弃。
- 对象赋值给附有__autoreleasing修饰符的变量等价于无效时调用对象的autorelease方法。
如果方法名以alloc、new、copy、mutableCopy开头,那么返回的对象不会自动加入autoreleasepool,反之,以array、dictionary、stringWithFormat:等命名的返回的对象会被子哦那个加入。因此同_ _strong修饰符一样,不用显式的使用__autoreleasing修饰符也可以。
objc
@autoreleasepool {
id __strong obj = [NSMutableArray array];
}
因为obj强引用,自己持有对象。并且由编译器判断方法名后自动注册到autoreleasepool。然而超出作用域时,强引用实效,自动释放自己持有的对象,同时,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象(包含obj)也自动释放。
这样,不使用__autoreleasing修饰符也能使对象注册到autoreleasepool。
那么,如果不在@autoreleasepool块中,不显式使用__autoreleasing也会自动注册吗?
答案是会的。前面我们说到每个线程的 RunLoop在每一轮事件循环中,系统都会自动创建并销毁一个 autoreleasepool。因此即使没有显式写@autoreleasepool代码块,系统也会在每次事件循环自动帮助我们创建一个隐式的@autoreleasepool代码块。因此,@autoreleasepool块中,不显式使用__autoreleasing也会自动注册。不过,区别在于显式autoreleasepool中生成的对象会在离开块时立即释放,而不在显式autoreleasepool中的对象会在当前事件循环结束后才释放。
访问带有__weak修饰符的对象,系统实际上会将被访问的对象注册到autoreleasepool中。
objc
id __strong obj1 = [[NSObject alloc] init];
id __weak obj2 = obj1;

那么这是为什么呢?
这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。此时,如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在。
autoreleasing隐式使用机制
id的指针或对象的指针在没有显式指定时会被附加上__autoreleasing修饰符。因此一下代码等价:
objc
- (BOOL) performOperationWithError:(NSError **)error;
- (BOOL) performOperationWithError:(NSError *_autoreleasing *)error;
作为alloc、new、copy、mutableCopy方法返回值取得的对象是自己生成并持有的,其他情况下是取得的是非自己生成并持有的对象,会被注册到autoreleasepool。因此,使用__autoreleasing修饰符的变量作为对象取得参数,与除alloc、new、copy、mutableCopy外其他方法的返回值取得对象完全一样,都会注册到autoreleasepool,并取得非自己生成并持有的对象。
总结一下:
- alloc、new、copy、mutableCopy:自己持有,不注册到autoreleasepool
- 其他工厂方法:非自己持有,注册到autoreleasepool
- autoreleasing 参数:非自己持有,注册到autoreleasepool
规则
具体的ARC规则有:
- 不能使用retain、release、retainCount、autorelease
内存管理是编译器的工作,因此不必使用内存管理方法。如若使用,将产生报错:

总之,只能在ARC无效且手动进行内存管理时使用retain、release、retainCount、autorelease方法。
- 不能使用NSAllocateObject、NSDeallocateObject
一般通过NSObject类的alloc类方法来生成并持有Objective-C对象。若使用,同样会产生报错。
- 遵守内存管理的方法命名规则
在ARC无效时,以alloc、new、copy、mutableCopy开始的方法在返回对象时,必须返回给调用方所应当持有的对象。ARC有效时是在此基础上追加一条init。
以init开始的方法规则更加严格,该方法必须是实例方法,并且必须返回对象。返回的对象应为id类型或该方法声明类的对象类型,或是该类的超类型(父类)或子类型(子类)。该返回对象并不注册到autoreleasepool上,基本上只是对alloc方法返回值的对象进行初始化处理并返回对象。
正确的命名方法:
objc
-(id)initWithObject;
错误的命名方法:
objc
-(void)initThisObject;
- 不要显示调用dealloc
dealloc方法适用的情况有:
- 对象的所有者不持有该对象,要被废弃时。

- 删除已注册的代理或观察者对象。
objc
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
ARC会自动对此进行处理,因此在ARC有效时会遵循无法显式调用dealloc这一规则。若使用,同样会引起编译错误。
- 使用@autoreleasepool块代替NSAutoreleasepool
使用NSAutoreleasepool类会引起编译器报错。
- 不能使用区域(NSZone)
NSZone是早期Objective-C中的一种内存分配优化机制,是为了让开发者能够把不用类型的对象分配在不同的内存区域中,以便优化内存管理或调试。
现代Objective-C宏定义#define __OBJC2__表示当前编译环境使用现代运行时:
- 所有对象分配都统一由malloc管理。
- 内存区域概念已完全废弃。
- allocWithZone:仍然存在,但zone参数被忽略。
- alloc方法现在直接调用allocWithZone:nil。
- 对象型变量不能作为C语言结构体的成员
- 显式转换"id"和"void"

id型或对象型变量赋值给void*或者逆向赋值时都需要进行特定的转换。解决这个问题可以使用"__bridge转换"。
__bridge转换有三种桥接修饰符:
- __bridge转换
objc
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)(obj);
只做类型转换,不改变所有权,不改变引用计数,两个指针指向同一内存。安全性与__unsafe_unretained修饰符相近,甚至更低。如果管理时不注意赋值对象的所有者,就会因悬垂指针而导致程序崩溃。
- __bridge_retained转换
objc
id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void*)obj;
与retain类似,从ARC管理被retain到Core Foundation,并增加引用计数。
- __bridge_transfer转换
objc
void *p = (__bridge_retained void*) [[NSObject alloc] init];
(void)(__bridge_transfer id)p;
与release类似,把Core Foundation转换到ARC,把所有权交给ARC管理。
Objective-C对象 & Core Foundation对象:
Core Foundation对象主要用于C语言编写的Core Foundation框架中,并使用引用计数的对象。
Core Foundation对象与Objective-C对象区别很小,不同之处只在于由哪一个框架生成。Foundation框架的API生成并持有的对象可以用Core Foundation框架的API释放,反之也可以。
属性
当ARC有效时,Objective-C类的属性也会发生变化。

以上,只有copy属性不是简单的赋值,它赋值的是通过NSCopying接口的copyWithZone:方法复制赋值源所生成的对象。
不过,在声明类成员变量时,如果同属性声明中的属性不一致就会引起编译错误。
objc
@interface Auto : NSObject {
id obj;
}
@property(nonatomic, weak) id obj;
我们尝试跑这个代码,发现可以正常运行。这是为什么呢?
这是因为我们现在使用了新版 Xcode(Clang ≥ 8),该编译器不会强制使用我们声明的ivar,而是为属性自动生成一个实例变量_obj。这样的情况下,@property使用_obj,而我们定义的成员变量obj只是一个普通指针,没有被使用,因此编译器不会报冲突。那么哪种情况下会报错呢?
当我们显式使用@synthesize,并让属性和我们定义的ivar绑定时。
objc
@synthesize obj = obj;
这时出现报错:

这是因为@synthesize强制让weak属性使用了强引用的ivar。
那么解决这个问题的办法有两种:
objc
@interface Auto : NSObject {
id __weak obj;
}
@property(nonatomic, weak) id obj;
objc
@interface Auto : NSObject {
id obj;
}
@property(nonatomic, strong) id obj;
数组
静态数组
静态数组除 __unsafe_unretained 外,__strong、__weak、__autoreleasing 修饰的数组元素会被自动初始化为nil。
objc
{
id obj[2];
obj[0] = [[NSObject alloc] init];
obj[1] = [NSMutableArray array];
}
数组超出其变量作用域时,数组中各个附有__strong修饰符的变量也随之实效,其强引用消失,所赋值的对象也随之释放。
动态数组
将附有__strong修饰符的变量作为动态数组来使用时,需要手动管理内存。必须遵守以下事项:
-
声明方式
-
需用指针显式指定修饰符:由于"id *类型"默认为"id__autoreleasing *类型",所以显式指定为_ _strong修饰符。
objcid __strong *array = nil; -
指定类名的形式
objcNSObject * __strong *array = nil;
-
-
内存分配
必须使用calloc函数分配内存。因calloc会将内存初始化为0,满足__strong变量使用前需初始化为nil的要求。那么为什么不使用malloc呢?
这是因为使用malloc函数分配的内存区域没有被初始化为0,因此nil会被赋值给__strong修饰符的并被赋值了随机地址的变量中,从而释放一个不存在的对象。
objc
NSInteger entries = 10;
id __strong *array = (id __strong*)calloc(entries, sizeof(id));
若用malloc分配后,需要使用memset等函数将内存填充为0,禁止直接遍历数组给元素赋值为nil。
- 内存释放
不能直接使用free释放数组内存,需将所有元素赋值为nil,即让元素对对象的强引用实效,释放对象,再调用free释放内存块。否则会内存泄漏。
objc
for (int i = 0; i < entries; i++) {
array[i] = nil;
}
free(array);
使用memset等函数填充0也无法释放对象,会导致内存泄漏。
- 禁止操作
禁止使用memcpy拷贝数组元素,realloc重新分配内存块,这会导致对象被错误保留或重复释放。