再谈 ARC 和 autorelease

2,513 阅读5分钟
原文链接: zhuanlan.zhihu.com

最近在学习 Objective-C 的 MRC 内存管理方式,由于之前一直用 ARC,很多规则都不是很清楚,需要反编译看编译器 emit 的指令来分析对象的 retain、release 时机。

”Basic Memory Management Rules“

You own any object you create
You create an object using a method whose name begins with “alloc”, “new”, “copy”, or “mutableCopy” (for example, alloc, newObject, or mutableCopy).

You can take ownership of an object using retain
A received object is normally guaranteed to remain valid within the method it was received in, and that method may also safely return the object to its invoker. You use retain in two situations: (1) In the implementation of an accessor method or an init method, to take ownership of an object you want to store as a property value; and (2) To prevent an object from being invalidated as a side-effect of some other operation (as explained in Avoid Causing Deallocation of Objects You’re Using).

When you no longer need it, you must relinquish ownership of an object you own
You relinquish ownership of an object by sending it a release message or an autorelease message. In Cocoa terminology, relinquishing ownership of an object is therefore typically referred to as “releasing” an object.

You must not relinquish ownership of an object you do not own
This is just corollary of the previous policy rules, stated explicitly.

Apple 官方文档中规定了几个基本准则,概括来说就是:

自己产生的垃圾自己收拾,别人的东西别馋和。

哪类垃圾属于自己产生的呢?基本上就是使用 alloc 或 CoreFoundation 中 XxxxxxCreate 函数所返回的对象。这些对象创建时的 retain count 是 1 且没有被放入 Autorelease Pool 中。因此在使用完毕之后需要自行 release

别人的东西又属于那些呢?基本上就是 getter 方法获取到的对象,或者通过非 alloc、init 类的类方法创建的对象,比如 arrayWithObject: 方法。这类方法返回的对象不能被使用者 release,因为它们已经被 autorelease 过了,或者 receiver 持有它们作为实例属性或 ivars。

但是如果使用者需要长期保留”别人的东西“,那就需要自己 retain 了,当然,使用完毕后还要 release。

ARC & MRC 交火

根据上面的规则,MRC 中一个十分重要的约定就是,getter 方法或者非 alloc、init 类构造方法返回的对象处于 receiver,那 ARC 与 MRC 混用时就会出现一个问题,我们假设下面的场景:

Foo 是一个使用了 ARC 的 Framework 里的一个类,它有一个 fooWithIdentifier: 方法返回一个 Foo 实例(根据上面的规则,这个对象不属于 caller)。

然后在一个 MRC 项目中使用这个 Foo 类,由于开发者知道 fooWithIdentifier: 这类方法返回的对象会被 autorelease,所以他在使用完这个对象后并没有再次调用 release。

问题发生了,上面的 Foo 实例 leaks 了。

由于 ARC 的内存管理语义是编译器自动插入的,那么 fooWithIdentifier: 在编译时理应没有插入 autorelease,因为编译器知道,调用者在对象使用完毕时已经插入了 release 指令。

然而我们实际开发中,这样 ARC、MRC 框架混用是没有任何问题的,为什么?

我们反编译一下上述方法:

这方法只创建了 Foo 对象,没有做其他操作,然后返回被创建的对象,可以看到最后被插入了 objc_autoreleaseReturnValue 这个函数调用,它是干什么的呢?这个函数的实现可以在 Objective-C Runtime 的源码中找到:

id 
objc_autoreleaseReturnValue(id obj)
{
#if SUPPORT_RETURN_AUTORELEASE
    assert(tls_get_direct(AUTORELEASE_POOL_RECLAIM_KEY) == NULL);

    if (callerAcceptsFastAutorelease(__builtin_return_address(0))) {
        tls_set_direct(AUTORELEASE_POOL_RECLAIM_KEY, obj);
        return obj;
    }
#endif

    return objc_autorelease(obj);
}

首先这个函数调用了 callerAcceptsFastAutorelease 函数,并传入了 __builtin_return_address(0) 的结果作为参数。

首先解释一下 __builtin_return_address 这个函数,它是一个编译器内置函数,用于获取函数返回时地址,它的参数是函数调用的层级,0 代表第一层,也就是当前函数。

接下来来看 callerAcceptsFastAutorelease 这个函数:

/*
  Fast handling of returned autoreleased values.
  The caller and callee cooperate to keep the returned object 
  out of the autorelease pool.

  Caller:
    ret = callee();
    objc_retainAutoreleasedReturnValue(ret);
    // use ret here

  Callee:
    // compute ret
    [ret retain];
    return objc_autoreleaseReturnValue(ret);

  objc_autoreleaseReturnValue() examines the caller's instructions following
  the return. If the caller's instructions immediately call
  objc_autoreleaseReturnValue, then the callee omits the -autorelease and saves
  the result in thread-local storage. If the caller does not look like it
  cooperates, then the callee calls -autorelease as usual.

  objc_autoreleaseReturnValue checks if the returned value is the same as the
  one in thread-local storage. If it is, the value is used directly. If not,
  the value is assumed to be truly autoreleased and is retained again.  In
  either case, the caller now has a retained reference to the value.

  Tagged pointer objects do participate in the fast autorelease scheme, 
  because it saves message sends. They are not entered in the autorelease 
  pool in the slow case.
*/

// 以下只截取 arm 架构的实现

static bool callerAcceptsFastAutorelease(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
        if (*(uint16_t *)((uint8_t *)ra - 1) == 0x463f) {
            return true;
        }
    } else {
        // 07 70 a0 e1    mov r7, r7
        if (*(uint32_t *)ra == 0xe1a07007) {
            return true;
        }
    }
    return false;
}

如果看不懂汇编指令,上面的注释也能解释得很清楚,这个函数检查上面函数返回地址下的指令,如果发现 mov r7, r7(which 作为 ARC 的标志)就返回 true。

所以 objc_autoreleaseReturnValue 的作用就是区分处理 ARC 和 MRC 两种环境下的内存管理。

然后我们就可以梳理一下这两种情况的处理方法了。

1. 对于调用方是 ARC 的情况:

将对象放入 TLS(Thread Local Storage,AUTORELEASE_POOL_RECLAIM_KEY 的值用来表示当前调用栈的返回值) 里,然后直接返回。

调用方紧接着会把返回的对象当做参数来调用 objc_retainAutoreleasedReturnValue 这个函数,它会检查 TLS 中的值,如果跟返回值相同,那么不做任何操作;如果不同,则做 retain 操作。

这样一来,ARC 框架返回的对象,返回时 retain count 为 1(假设对象刚创建),既不 autorelease 也不 retain。调用方接收时不 retain,在使用完毕后调用 release,对象释放。

2. 对于调用方是 MRC 的情况:

将对象做 autorelease 操作,然后返回,调用者拿到后根据约定,不再做任何内存管理的操作。对象在 Run Loop 睡眠前被释放。


References:

[1] Memory Management Policy

[2] Using the GNU Compiler Collection (GCC): Return Address

[3] NSObject.mm - opensource.apple.com

[4] Objective-C Automatic Reference Counting (ARC)