Objective-C ARC内存管理的深入理解(一)
引子
这段时间一直在看关于内存管理的资料,把我整的够呛,尤其是看了
<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: