iOS 底层拾遗:autorelease 优化

前言

听闻 ARC 下 autorelease 操作有一些优化,总感觉云里雾里的,笔者初略的探究了一番,记录下来变成这篇水文。

由于 ARC 下 retain/release/autorelease 的调用都是编译器代劳,所以需要使用编译后的代码进行分析,通常笔者选择 Xcode 自带的工具,它有一个优势是自动将一些符号地址改为符号名,并且可以选择 Running 或 Archiving 下的汇编代码,后者生成的代码往往是前者的优化版本。

本文基于 Runtime 750,arm64,Archiving 汇编代码。

先前置声明一个后文会用到的类:

@interface TestObject : NSObject <NSCopying>
@end
@implementation TestObject
+ (id)foo {
    return [NSObject new];
}
- (id)copyWithZone:(NSZone *)zone {
    return [NSObject new];
}
@end

一、alloc / new / copy / mutableCopy 方法

编写这样一段代码:

[[NSObject new] copy];

汇编代码为:

...
    adrp    x8, l_OBJC_CLASSLIST_REFERENCES_$_@PAGE
    ldr x0, [x8, l_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF]
    adrp    x8, l_OBJC_SELECTOR_REFERENCES_@PAGE
    ldr x1, [x8, l_OBJC_SELECTOR_REFERENCES_@PAGEOFF]
    bl  _objc_msgSend
    mov x19, x0
    adrp    x8, l_OBJC_SELECTOR_REFERENCES_.6@PAGE
    ldr x1, [x8, l_OBJC_SELECTOR_REFERENCES_.6@PAGEOFF]
    bl  _objc_msgSend
    bl  _objc_release
    mov x0, x19
    bl  _objc_release
...

...@PAGE / ...@PAGEOFF两句一起出现表示通过页基址+符号在页中偏移来计算地址。首先把NSObject类地址和new方法地址找到分别放入x0x1,然后调用_objc_msgSend,调用完成后x0里面放的就是[NSObject new]得到的对象地址,所以后面直接找到copy方法调用。这一段基本分析后文不会再详细描述了。

由于newcopy将引用计数+2,其后调用了两次_objc_release,这很符合直觉,看起来编译器并未做什么优化。

尝试使用allocmutableCopy,得到几乎一致的结果,似乎就能得到只要是生成本类实例的方法都不会做优化的结论? 并不能。

将上面的代码换成:

[[TestObject new] copy];

发现编译后的汇编代码仍然和上面的差不多,而此时TestObjectcopy返回了另外类的实例,退一步讲,前面的NSObjectcopy方法也并未实现,所以可以猜测:

编译器不是通过返回类型来判断的,而是通过简单的符号匹配,发现alloc/new/copy/mutableCopy符号就不做优化。

二、自定义带返回值的方法

写这样一句代码:

[TestObject foo];

汇编代码为:

...
    bl  _objc_msgSend
Ltmp9:
    mov x29, x29    ; marker for objc_retainAutoreleaseReturnValue
    bl  _objc_unsafeClaimAutoreleasedReturnValue
...

调用方的逻辑

调用_objc_msgSendx0 -> TestObject, x1 -> foo,这里出现了一个不符合直觉的 C 函数_objc_unsafeClaimAutoreleasedReturnValue,光看名字猜不到具体是干嘛的,所以去掉 C 语言符号修饰_,直接查看源码:

id objc_unsafeClaimAutoreleasedReturnValue(id obj) {
    if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;
    return objc_releaseAndReturn(obj);
}

objc_releaseAndReturn函数不展开,只需知道是真正的release操作。那么大家就奇怪了,理论上foo方法会把返回值先加入自动释放池,这里根本就不需要release,常理来说只有当这个acceptOptimizedReturn() == ReturnAtPlus0为 YES 不做release操作才与大家的意识契合。那么看一下这个关键方法:

enum ReturnDisposition : bool {
    ReturnAtPlus0 = false, ReturnAtPlus1 = true
};
static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn() {
    ReturnDisposition disposition = getReturnDisposition();
    setReturnDisposition(ReturnAtPlus0);  // reset to the unoptimized state
    return disposition;
}
static ALWAYS_INLINE ReturnDisposition getReturnDisposition() {
    return (ReturnDisposition)(uintptr_t)tls_get_direct(RETURN_DISPOSITION_KEY);
}
static ALWAYS_INLINE void setReturnDisposition(ReturnDisposition disposition) {
    tls_set_direct(RETURN_DISPOSITION_KEY, (void*)(uintptr_t)disposition);
}

看到最终是使用tls_get_direct(RETURN_DISPOSITION_KEY)取的 TLS(Thread Local Storage)里的值,取了后还把这个值变为false。看起来这里的代码非常简单,我们只需要关心是 何时把 TLS 对应的值变成true

被调用方的逻辑

由于刚才分析了,foo函数会将生成的对象放入自动释放池,这里理论上不需要release,直接分析汇编代码看是否有什么奇怪操作:

...
    bl  _objc_msgSend
    ldp x29, x30, [sp], #16
    b   _objc_autoreleaseReturnValue
...

发现调用了_objc_autoreleaseReturnValue函数,切到源码:

id objc_autoreleaseReturnValue(id obj) {
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
    return objc_autorelease(obj);
}
static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) {
    assert(getReturnDisposition() == ReturnAtPlus0);
    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }
    return false;
}

objc_autorelease就是真正的autorelease操作,会将对象加入自动释放池。若if判断成功会调用了一个前面分析过的方法setReturnDisposition(...)将 TLS 对应 Key 的值设置为true,如果设置成功后将放弃autorelease操作。

核心逻辑分析

这里先考虑做了优化的情况。

重点分析: 若这里不进行autorelease,调用foo后生成的对象将不会被自动释放池管理,这个对象的引用计数为 1。那之前的objc_unsafeClaimAutoreleasedReturnValue(...)函数就需要进行release操作,[TestObject foo]生成对象的引用计数才能减为 0。对比前面分析完全符合,至此,形成了逻辑通路。

优化点: 这个场景少了一步autorelease,多了一步release,也就是省去了加入自动释放池的时间消耗、避免对象对自动释放池的内存占用。

另外一个场景,代码如下:

id obj = [TestObject foo];

汇编代码有些变化:

...
    bl  _objc_msgSend
    mov x29, x29    ; marker for objc_retainAutoreleaseReturnValue
    bl  _objc_retainAutoreleasedReturnValue
    bl  _objc_release
...

笔者只是加了一个临时变量持有这个返回值,直觉上会调用_objc_retain然后调用_objc_release,但先调用的是_objc_retainAutoreleasedReturnValue

id objc_retainAutoreleasedReturnValue(id obj) {
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
    return objc_retain(obj);
}

foo方法若不会将创建的对象进行autorelease,那么这里也就不需要进行retain了,很好理解。

优化点: 这个场景少了一步autorelease,少了一步retain,优化效果就变得明显了。

如何判断 autorelease 是否需要优化?

看关键的一个判断:

if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) ...

__builtin_return_address(n)拿到的是前第 n + 1 函数栈的lr (Link Register) ,那么这里就是拿到上一个栈的lr(函数在调用其它函数时会将下一句指令地址写入lr便于恢复执行),也就是前面的这句汇编代码(别疑惑函数栈这么深怎么会拿到第一个函数的lr,因为这些函数都是内联的):

mov x29, x29    ; marker for objc_retainAutoreleaseReturnValue

看起来这句代码什么也没做,先看一下这个判断函数:

static ALWAYS_INLINE bool callerAcceptsOptimizedReturn(const void *ra) {
    // fd 03 1d aa    mov fp, fp
    // arm64 instructions are well-aligned
    if (*(uint32_t *)ra == 0xaa1d03fd) {
        return true;
    }
    return false;
}

这个函数就是将ra指向的值和0xaa1d03fd对比,若相等就执行优化。注释已经比较明白了,这个值就是mov fp, fp的十六进制表示(注意 iOS 是小端)。

那么只要被调用方拿到调用方的lr判断就行了,所以是否进行这个优化的决定权在调用方手中。

MRC 模式下是否开启了优化

将代码设置为-fno-objc-arc,随便写些代码查看汇编,发现编译器并不会将开发者显式写的objc_autorelease/objc_retain/objc_release函数强制改为objc_autoreleaseReturnValue等方法,且 MRC 下编译的代码在调用方法时不会加入mov fp fp企图优化(推理也可知,因为retain/release操作是不能优化的)。

objc_retain/objc_release就是直接进行引用计数加减,而objc_autoreleaseOBJC2上的定义却有所变化:

__attribute__((aligned(16))) id objc_autorelease(id obj) {
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}
inline id objc_object::autorelease() {
    if (isTaggedPointer()) return (id)this;
    if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
inline id objc_object::rootAutorelease() {
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    return rootAutorelease2();
}

若当前类没有自定义的retain/release方法实现时,最终仍然会调用prepareOptimizedReturn(ReturnAtPlus1)进行优化,所以当调用方是 ARC,被调用方是 MRC 时这个优化仍然有效。

为什么要双方协商 autorelease 优化?

总结一下: 不管被调用方是 MRC 还是 ARC,进行autorelease操作时都会尝试去优化,但是只有调用方是 ARC 时才能优化成功。

那么协商的意义就很重要了,这样才能保证版本兼容以及 MRC 和 ARC 的兼容。

为什么使用线程局部存储?

一个线程可以理解为一个一个顺序执行指令的,那么用一个 bool 值就能解决线程执行的所有代码的优化。

TLS 是线程私有的,所以 autorelease 的优化是线程间互不影响的,引用计数的加减线程安全由相应的函数保证。若这个优化的控制是多线程共同制约的,那么单纯一个 bool 值是实现不了的,需要更复杂的数据结构,并且要额外处理线程并发的安全问题,这些工作反而会降低程序执行的效率。

后语

本文通过探索的方式分析了 autorelease 的优化逻辑,实际上并不能铁板钉钉的说明事实,只有通过查看 clang 编译器代码才能真正的有说服力。不过从理解原理的角度来说按图索骥是个非常好的学习方式,希望本文能给读者朋友带来一些帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 29.理解引用计数 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数...
    Code_Ninja阅读 1,421评论 1 3
  • 内存的引用   计算机是按地址访问数据,如果一块内存被使用,就必须让外界知道它在哪儿,反过来讲,要访问一个数据就必...
    吸血鬼de晚餐阅读 1,952评论 0 5
  • 37.cocoa内存管理规则 1)当你使用new,alloc或copy方法创建一个对象时,该对象的保留计数器值为1...
    如风家的秘密阅读 768评论 0 4
  • 家庭教育的名词解释
    操碎心的老豆阅读 470评论 0 10