引子
这段时间一直在看关于内存管理的资料,把我整的够呛,尤其是看了
<Objective-C高级编程>、雷纯锋大神的<Objective-C Autorelease Pool 的实现原理>和Sunny大神的<黑幕背后的Autorelease>,这几份作品后,知识体系始终是零零碎碎的,脑袋一团糊浆。直到帮人分析了一个程序的Bug后,恍然大悟。本文主要参考以上几份作品,对ARC下的内存管理进行了实践与梳理,本人是iOS开发新手,文章有误之处请轻拍。
命名规则
根据苹果官方文档<Memory Management Policy>中所述,总结规则如下:
You take ownership of an object if you create it using a method whose name begins with “alloc”, “new”, “copy”, or “mutableCopy”.
使用以下名称开头的方法名意味着自己生成并持有对象:
- alloc
- new
- copy
- mutableCopy
这个命名规则并不是指命名规范之类的大家约定俗成的东西(譬如成员变量以_开头、驼峰拼写法等规范),编译器会自动判断方法名字的开头是否符合以上命名规则,不遵循命名规则会遭到编译器的警告。下面看看一个例子:

上图中声明了一个以new开头的属性,编译器为我们生成了setter和getter方法,getter方法名如下:- (void)newPerson,根据命名规则,编译器认为我们要自己生成并持有一个对象,显然getter方法不产生新对象,于是编译错误。
这里提到的命名规则,对后文所述内容有帮助。
ARC规则
下面从一个例子入手:

在上图中,声明了两个以__weak所有权修饰符修饰的id类型对象。忽略obj1和obj2都有的一个未使用的警告。obj1还有一个警告,说对象创建后即会被释放(当然啦,生成的对象没有强引用指着,结果就挂了,弱引用不能持有对象实例)。可是为什么obj2没有该警告呢?
我们注意到obj2获取的对象是通过类方法获得的,根据命名规则,不是以alloc/new/copy/mutableCopy开头的方法,obj2取得的是非自己生成并持有的对象,那么显然,这个UIImage对象的持有者另有他人。
下面利用lldb指令,我们跟踪这两个对象的生命周期,由于test方法内部声明的两个变量是在栈上产生的局部变量,对后续工作有影响,我们将两个变量定义成成员变量。

在test开始前和开始后分别打上断点:

接下来我们运行程序,当来到第一个断点处,在控制台输入watchpoint set v _obj1和watchpoint set v _obj2,对这两个成员变量进行观测。

由于test方法前_obj1、_obj2都为nil,从控制台可以看到地址值0x00。
接下来点击继续:

我们发现Watchpoint 1 hit,即obj1的值发生了变化,这是因为
_obj1 = [[NSObject alloc] init];
_obj1指向了一个对象,我们再来看看左边的堆栈信息:

这里我们先留意这里有objc_storeWeak,后文会提到。
然后我们继续运行。

我们看到,_obj1挂掉了,它被置为了0x00,也就是nil。并且它是立即被释放了,为什么? 因为_obj2都还没被赋值,_obj1就被release了。这时再看看堆栈信息。

可以看到几个关键词,"release(释放)"、"dispose(丢弃)"、"destructInstance(销毁实例)"。可以证明_obj1确实挂了,而且是立即被释放。
再点继续。

我们来到了42行的断点,这验证了刚创建就被释放的说法。
不管_obj1了,我们继续点运行。

这时,_obj2的值也被改变了,它指向的是UIImage实例对象。左边的堆栈信息和_obj1一样,就不截图了。下面再点运行。

代码安全地到了44行,_obj2并没有被释放。
我们继续运行,来到了_obj2被释放的时候:

这时再来看看堆栈信息:

这图与_obj1被释放时很相似,但请注意这里多了[UIImage dealloc]、[NSAutoreleasePool release]和 AutoreleasePoolPage等字眼。说明_obj2所指向的对象曾被加入了自动释放池并且在这个时候被释放了。
事实上,“自动释放池并不是在
test方法作用域结束后释放池中对象的,而是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。”
编译器做了什么?
当我们用所有权修饰符去声明时,编译器为我们做了什么?
先看看__strong下的情况。
{
id __strong obj = [[NSObject alloc] init];
}
编译器将以上代码转换成以下代码,注意大括号标明作用域:
{
/*编译器模拟代码*/
id obj = obj_msgSend(NSObject,@selector(alloc));
obj_msgSend(obj,@selector(init));
obj_release(obj);
}
上面代码调用了两次obj_msgSend,分别调用了alloc方法和init方法,变量作用域结束时,通过obj_release释放了obj对象。__strong所修饰的局部变量的生命周期通常是花括号内,这与我们平常的认知相符。
然后看看__weak。
{
id __weak obj1 = obj;
}
其中假设obj被__strong修饰且有值。
{
/*编译器模拟代码*/
id obj1;
objc_initWeak(&obj1,obj);
objc_destroyWeak(&obj1);
}
首先来看看objc_initWeak函数。它先初始化__weak修饰的变量。
obj1 = 0;
objc_storeWeak(&obj1,obj);
objc_destroyWeak函数将0作为参数调用objc_storeWeak。
objc_storeWeak(&obj1,0);
于是。前面那段代码与下面相同。
obj1 = 0;
objc_storeWeak(&obj1,obj);
objc_storeWeak(&obj1,0);
“
objc_storeWeak函数把第二参数的复制对象的地址作为键值,将第一参数的附有__weak修饰符的变量的地址注册到weak表中。如果第二参数为0,则把变量的地址从weak表中删除。weak表与引用计数表实现相同。”
回到前面的例子中!
{
_obj1 = [[NSObject alloc]init];
}
这段代码将被编译器解释为:
{
/*编译器模拟代码*/
id obj1;
id tmp = objc_msgSend(NSObject, @selecter(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj1, tmp);
objc_release(tmp);
objc_destroyWeak(&obj1);
}
-
id obj1,首先编译器为我们创建一个obj1变量 -
id tmp = objc_msgSend(NSObject, @selecter(alloc)),创建了一个临时变量tmp存放调用NSObject的alloc方法的返回的实例 -
objc_msgSend(tmp, @selector(init)),tmp继续发送init方法的消息,这时tmp是一个初始化完毕的实例对象的持有者 - 通过
objc_initWeak(&obj1, tmp)函数初始化附有__weak修饰符的变量obj1,objc_initWeak(&obj1, tmp)函数内部又进行了obj1 = 0;和obj_storeWeak(&obj1, tmp)这两个步骤。 -
obj_storeWeak(&obj1, tmp)以tmp的赋值对象的地址作为键值,将附有__weak修饰符的obj1变量的地址注册到weak表中。 - 虽然通过
alloc方法生成并持有了一个对象,但赋值给了附有__weak修饰符的变量,编译器判断这个对象不能继续被持有,通过objc_release(tmp)被立即释放和废弃了。 -
objc_destroyWeak(&obj1)即obj_storeWeak(&obj1, 0),将weak表中所有包含obj1变量地址的记录赋值为nil。然后删除该记录。
然后继续解释第二条_obj2 = [UIImage imageNamed:@"xxx"]。在这之前还需要补充一个知识点。
最优化——省略autoreleasepool注册
调用类方法,情况有些许不同。
{
id __strong obj = [NSMutableArray array];
}
{
/*编译器模拟代码*/
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
}
这条objc_retainAutoreleasedReturnValue是什么呢?
“
objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行。它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool中的对象的方法,或是函数的返回值。”
要想理解以上这段话的含义,还得看一个函数。
接下来看看NSMutableArray的array类方法经过编译器怎样的转换。
+ (id) array
{
return [[NSMutableArray alloc] init];
}
{
/*编译器模拟代码*/
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
在alloc/new/copy/mutableCopy开头的方法以外的一些方法上,编译器判断是否为函数的返回值,自动将其注册到autoreleasepool中。返回注册到autoreleasepool中对象的方法使用了objc_autoreleaseReturnValue函数。
并且!:
objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnValue函数,那么就不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的调用方。不将对象注册到autoreleasepool中,这一过程达到了最优化。
为了理解这段话,我又做了一个试验。
与之前一样,这里的_obj是id类型的弱指针

在36行断点时,我们跟踪成员变量的值。

继续运行。跟踪到了_obj1挂掉的时候。

再看看堆栈。

可以发现,它并不是通过自动释放池释放的!而是刚创建立刻被释放了
但为什么通过+ (UIImage *)imageNamed:(NSString *)name产生的对象是在自动释放池中的呢?说明这个方法是注册到自动释放池的。
回顾上面在模拟的代码
+ (id) array
{
/*编译器模拟代码*/
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
显然,在调用array方法的过程中,使用了objc_autoreleaseReturnValue使编译器做了最优化的处理,使得返回值并没有注册到自动释放池中。
至于本文开头的例子为何+ (UIImage *)imageNamed:(NSString *)name方法产生的对象会被注册到自动释放池呢?
这是因为在苹果中,有一大类构造方法
(constructor method),由它们构造的对象直接就是自动释放的对象;这一类构造方法叫做便捷方法。
比如:
-
NSArray的arrayWithObjects :和arrayWithArray : -
UIImage的imageNamed : -
NSNumber的numberWithBool等 -
NSString的stringWithFormat: