iOS内存管理 ARC与MRC

想驾驭一门语言,首先要掌握它的内存管理特性。iOS开发经历了MRC到ARC的过程,下面就记录一下本人对iOS内存管理方面的一些理解。

说到iOS开发,肯定离不开objective-c语言(以下简称OC)。OC的内存管理机制叫做引用计数,就是一块内存地址可以同时被多个对象引用,每引用一次,引用计数都会递增1,当对象每解除一次引用,引用计数就会递减1,直到引用计数为0时,系统才会讲这块内存地址回收释放掉,这与C/C++语言有些不同,但是它们都遵守同一个内存管理法则:谁申请,谁释放。

在早些时候,iOS开发只能使用MRC,通过retain、release来递增引用计数和递减引用计数。但是OC语言的设计者发现,光有这些可能会违背内存管理法则,比如下面代码所示:

- (People *)blackPeople {
People *p = [People new];return p;
}

这段代码有2个问题:

1. 方法在堆空间上创建了一块内存地址,并用一个局部指针变量p指向它,然后将指针p返回给调用者使用(函数返回都是值拷贝)。在这之前不能调用[p release];操作,因为这会使p指针成为野指针。所以最终就要求使用者必须负责释放内存,然而调用者并不应该有这个义务。说好的谁申请,谁释放呢?

2. 在这里要说明一下,在iOS开发中是很注重方法命名的,很多人不是太理解这个说法,笔者在此提醒大家,不注重方法命名,内存管理就会变的很糟糕,尤其是在使用了ARC时,更是应该注意。下面会详细说明。就以上代码来看,如果就想让调用者负责管理内存,我们这样返回没有问题,但是方法的名字不合适,从blackPeople中,我们看不出来任何信息是要求调用者来负责管理内存的,因此调用者肯定不会去负责内存的管理,从而导致内存泄漏。

解决以上2个问题:

1. OC语言的设计者加入了一个autorelease的概念,从名字就可以看出来,是让对象自动释放。这就很好的解决了问题1,如下面正确代码:

- (People *)blackPeople {
People *p = [People new];
return [p autorelease];
}

这样,在堆上分配的这块内存,就会在某个时间点自动递减引用计数,而不需要调用者去管理。

2. 我们应该更改方法名字为newBlackPeople或者allocBlackPeople还有copyBlackPeople。大家可能发现,方法名称中有new、alloc、copy这样的字眼,没错,通过这些词语,任何人都很清楚,此方法返回的对象是需要自己管理内存的。

以上就是MRC机制使用过程中会遇到的内存管理相关的操作语句,至于autorelease到底会在哪个时间点去自动递减引用计数,答案是在下一个runloop开始之前,读者可以去查看runloop的相关知识。

使用MRC难免会产生内存管理方面的问题,因此苹果公司在Xcode中整合了一款名为Clang的静态分析器,帮助开发者发现内存错误,比如内存泄漏或者过早释放。既然Clang可以出色的完成它的工作,那为何不能自动的帮助开发人员插入retain和release来管理内存呢?显然是可行的,因此就有了ARC机制。

如今,iOS开发人员基本都在使用ARC,笔者总结了2点:ARC能够通过方法名称帮助开发人员管理内存;ARC比MRC更效率。下面开始介绍:

1. 刚才在上面提到过方法命名的重要性,这里详细说一下。如果方法名以alloc、new、copy、mutableCopy开头,那么其返回的对象归调用者负责内存管理。正因为ARC以这个规则工作,所以我们必须注重方法命名。看下面代码:

+ (People *)newPeople {
People *p = [[People alloc] init];
return p;
// 方法名中以new开头,所以ARC知道返回的对象该由调用者管理其内存,因此此处不插入
// 任何代码
} + (People *)blackPeople {
People *p = [[People alloc] init];
return p;
// 方法名中没有以上面4种词语开头,所以ARC知道放回的对象该由方法内部管理其内存,
// 因此此处会插入autorelease,最终代码为:return [p autorelease];
} - (void)doSomething {
People *p1 = [People newPeople];
People *p2 = [People somePeople];
// 通过上面的说明,这里大家应该知道,ARC会在方法的最后插入[p1 release];
}

2. ARC在调用retain、release等操作时并不通过普通的OC消息派发机制,而是直接调用底层的C语言版本。而且保留及释放操作需要频繁执行,直接调用底层函数能够节省很多CPU周期,性能更好。比如ARC调用与retain等价的底层函数objc_retain。而且ARC会发现在同一个对象上执行了多次retain和release操作,那么它会成对的移除这些语句。

在ARC机制下,每个变量都是指向对象的强引用(strong),会递增引用计数。看下面代码:

_people = [People blackPeople];
// 可以知道,blackPeople返回对象时已经执行了autorelease操作,但是在ARC机制下,
// 代码其实是下面这样的: People *temp = [People blackPeople];
_people = [temp retain];

可以看出,blackPeople方法中的autorelease和上面代码中的retain是多余的,ARC当然会去优化代码,它会在方法中返回自动释放的对象时,不执行autorelease,而是去调用objc_autoreleaseReturnValue函数。此函数会查看当前方法返回之后即将要执行的那段代码,如果发现那段代码会在返回的对象上执行retain操作,则去设置全局数据结构中的一个标志位,而不执行autorelease操作;同时,调用retain的那边也不直接执行retain操作,而是去执行objc_retainAutoreleasedReturnValue函数。此函数会去检测感概提到的那个标志位,如果已经置位,则不执行retain操作。这样去检测一个标志位,比调用autorelease和retain更快。大概代码如下:

+ (People*)blackPeople {
People *p = [People new];
objc_autoreleaseReturnValue(p);
} // 调用上面方法
People *temp = [People blackPeople];
_people = objc_retainAutoreleasedReturnValue(temp); id objc_autoreleaseReturnValue(id object) {
if (返回将retain对象) {
set_flag(object);
return object; // 不执行autorelease
} else {
return [object autorelease];
}
} id objc_retainAutoreleasedReturnValue(id object) {
if (get_flag(object)) {
clear_flag(object);
return object; // 不执行retain
} else {
return [object retain];
}
}

补充:当我们进行MRC与ARC混编的时候,尤其要注意编码规范,也就是上面一直强调的方法命名规则,看下面代码:

// 假如在MRC下编写了下面这段代码,意思是想让调用者去管理返回对象的内存,
// 然而方法命名确是blackPeople - (People *)blackPeople {
People *p = [People new];
return p;
} // 将如在ARC中调用了上面的方法
- (void)testFun
{
People *people = [[People alloc] init];
people = [people blackPeople];
people = nil;
// 首先在ARC下,第一行代码会申请一块内存,第二行代码调用了blackPeople方法,
// 也会申请一块内存。在ARC看来,它需要将申请的第一块内存释放,
// 然后再让people变量指向申请的第二块内存,这符合我们的意愿。
// 但是要注意第三行代码,尽管我们将people变量置为nil,但ARC并不会去按照我们
// 的意思去释放内存,因为ARC是遵守规则的,它根据blackPeople方法的命名,知道
// 对象的内存由申请者管理,就不需要自己去管理了,从而导致了内存泄漏。
  // 解决的办法就是我们应该遵守规范,这样返回对象:return [p autorelease];
}

还有就是不小心可能会重复释放同一块内存,造成程序崩溃,这里就不上代码了。

以上就是笔者的一些总结,希望能帮助到大家。

上一篇:iOS内存管理(一)


下一篇:IOS内存nil与release的区别