概述
在iOS中开发中,我们或多或少都听说过内存管理。iOS的内存管理一般指的是OC对象的内存管理,因为OC对象分配在堆内存,堆内存需要程序员自己去动态分配和回收;基础数据类型(非OC对象)则分配在栈内存中,超过作用域就会由系统检测回收。如果我们在开发过程中,对内存管理得不到位,就有可能造成内存泄露。
我们通常讲的内存管理,实际上从发展的角度来说,分为两个阶段:MRC和ARC。MRC指的是手动内存管理,在开发过程中需要开发者手动去编写内存管理的代码;ARC指的是自动内存管理,在此内存管理模式下由LLVM编译器和OC运行时库生成相应内存管理的代码。
通篇主要介绍关于内存管理的原理及ARC和MRC环境下编写代码实现的差异。
提纲
一 引用计数
二 MRC操作对象的方法
- alloc/new/copy/mutableCopy
- retain
- release
- autorelease
- autorelease pool
三 ARC操作对象的修饰符
- __strong
1.1 strong与变量
1.2 strong与属性
1.3 strong的实现 - __weak
2.1 weak和循环引用
2.2 weak和变量
2.3 weak的实现
2.3.1 weak和赋值
2.3.2 weak和访问 - __unsafe_unretained
- _autoreleasing
具体介绍
一、引用计数
在OC中,使用引用计数来进行内存管理。每个对象都有一个与其相对应的引用计数器,当持有一个对象,这个对象的引用计数就会递增;当这个对象的某个持有被释放,这个对象的引用计数就会递减。当这个对象的引用计数变为0,那么这个对象就会被系统回收。
当一个对象使用完没有释放,此时其引用计数永远大于1。该对象就会一直占用其分配在堆内存的空间,就会导致内存泄露。内存泄露到一定程度有可能导致内存溢出,进而导致程序崩溃。
二、MRC操作对象的方法
1.alloc/new/copy/mutableCopy
1.1 持有调用者自己的对象
在苹果规定中,使用alloc/new/copy/mutableCopy创建返回的对象归调用者所有,例如以下MRC代码:
NSMutableArray *array = [[NSMutableArray alloc] init];/*NSMutableArray类对象A*/
NSLog(@"%p", array);
[array release];//释放
由于对象A由alloc生成,符合苹果规定,指针变量array指向并持有对象A,引用计数器会加1。另外,array在使用完对象A后需要对其进行释放。当调用release后,释放了其对对象A的引用,计数器减1。对象A此时引用计数值为零,所以对象A被回收。不能访问已经被回收的对象,会发生崩溃。
1.2 持有非调用者拥有的对象
当持有非调用者自己拥有的对象的时候,例如以下代码:
id obj = [Person person];
[obj retain];
/*do something*/
[obj release];
此时obj变量获得但不持有Person类对象,可以通过retain进行持有该对象。当我们使用完该对象,应该调用release方法释放该对象。
注意:按照苹果的命名规则,必须是alloc/new/copy/mutableCopy开头,并且是符合驼峰命名规则生成的对象才归调用者所有。例如以下的方法,生成的对象不归调用者所有:
- (id)newarray;
- (id)allocwithInfo;
- (id)coPySomething;
- (id)mutablecopyItem;
2.retain
2.1 retain和属性
我们可以通过属性来保存对象,如果一个属性为强引用,我们就可以通过属性的实例变量和存取方法来对某个对象进行操作,例如某个属性的setter方法如下:
- (void)setPerson:(Person *)person {
[person retain];
[_person release];
_person = person;
}
我们通过retain新值,release旧值,再给实例变量更新值。需要注意的一点是:需要先retain新值,再release新值。因为如果新旧值是同一个对象的话,先release就有可能导致该对象被系统回收,再去retain就没有任何意义了。例如下面这个例子:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong)Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//实例变量持有Person类对象(P对象)。这样赋值不会调用set方法
_person = [[Person alloc] init];
self.person = _person;//调用set方法
}
- (void)setPerson:(Person *)person {
//release释放对P对象的引用,P对象引用计数值变为零,则P对象被系统回收
[_person release];
//由于P对象已经被回收,再去retain就容易出问题
[person retain];
_person = person;
}
@end
由于P对象被回收,对应其所分配的内存被置于“可用内存池”中。如果该内存未被覆写,那么P对象依然有效;如果内存被覆写,那么实例变量_person就会指向一个被覆写的未知对象的指针,那么实例变量就变成一个“悬挂指针”。
2.2 retain和数组
如果我们把一个对象加入到一个数组中,那么该数组的addObject方法会对该对象调用retain方法。例如以下代码:
//person获得并持有P对象,P对象引用计数为1
Person *person = [[Person alloc] init];//Person类对象生成的P对象
NSMutableArray *array = [NSMutableArray array];
//person被加入到数组,对象P引用计数值为2
[array addObject:person];
此时,对象P被person和array两个变量同时持有。
3.release
3.1 自己持有的对象自己释放
当我们持有一个对象,如果在不需要继续使用该对象,我们需要对其进行释放(release)。例如以下代码:
//array获得并持有NSArray类对象
NSArray *array = [[NSArray alloc] init];
/*当不再需要使用该对象时,需要释放*/
[array release];
//obj获得但不持有该对象
id obj = [NSArray array];
//持有对象
[obj retain];
/*当不在需要使用该对象时,需要释放*/
[obj release];
3.2 非自己持有的对象不要释放
当我们不持有某个对象,却对该对象进行释放,应用程序就会崩溃。
//获得并持有A对象
Person *p = [[Person alloc] init];//Person类对象A
//p释放对象A的强引用,对象A所有者不存在
//对象A引用计数为零,所以对象A被回收
[p release];
[p release];//释放非自己持有的对象
另外,我们也不能访问某个已经被释放的对象,该对象所占的堆空间如果被覆写就会发生崩溃的情况。
4.autorelease
autorelease指的是自动释放,当一个对象收到autorelease的时候,该对象就会被注册到当前处于栈顶的自动释放池(autorelease pool)。如果没有主动生成自动释放池,则当前自动释放池对应的是主运行循环的自动释放池。在当前线程的RunLoop进入休眠前,就会对被注册到该自动释放池的所有对象进行一次release操作。
autorelease和release的区别是:release是马上释放对某个对象的强引用;autorelease是延迟释放某个对象的生命周期。
autorelease通常运用在当调用某个方法需要返回对象的情况下,例如以下代码:
//外部调用
Person *p = [Person person];
NSLog(@"%p", p);//使用无须retain
//持有则需要retain
[p retain];
_person = p;
[_person release];
//Person类内部定义
+ (id)person {
//创建的Person类对象由person获得并持有
Person *person = [[Person alloc] init];
//[person release];
[person autorelease];
return person;
}
在外部调用,从方法名person知道,创建的对象由p指针变量获得但不持有。在函数内部,person获得并持有了Person类对象,所返回的person对象的引用计数加1。换句话说,调用者需要额外处理这多出来的一个持有操作。另外,我们不能在函数内部调用release,不然对象还没返回就已经被系统回收。这时候使用autorelease就能很好地解决这个问题。
只要把要返回的对象调用autorelease方法,注册到自动释放池就能延长person对象的生命周期,使其在autorelease pool销毁(drain)前依然能够存活。
另外,person对象在返回时调用了autorelease方法。该对象已经在自动释放池中,我们可以直接使用对象p,无须再通过[p retain]访问;不过,如果要用实例变量持有该对象,则需要对变量p进行一次retain操作,示例变量使用完该对象需要释放该对象。
5. autorelease pool
5.1 autorelease pool和RunLoop(运行循环)
每条线程都包含一个与其对应的自动释放池,当某条线程被终止的时候,对应该线程的自动释放池会被销毁。同时,处于该自动释放池的对象将会进行一次release操作。
当应用程序启动,系统默认会开启一条线程,该线程就是“主线程”。主线程也有一个与之对应的自动释放池,例如我们常见的ARC下的main.h文件:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
该自动释放池用来释放在主线程下注册到该自动释放池的对象。需要注意的是,当我们开启一条子线程,并且在该线程开启RunLoop的时候,需要为其增加一个autorelease pool,这样有助于保证内存的安全。
5.2 autorelease pool和降低内存峰值
当我们执行一些复杂的操作,特别是如果这些复杂的操作要被循环执行,那么中间会免不了会产生一些临时变量。当被加到主线程自动释放池的对象越来越来多,却没有得到及时释放,就会导致内存溢出。这个时候,我们可以手动添加自动释放池来解决这个问题。如以下例子所示:
for (int i = 0; i < largeNumber; i++) {
//创建自动释放池
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//产生许多被注册到自动释放池的临时对象
id obj = [Person personWithComplexOperation];
//释放池中对象
[pool drain];
}
如上述例子所示,我们执行的循环次数是一个非常大的数字。并且调用personWithComplexOperation方法的过程中会产生许多临时对象,所产生的临时对象有可能会被注册到自动释放池中。我们通过手动生成一个自动释放池,并且在每次循环结束前把该自动释放池的对象执行release操作释放掉,这样就能有效地降低内存的峰值了。
三 ARC操作对象的修饰符
1. __strong
1.1 __strong与变量
在ARC模式下,id类型和OC对象的所有权修饰符默认是__strong。当一个变量通过__strong修饰符来修饰,当该变量超出其所在作用域后,该变量就会被废弃。同时,赋值给该变量的对象也会被释放。例如:
{
//变量p持有Person对象的强引用
Person *p = [Person person];
//__strong修饰符可以省略
//Person __strong *p = [Person person];
}
//变量p超出作用域,释放对Person类对象的强引用
//Person类对象持有者不存在,该对象被释放
上述例子对应的MRC代码如下:
{
Person *p = [Person person];
[p retain];
[p release];
}
可以看出,ARC和MRC是对应的。只不过在ARC下,对象“持有”和“释放”的内存管理代码交由系统去调用罢了。
1.2 strong与属性
如果一个属性的修饰符是strong,对于其实例变量所持有的对象,编译器会在该实例变量所属类的dealloc方法为其添加释放对象的方法。在MRC下,我们的dealloc方法有如:
- (void)dealloc {
[p release];//ARC无效
[super dealloc];//ARC无效
}
在ARC下,我们就无须再去写这样的代码了。另外,在dealloc方法中,ARC只能帮我们处理OC对象。如果实例变量持有类似CoreFoundation等非OC对象,则需要我们手动回收:
- (void)dealloc {
CFRelease(_cfObject);
}
在ARC下,dealloc方法一般用来执行两个任务。第一个就是手动释放非OC对象;第二个是接触监听。另外,在ARC下我们不能主动调用dealloc方法。因为一旦调用dealloc,对象就不再有效。该方法运行期系统会在合适的时机自动去调用。
1.3 strong的实现
在ARC中,除了会自动调用“保留”和“释放”方法外,还进行了优化。例如某个对象执行了多次“保留”和“释放”,那么ARC针对特殊情况有可能会将该对象的“保留”和“释放”成对地移除。例如:
+ (id)person {
Person *tmp = [[Person alloc] init];//引用计数为1
[tmp autorelease];//注册到自动释放池(ARC无效)
return tmp;
}
{
//ARC
_p = [Person person];//_p是强引用属性对应的实例变量
//实现展示
Person *p = [Person person];//Person类对象引用计数为1
_p = [p retain];//递增为2
[_p release];//递减为1
}
//清空自动释放池,Person类对象递减为0,释放该对象
在上述代码中,ARC对应MRC的具体实现展示。在上面的展示中,+(id)person方法内部会调用一次autorelease操作来延迟其返回对象的生命周期,并且稍后在自动释放池进行release操作。_p通过retain来持有该对象,使用完就执行release操作。从而看出retain和autorelease是多余的,完全可以简化成以下代码:
+ (id)person {
Person *tmp = [[Person alloc] init];//引用计数为1
return tmp;
}
{
//ARC
_p = [Person person];//_p是强引用属性对应的实例变量
//实现展示
_p = [Person person];//Person类对象引用计数为1
[_p release];//递减为0,Person类对象被回收
}
那么ARC是如何判断是否移除这种成对的操作呢?其实在ARC中,并不是直接执行retain和autorelease操作的,而是通过以下两个方法:
objc_autoreleaseReturnValue(obj);//对应autorelease
objc_retainAutoreleasedReturnValue(obj);//对应retain
以下为两个方法对应的伪代码:
id objc_autoreleaseReturnValue(id obj) {
if ("返回对象obj后面的那段代码是否执行retain") {
//是
set_flag(obj);//设置标志位
return obj;
} else {
return [obj autorelease];
}
}
id objc_retainAutoreleasedReturnValue(obj) {
if (get_flag(obj)) {
//有标志位
return obj;
} else {
return [obj retain];
}
}
通过以上两段伪代码,我们重新梳理一下代码:
+ (id)person {
Person *tmp = [[Person alloc] init];//引用计数为1
return objc_autoreleaseReturnValue(id tmp);
}
{
//ARC
_p = [Person person];//_p是强引用属性对应的实例变量
//实现展示
Person *p = [Person person];//Person类对象引用计数为1
_p = objc_retainAutoreleasedReturnValue(p);//递增为1
[_p release];//递减为0
}
从上述示例代码可以看出:
当我们用变量p获取person方法返回的对象前,person方法内部会执行objc_autoreleaseReturnValue方法。该方法会检测返回对象之后即将执行的那段代码,如果那段代码要向所返回的对象执行retain方法,则为该对象设置一个全局数据结构中的标志位,并把对象直接返回。反之,在返回之前把该对象注册到自动释放池。
当我们对Person类对象执行retain操作的时候,会执行objc_retainAutoreleasedReturnValue方法。该方法会检测对应的对象是否已经设置过标志位,如果是,则直接把该对象返回;反之,会向该对象执行一次retain操作再返回。
在ARC中,通过设置和检测标志位可以移除多余的成对(“保留”&“释放”)操作,优化程序的性能。
2. __weak
2.1 weak和循环引用
__weak与我们上述所提到的__strong相对应,__strong对某个对象具有强引用。那么,__weak则指的是对某个对象具有弱引用。一般weak用来解决我们开发中遇到的循环引用问题,例如以下代码:
/*Man类*/
#import <Foundation/Foundation.h>
@class Woman;
@interface Man : NSObject
@property (nonatomic, strong)Woman *person;
@end
/*Woman类*/
#import <Foundation/Foundation.h>
@class Man;
@interface Woman : NSObject
@property (nonatomic, strong)Man *person;
@end
/*调用*/
- (void)viewDidLoad {
[super viewDidLoad];
Man *man = [[Man alloc] init];
Woman *woman = [[Woman alloc] init];
man.person = woman;
woman.person = man;
}
从上述代码可以看出,Man类对象和Woman类对象分别有一个所有权修饰符为strong的person属性。两个类之间互相通过实例变量进行强引用,Man类对象如果要释放,则需要Woman类对象向其发送release消息。然而Woman类对象要执行dealloc方法向Man类对象发送release消息的话,又需要Man类对象向其发送release消息。双方实例变量互相强引用类对象,所以造成循环引用,如下图所示:
这时候weak修饰符就派上用场了,因为weak只通过弱引用来引用某个对象,并不会真正意义上的持有该对象。所以我们只需要把上述两个类对象的其中一个属性用weak来修饰,就可以解决循环引用的问题。例如我们把Woman类的person属性用weak来修饰,分析代码如下:
/*调用*/
- (void)viewDidLoad {
[super viewDidLoad];
Man *man = [[Man alloc] init];//Man对象引用计数为1
Woman *woman = [[Woman alloc] init];//Woman对象引用计数为1
man.person = woman;//强引用,Woman对象引用计数为2
woman.person = man;//弱引用,Man对象引用计数为1
}
//变量man超出作用域,对Man类对象的强引用失效
//Man类对象持有者不存在,Man类对象被回收
//Man类对象被回收, woman.person = nil;
//Man调用dealloc方法,Woman类对象引用计数递减为1
//变量woman超出作用域,对Woman类对象强引用失效
//Woman类对象持有者不存在,Woman类对象被回收
从上述示例代码可以看出,我们只需要把其中一个强引用修改为弱引用就可以打破循环引用。类似的循环引用常见的有block、NSTimer、delegate等,感兴趣的自行查阅相关资料,这里不作一一介绍。
另外,基于运行时库,如果变量或属性使用weak来修饰,当其所指向的对象被回收,那么会自动为该变量或属性赋值为nil。这一操作可以有效地避免程序出现野指针操作而导致崩溃,不过强烈不建议使用一个已经被回收的对象的弱引用,这本身对于程序设计而言就是一个bug。
2.2 weak和变量
如果一个变量被__weak修饰,代表该变量对所指向的对象具有弱引用。例如以下代码:
Person __weak *weakPerson = nil;
if (1) {
Person *person = [[Person alloc] init];
weakPerson = person;
NSLog(@"%@", weakPerson);//weakPerson弱引用
}
NSLog(@"%@", weakPerson);
输出结果如下:
[1198:73648] <Person: 0x600000018120>
[1198:73648] (null)
从上述输出结果可以分析,当超出作用域后,person变量对Person对象的强引用失效。Person对象持有者不存在,所以该对象被回收。同时,weakPerson变量对Person的弱引用失效,weakPerson变量被赋值为nil。
另外需要注意的是,如果一个变量被weak修饰,那么这个变量不能持有对象示例,编译器会发出警告。例如以下代码:
Person __weak *weakPerson = [[Person alloc] init];
因为weakPerson被__weak修饰,不能持有生成的Person类对象。所以Person类对象创建完立即被释放,所以编译器会给出相应的警告:
Assigning retained object to weak variable; object will be released after assignment
2.3 weak的实现
2.3.1 weak和赋值
要解释weak赋值的实现,我们先看以下示例代码:
Person *person = [[Person alloc] init];
Person __weak *p = person;
上述对应的模拟代码如下:
Person *person = [[Person alloc] init];
Person *p;
objc_initWeak(&p, person);
objc_destroyWeak(&p, 0);
上述两个函数分别用来对变量p的初始化和释放,它们都调用同一个函数。如下:
id p;
p = 0;
objc_storeWeak(&p, person);//对应objc_initWeak
objc_storeWeak(&p, 0);//对应objc_destroyWeak
根据上述代码我们进行分析:
1.初始化变量
当我们使用变量弱引用指向一个对象时,通过传入变量的地址和赋值对象两个参数来调用objc_storeWeak方法。该方法内部会将对象的地址&person作为键值,把变量p的地址&p注册到weak表中。
2.释放变量
当超出作用域,Person类对象被回收,此时调用objc_storeWeak(&p, 0)把变量的地址从weak表中删除。 变量地址从weak表删除前,利用被回收对象的地址作为键值进行检索,把对应的变量地址赋值为nil。
2.3.2 weak和访问
当我们访问一个被__weak修饰过的变量所指向的对象时,其内部是如何实现的?我们先看以下示例代码:
Person *person = [[Person alloc] init];
Person __weak *p = person;
NSLog(@"%@", p);
上述代码对应的模拟代码如下:
Person *person = [[Person alloc] init];
Person *p;
objc_initWeak(&p, person);
id tmp = objc_loadWeakRetained(&p);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&p);
通过上述代码可以看出,访问一个被__weak修饰的变量,相对于赋值多了两个步骤:
- objc_loadWeakRetained,通过该函数取出对应变量所引用的对象并retain;
- 把变量指向的对象注册到自动释放池。
这样一来,weak变量引用的对象就被加入到自动释放池。在自动释放池结束前都能安全使用该对象。需要注意的是,如果大量访问weak变量,就会导致weak变量所引用的对象被多次加入到自动释放池,从而导致影响性能。如果需要大量访问,我们通过__strong修饰的变量来解决,例如:
Person __weak *p = obj;
Person *tmp = p;//p引用的对象被注册到自动释放池
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
通过利用__strong中间变量的使用,p引用的对象仅注册1次到自动释放池中,有效地减少了自动释放池中的对象。
3. __unsafe_unretained
首先我们需要明确一点,如果一个变量被__unsafe_unretained修饰,那么该变量不属于编辑器的内存管理对象。该修饰符表明不保留值,即对其所指向的对象既不强引用,也不弱引用。例如以下代码:
根据上述代码分析:当超出作用域,变量p对Person类对象的强引用失效。Person类对象的持有者不存在,该对象被回收。当我们再次使用该变量的时候,因为其所表示的对象已经被回收,所以就会发生野指针崩溃。这一点区别于__weak,当被__weak修饰变量所指向的对象被回收,该变量会赋值为nil。
另外,被__unsafe_unretained修饰的变量跟__weak一样,不能持有对象实例。因为__unsafe_unretained修饰的变量不会对生成的对象实例进行保留操作,所以对象创建完立马被回收,编译器会给出相应的警告。
当我们给被__unsafe_unretained修饰的变量赋值时,必须保证赋值对象确实存在,不然程序就会发生崩溃。
4. __autoreleasing
在ARC和MRC中,autorelease作用一致,只是两者的表现方式有所不同。
1.如果返回的对象归对用者所有,如下:
@autoreleasepool {
Person __autoreleasing *p = [[Person alloc] init];
}
上述代码模拟代码如下:
id pool = objc_autoreleasePoolPush();
Person *p = [[Person alloc] init];//持有
objc_autorelease(p);
objc_autoreleasePoolPop(pool);
2.如果返回的对象不归调用者所有,如下:
@autoreleasepool {
Person __autoreleasing *p = [Person person];
}
上述代码模拟代码如下:
id pool = objc_autoreleasePoolPush();
Person *p = [Person person];
objc_retainAutoreleasedReturnValue(p);//持有(优化后的retain方法)
objc_autorelease(p);
objc_autoreleasePoolPop(pool);
从上面两个例子可以看出,__autoreleasing的实现都是通过objc_autorelease()函数,只不过是持有方法有所不同而已。
至此,内存管理就介绍完毕了。由于技术有限,难免会存在纰漏,欢迎指正。转载请注明出处,万分感谢。
参考资料:
Objective-C 高级编程 iOS和OS X多线程和内存管理
Effective Objective 2.0