本文为 《Objective-C 高级编程》1.4 中的内容
ARC 由以下工具、库来实现:
- clang(LLVM 编译器)3.0 以上
- objc4 Objective-C 运行时库 493.9 以上
通过 clang 汇编输出和 objc4 库(主要是 runtime/objc-arr.mm)的源代码进行说明。
__strong 修饰符
赋值给附有 __strong 修饰符的变量在实际的程序中到底是怎样运行的呢?
{
id __strong obj = [[NSObject alloc] init];
}
复制代码
在编译器选项 “-S” 的同时运行 clang,可取得程序汇编输出。看看汇编输出和 objc4 库的源代码就能够知道程序是如何工作的。该源代码实际上可转换为调用以下的函数。为了便于理解,以后的源代码有时也使用模拟源代码。
/* 编译器的模拟代码*/
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
复制代码
如原源代码所示,2次调用 objc_msgSend 方法(alloc 方法和 init 方法),变量作用域结束时通过 objc_release 释放对象。虽然 ARC 有效时不能使用 release 方法,但由此可知编译器自动插入了 release。下面我们来看看使用 alloc/new/copy/mutableCopy 以外的方法会是什么情况。
{
id __strong obj = [NSMutableArray array];
}
复制代码
虽然调用了我们熟知的 NSMutableArray 类的 array 类方法,但得到的结果却与之前稍有不同。
/* 编译器的模拟代码*/
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
复制代码
虽然最开始的 array 方法的调用以及最后变量作用域结束时的 release 与之前相同,但中间的 objc_retainAutoreleasedReturnValue 函数是什么呢?
objc_retainAutoreleasedReturnValue 函数主要用于最优化程序运行。顾名思义,它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在 autoreleasepool 中对象的方法,或是函数的返回值。像该源代码这样,在调用 alloc/new/copy/mutableCopy 以外的方法,即 NSMutableArray 类的 array 类方法等调用之后,由编译器插入该函数。
这种 objc_retainAutoreleasedReturnValue 函数是成对的,与之相对的函数是 objc_autoreleaseReturnValue。它用于 alloc/new/copy/mutableCopy 方法以外的 NSMutableArray 类的 array 类方法等返回对象的实现上。下面我们看看 NSMutableArray 类的 array 类通过编译器会进行怎样的转换。
+(id)array {
return [[NSMutableArray alloc] init];
}
复制代码
以下为该源代码的转换,转换后的源代码使用了 objc_autoreleaseReturnValue 函数。
+(id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
复制代码
像该源代码这样,返回注册到 autoreleasepool 中对象的方法使用了 objc_autoreleaseReturnValue 函数返回注册到 autoreleasepool 中的对象。但是 objc_autoreleaseReturnValue 函数同 objc_autorelease 函数不同,一般不仅限于注册对象到 autoreleasepool 中。
objc_autoreleaseReturnValue 函数会检查使用该函数的方法或函数调用方的执行列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用 objc_retainAutoreleaseReturnValue() 函数,那么就不将返回的对象注册到 autorelease 中,而是直接传递到方法或函数的调用方。objc_retainAutoreleasedReturnValue 函数与 objc_retain 函数不同,它即便不注册到 autoreleasepool 中而返回对象,也能够正确地获取对象。通过 objc_autoreleaseReturnValue 函数和 objc_retainAutoreleasedReturnValue 函数的写作,可以不将对象注册到 autorelease 中而直接传递,这一过程达到了最优化。
objc_autoreleaseReturnValue
objc_autoreleaseReturnValue 实现如下:
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id
objc_autoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
复制代码
prepareOptimizedReturn 是优化返回的关键,其实现如下:
// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool
prepareOptimizedReturn(ReturnDisposition disposition)
{
assert(getReturnDisposition() == ReturnAtPlus0);
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
if (disposition) setReturnDisposition(disposition);
return true;
}
return false;
}
复制代码
__builtin_return_address
gcc 的编译特性使用 __builtin_return_address(level) 打印出一个函数的堆栈地址。其中 level 代表是堆栈中第几层调用地址,__builtin_return_address(0) 表示第一层调用地址,即当前函数,__builtin_return_address(1) 表示第二层。
callerAcceptsOptimizedReturn
callerAcceptsOptimizedReturn 在 arm 架构上的实现如下:
static ALWAYS_INLINE bool
callerAcceptsOptimizedReturn(const void *ra)
{
// if the low bit is set, we're returning to thumb mode
if ((uintptr_t)ra & 1) {
// 3f 46 mov r7, r7
// we mask off the low bit via subtraction
// 16-bit instructions are well-aligned
if (*(uint16_t *)((uint8_t *)ra - 1) == 0x463f) {
return true;
}
} else {
// 07 70 a0 e1 mov r7, r7
// 32-bit instructions may be only 16-bit aligned
if (*(unaligned_uint32_t *)ra == 0xe1a07007) {
return true;
}
}
return false;
}
复制代码
关于这段代码的含义请看这篇文章,总结的比我好:How does objc_retainAutoreleasedReturnValue work?
objc_retainAutoreleasedReturnValue
objc_retainAutoreleasedReturnValue 实现如下,基本就是 objc_autoreleaseReturnValue 的逆操作。
// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
return objc_retain(obj);
}
复制代码
__weak 修饰符
就像前面我们看到的一样,__weak 修饰符提供的功能如同魔法一般。
- 若附有 __weak 修饰符的变量所引用的对象被废弃,则将 nil 赋值给该变量。
- 使用附有 __weak 修饰符的变量,即是使用注册到 autoreleasepool 中的对象。
这些功能像魔法一样,到底发生了什么,我们一无所知。所以下面我们来看看它们的实现。
{
id __weak obj1 = obj;
}
复制代码
假设变量 obj 附加 __strong 修饰符且对象被赋值。
/* 编译器的模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
objc_destoryWeak(&obj1);
复制代码
通过 objc_initWeak 函数初始化附有 __weak 修饰符的变量,在变量作用域结束时通过 objc_destroyWeak 函数释放该变量。
如以下源代码所示,objc_initWeak 函数将附有 __weak 修饰符的变量初始化为0后,会将赋值的对象作为参数调用 objc_storeWeak 函数。
obj1 = 0;
objc_storeWeak(&obj1, obj);
复制代码
objc_destroyWeak 函数将0作为参数调用 objc_storeWeak 函数。
objc_storeWeak(&obj1, 0);
复制代码
即前面的源代码与下列源代码相同。
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(&obj1, 0);
复制代码
objc_storeWeak 函数把第二参数的赋值对象的地址作为键值,将第一参数的附有 __weak 修饰符的变量的地址注册到 weak 表中。如果第二参数为 0,则把变量的地址从 weak 表中删除。
weak 表与引用计数表相同,作为散列表被实现。如果使用 weak 表,将废弃对象的地址作为键值进行检索,就能高速地获取对应的附有 __weak 修饰符的变量的地址。另外,由于一个对象可同时赋值给多个附有 __weak 修饰符的变量中,所以对于一个键值,可注册多个变量的地址。
释放对象时,废弃谁都不持有的对象的同时,程序的动作是怎样的呢?下面我们来跟踪观察。对象将通过 objc_release 函数释放。
- objc_release
- 因为引用计数为 0 所以执行 dealloc
- _objc_rootDealloc
- object_dispose
- objc_destructInstance
- objc_clear_deallocating
对象被废弃时最后调用的 objc_clear_deallocating 函数的动作如下:
- 将 weak 表中获取废弃对象的地址为键值的记录。
- 将包含在记录中的所有附有 __weak 修饰符变量的地址,赋值为 nil。
- 从 weak 表中删除改记录。
- 从引用计数表中删除废弃对象的地址为键值的记录。
根据以上步骤,前面说的如果附有 __weak 修饰符的变量所引用的对象被废弃,则将 nil 赋值给该变量这一功能即被实现。由此可以,如果大量使用附有 __weak 修饰符的变量,则会消耗相应的 CPU 资源,良策是只在需要避免循环引用时使用 __weak 修饰符。
使用 __weak 修饰符时,以下源代码会引起编译器警告。
id __weak obj = [[NSObject alloc] init];
复制代码
因为该源代码将自己生成并持有的对象赋值给附有 __weak 修饰符的变量中,所以自己不能持有该对象,这时会被释放并废弃,因此会引起编译器警告。
编译器会如何处理改源代码呢?
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init))l
objc_initWeak(tmp);
objc_release(tmp);
objc_destroyWeak(&object);
复制代码
虽然自己生成并持有对象通过 objc_initWeak 函数被赋值给附有 __weak 修饰符的变量中,但编译器判断其没有持有者,故该对象立即通过 objc_release 函数被释放和废弃。
这样一来,nil 就会被赋值给引用废弃对象的附有 __weak 修饰符的变量中。下面我们通过 NSLog 函数来验证一下。
id __weak obj = [[NSObject alloc] init];
NSLog(@"obj=%@", obj);
复制代码
以下为该源代码的输出结果,其中用 %@ 输出 nil。
obj=(null)
复制代码
这次我们再用附有 __weak 修饰符的变量来确认另一功能:使用附有 __weak 修饰符的变量,即是使用注册到 autoreleasepool 中的对象。
{
id __weak obj1 = obj;
NSLog(@"%@", obj1);
}
复制代码
该源代码可转换为如下形式:
/* 编译器的模拟代码 */
id objl
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);
复制代码
与被赋值时相比,在使用附有 __weak 修饰符变量的情形下,增加了对 objc_loadWeakRetained 函数和 objc_autorelease 函数的调用。这些函数的动作如下:
- objc_loadWeakRetained 函数取出附有 __weak 修饰符变量所引用的对象并 retain。
- objc_autorelease 函数将对象注册到 autoreleasepool 中。
由此可知,因为附有 __weak 修饰符变量缩阴用的对象像这样被注册到 autoreleasepool 中,所以在 @autoreleasepool 块结束之前都可以放心使用。但是,如果大量地使用附有 __weak 修饰符的变量,注册到 autorelease 的对象也会大量地增加,因此在使用附有 __weak 修饰符的变量时,最好先暂时赋值给附有 __weak 修饰符的变量后再使用。
{
id __weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);
}
复制代码
相应地,变量 o 所赋值的对象也就注册到 autoreleasepool 中 5 次。
将附有 __weak 修饰符的变量 o 赋值给附有 __strong 修饰符的变量后再使用可以避免此类问题。
{
id __weak o = obj;
id tmp = o;
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
NSLog(@"5 %@", tmp);
}
复制代码
在 "tmp = 0;" 时对象仅登录到 autoreleasepool 中1次。
在 iOS 4 和 OS X Snow Leopard 中是不能使用 __weak 修饰符的,而有时在其他环境下也不能使用。实际上存在着不支持 __weak 修饰符的类。
例如 NSMachPort 类就是不支持 __weak 修饰符的类。这些类重写了 retain/release 并实现该类肚子的引用计数机制。但是赋值以及使用附有 __weak 修饰符的变量都必须恰当地使用 objc4 运行时库中的函数,因此独自实现引用计数机制的类大多不支持 __weak 修饰符。
不支持 __weak 修饰符的类,在其类声明中附加了 "__attribute ((objc_arc_weak_reference_unavailable))" 这一属性,同时定义了 NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE。如果将不支持 __weak 声明类的对象赋值给附有 __weak 修饰符的变量,那么一旦编译器检验出来就会报告编译错误。而且在 Cocoa 框架类中,不支持 __weak 修饰符的类极为罕见,因此没有必要太过担心。
objc_initWeak
objc_initWeak 方法的实现如下:
/**
* Initialize a fresh weak pointer to some object location.
* It would be used for code like:
*
* (The nil case)
* __weak id weakPtr;
* (The non-nil case)
* NSObject *o = ...;
* __weak id weakPtr = o;
*
* This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
*
* @param location Address of __weak ptr.
* @param newObj Object ptr.
*/
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
复制代码
objc_destroyWeak
objc_destroyWeak 实现如下:
/**
* Destroys the relationship between a weak pointer
* and the object it is referencing in the internal weak
* table. If the weak pointer is not referencing anything,
* there is no need to edit the weak table.
*
* This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
*
* @param location The weak pointer address.
*/
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
复制代码
objc_storeWeak
/**
* This function stores a new value into a __weak variable. It would
* be used anywhere a __weak variable is the target of an assignment.
*
* @param location The address of the weak pointer itself
* @param newObj The new object this weak ptr should now point to
*
* @return \e newObj
*/
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
复制代码
storeWeak 方法实现如下
看了上述代码我们可以知道,objc_initWeak、objc_destroyWeak 和 objc_storeWeak 三个方法本质上都调用了 storeWeak 方法,其实现如下
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
复制代码
__unsafe_unretained
如前所述,以下源代码会引起编译警告。
id __weak obj = [[NSObject alloc] init];
复制代码
这是由于编译器判断生成并持有的对象不能继续持有。附有 __unsafe_unretained 修饰符的变量又如何呢?
id __unsafe_unretained obj = [[NSObject alloc] init];
复制代码
与 __weak 修饰符完全相同,编译器判断生成并持有的对象不能继续持有,从而发出警告。
该源代码通过编译器转换为以下形式。
/* 编译器的模拟代码 */
id obj = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
复制代码
objc_release 函数立即释放了生成并持有的对象,这样该对象的悬垂指针被赋值给变量 obj 中。
那么如果最初不赋值变量又会如何呢?下面的源代码在 ARC 无效时必定会发生内存泄漏。
[[NSObject alloc] init];
复制代码
由于源代码不使用返回值的对象,所以编译器发出警告。
可像下面这样通过向 void 型转换来避免发生警告。
(void)[[NSObject alloc] init];
复制代码
不管是否转换为 void,该源代码都会转换为以下形式。
/* 编译器的模拟代码 */
id tmp = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_release(tmp);
复制代码
虽然没有指定赋值变量,但与赋值给附有 __unsafe_unretained 修饰符变量的源代码完全相同。由于不能继续持有生成并持有的对象,所以编译器生成了立即调用 objc_release 函数的源代码。而由于 ARC 的处理,这样的源代码也不会造成内存泄露。
另外,能调用被立即释放的对象的示例方法吗?
(void)[[[NSObject alloc] init] hash];
复制代码
该源代码可变为如下形式:
/* 编译器的模拟代码 */
id tmp = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_msgSend(tmp, @selector(hash));
objc_release(tmp);
复制代码
在调用了生成并持有对象的实例方法后,该对象被释放。看来“由编译器进行内存管理”这句话应该是正确的。
allowsWeakReference/retainWeakReference 方法
实际上还有一种情况也不能使用 __weak 修饰符。
就是当 allowsWeakReference/retainWeakReference 实例方法(没有写入 NSObject 接口说明文档中)返回 NO 的情况。这些方法的声明如下:
- (BOOL)allowsWeakReference;
- (BOOL)retainWeakReference;
复制代码
在赋值给 __weak 修饰符的变量时,如果赋值对象的 allowsWeakReference 方法返回 NO,程序将异常终止。
即对于所有 allowsWeakReference 方法返回 NO 的类都绝对不能使用 __weak 修饰符。这样的类必定在其参考说明中有所记述。
另外,在使用 __weak 修饰符的变量时,当被赋值对象的 retainWeakReference 方法返回 NO 的情况下,该变量将使用“nil”。如以下的源代码:
{
id __strong obj = [[NSObject alloc] init];
id __weak o = obj;
NSLog(@“1 %@”, o);
NSLog(@“2 %@”, o);
NSLog(@“3 %@”, o);
NSLog(@“4 %@”, o);
NSLog(@“5 %@”, o);
}
复制代码
由于最开始生成并持有的对象为附有 __strong 修饰符变量 obj 所持有的强引用,所以在该变量作用域结束之前都始终存在。因此如下所示,在变量作用域结束之前,没可以持续使用附有 __weak 修饰符的变量 o 所引用的对象。
下面对 retainWeakReference 方法进行实验。我们做一个 MyObject 类,让其继承 NSObject 类并实现 retainWeakReference 方法。
@interface MyObject : NSObject
{
NSUInteger count;
}
@end
@implementation MyObject
- (id)init
{
self = [super init];
return self;
}
- (BOOL)retainWeakReference
{
if (++count > 3)
return NO;
return [super retainWeakReference];
}
@end
复制代码
该例中,当 retainWeakReference 方法被调用4次或4次以上时返回 NO。在之前的源代码中,将从 NSObject 类生成并持有对象的部分更改为 MyObject 类。
{
id __strong obj = [[MyObject alloc] init];
id __weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);
}
复制代码
以下为执行结果。
从第4次起,使用附有 __weak 修饰符的变量 o 时,由于所饮用对象的 retainWeakReference 方法返回 NO,所以无法获取对象。像这样的类也必定在其参考说明中有所记述。
另外,运行时库为了操作 __weak 修饰符在执行过程中调用 allowsWeakReference/retainWeakReference 方法,因此从该方法中再次操作运行时库时,其操作内容会永久等待。原本这些方法并没有计入文档,因此应用程序编程人员不可能实现该方法群,但如果因某些原因而不得不实现,那么还是在全部理解的基础上实现比较好。
__autoreleasing 修饰符
将对象赋值给附有 __autoreleasing 修饰符的变量等同于 ARC 无效时调用对象的 autorelease 方法。我们通过以下源代码来看一下。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
复制代码
该源代码主要将 NSObject 类对象注册到 autoreleasepool 中,可作如下变换:
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
复制代码
这与苹果的 autorelease 实现中的说明完全相同。虽然 ARC 有效和无效时,其在源代码上的表现有所不同,但 autorelease 的功能完全一样。
在 alloc/new/copy/mutableCopy 方法群之外的方法中使用注册到 autoreleasepool 中的对象会如何呢?下面我们来看看 NSMutableArray 类的 array 类方法。
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
复制代码
这与前面的源代码有何不同呢?
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
复制代码
虽然持有对象的方法从 alloc 方法变为 objc_retainAutoreleasedReturnValue 函数,但注册 autoreleasepool 的方法没有改变,仍是 objc_autorelease 函数。