ARC 即为 “automatic reference counting”,相比 MRR,主要区别在于是人为还是编译器插入与内存管理相关的语句。此文只会记录 ARC 的内存管理规则,以及一些如弱引用、自动释放的快速返回等特性。所以这个标题起大了(目的是为了和之前的文章标题对齐),要真探究自动引用技术的实现,大多是编译器的工作了吧?
所有权修饰符
ARC 的出现引起了对引用计数模型的理解的变化:在 ARC 的环境下,开发者们不用苦思冥想加一、减一去操作对象的引用计数(这部分交给编译器去完成),只需要知道被强引用的对象会存在,不再被强引用的对象会被释放。
声明 id 类型和对象类型时都必须加上所有权(ownership)修饰符,有四个选项:__strong
, __weak
, __unsafe_unretained
, __autoreleasing
。
__strong
__strong
是默认的修饰符。将对象赋给 __strong
修饰的变量后,该变量对对象有强引用,当超出变量的作用域的时候,该变量销毁,对象的强引用不复存在:
// ARC 下的
{
id __strong obj = [[NSObject alloc] init];
id __strong arr = [NSMutableArray array];
}
// 等同于
// MRR 下的
{
id obj = [[NSObject alloc] init];
id arr = [NSMutableArray array];
[obj release];
}
编译器对于符合命名规则的实例化方法,能正确地判断怎么释放对象,比如上面的 arr
变量就不会被发送 -release
消息。所以光 __strong
修饰符是能完成内存管理法则中的前两条的工作,至于后面如何释放,由编译器推断好了。
__weak
前面提到被强用的对象不会被销毁,那么两个对象相互强引用那就不得了了,除非打破这个循环引用,否则它们永远都不会被释放,这个时候弱引用就派上用场了。除此之外,__weak
修饰的变量,在其所指向的强引用的对象被释放时,会自动设置为 nil
;
__unsafe_unretained
__unsafe_unretained
貌似是为了兼容 iOS 4 及以前的运行环境而出现的。作用与 __weak
类似,不同之处在于它所修饰的变量,不会在所指对象销毁时被置 nil
:
id __weak weakObj;
id __unsafe_unretained unsafeObj;
@autoreleasepool {
id obj = [[NSObject alloc] init];
weakObj = obj;
unsafeObj = obj;
}
NSLog(@"%@", weakObj); // 打印 (null)
NSLog(@"%@", unsafeObj); // 爆炸
__autoreleasing
对象赋给由 __autoreleasing
修饰的变量时,会被注册到自动释放池中。当然像下面这样不用显式使用 __autoreleasing
修饰,作为返回值的对象,也会被编译器注册到自动释放池中(也不一定,后面会提到另一种情况):
+(id)array {
return [NSMutableArray new];
}
关于 __autoreleasing
还有个很有意思的是,我们在写一些向上返回 NSError
对象的方法时,编译器会将 NSError **
解释为 NSError *__autoreleasing *
像这样:
-(void)foo {
NSError *error = nil;
if ([self barWithError:<#(NSError *__autoreleasing *)#>]) {
// handle error
}
}
-(BOOL)barWithError:(NSError **)error {
BOOL inevitableError = YES;
if (error && *error) {
*error = [[NSError alloc] init];
}
return inevitableError;
}
这个也是服从内存管理规则的,毕竟这个 NSError
对象不是外部调用者使用 allow/new/copy/mutableCopy 开头的方法生成并持的。
弱引用
__weak
修饰的变量是如何“自动”地被置为 nil
的?
要回答这个问题必须了解弱引用的实现,先看一个例子探究其如何存储( MRR 下也是可以开启弱引用的):
id obj = [NSObject new];
id __weak weakObj = obj;
[obj release];
NSLog(@"%@", weakObj);
结合 NSObject.mm 和 object-weak.mm 这两个文件,通过查看汇编和符号断点调试,可以推测上面例子的实际调用过程:
extern id objc_initWeak(id *location, id newObj);
extern void objc_destroyWeak(id *location);
id obj = [NSObject new];
id weakObj;
objc_initWeak(&weakObj, obj);
[obj release];
NSLog(@"%@", objc_loadWeak(&weakObj));
objc_destroyWeak(&weakObj);
介绍上面函数之前,先看一下与弱引用表等数据结构,如下图:
还记得那 64 个 SideTable 小格子吗?每个 SideTable 都有这么个结构体 weak_table
作为成员。而 weak_table_t
包含 weak_entries
这个指针,指向一块包含多个条目的内存区域,每一次给弱变量赋予不同的对象,都会产生一个新的条目,而当一个对象不再存在弱引用变量时,这个条目也会被移除,这块内存是大小是不断变化的。weak_entry_t
中的 referent
正是被弱引用指向的对象,下面有个联合体,其中上下两个结构体占用的内存是相同的,它们的使用是一种“或”的关系,其作用是存放弱变量的地址,当数目不超过 WEAK_INLINE_COUNT
时,把这些地址存放到 inline_referrers
这个数组中去,否则存到 referrers
指向的内存区域中,其大小也是动态变化的。
回到函数的实现,这里不贴代码,只记录其大概的工作流程:
-
objc_init()
通过简单的赋值让weakObj
和obj
指向相同的对象,然后对obj
散列,映射到一个 SideTable 的weak_table
后,创建一个weak_entry_t
把对象地址和weakObj
变量的地址存起来; -
objc_loadWeak()
任何取得__weak
变量的值的地方都会用到这个函数,它对&weakObj
解引用,如果解引用的结果是nil
或者weakObj
指向的对象在没有相应的weak_entry_t
,返回nil
;否则返回该对象并向对象发送-retain
和-autorelease
消息; -
objc_destroyWeak()
则是移除已注册的弱引用变量。如果移除后,某个对象不再有弱引用,那么释放存在于weak_table
中条目。
那么问题来了,哪个函数将 weakObj
置为 nil
了?
答案就在 [obj release]
这一行中,当 obj
要被释放,其调用的函授大概是这样的:
objc_object::rootDealloc()
object_dispose(id obj)
objc_object::clearDeallocating()
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
由于先前在 objc_init()
的时候存下了弱引用的地址,在 weak_clear_no_lock()
函数中很容易通过 *referrer = nil
将其置为 nil
。
自动释放的快速返回
好吧这个名字是我自己起的 = =,通过 objc_autoreleaseReturnValue()
和 objc_retainAutoreleasedReturnValue()
等函数,优化内存管理,减少注册到自动释放池的对象数量。比如说下面这一段 MRR 下的代码:
+(instancetype)randomPerson {
Person * p = [[Person alloc] init];
return [p autorelease];
}
+(void)test {
Person *p = [[Person randomPerson] retain];
[p doSomething];
[p release];
}
在获得 +randomPerson
后,由于我并不持有它,生怕它在某个时刻被释放掉而 do 不了 something,所以要 retain 一下。
而这份代码在 ARC 下经过编译器改写后据说是酱紫的:
+(instancetype)randomPerson {
Person * p = [[Person alloc] init];
return objc_autoreleaseReturnValue(p);
}
+(void)test {
Person *p = objc_retainAutoreleasedReturnValue([Person randomPerson]);
[p doSomething];
objc_storeStrong(&p, nil); // 相当于 [p release]
}
但是如果编译器知道代码会 retain 一下 +randomPerson
返回的对象,那么就不会把这个对象放到自动释放池中以减少额外的开销。
不管是什么书还是博客都这么说,但是我在测验的时候,写下这样的代码:
__strong Person *p = [Person randomPerson];
__strong Person *k = [Person randomPerson];
[p doSomething];
[k doSomething];
_objc_autoreleasePoolPrint();
还是能看到有一个 Person 对象被注册到自动释放池中。
把 __strong
改成 __weak
的话就合乎情理——两个对象都被注册到自动释放池中。
关于这点想了好久都没搞清楚,所以我打算得到新的 objc-runtime 的源码之后再回到这个问题上。