前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS 底层拾遗:autorelease 优化

iOS 底层拾遗:autorelease 优化

作者头像
波儿菜
发布2019-12-23 19:04:39
1.3K0
发布2019-12-23 19:04:39
举报
文章被收录于专栏:iOS技术

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

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

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

代码语言:javascript
复制
@interface TestObject : NSObject <NSCopying>
@end
@implementation TestObject
+ (id)foo {
    return [NSObject new];
}
- (id)copyWithZone:(NSZone *)zone {
    return [NSObject new];
}
@end

一、alloc / new / copy / mutableCopy 方法

编写这样一段代码:

代码语言:javascript
复制
[[NSObject new] copy];

汇编代码为:

代码语言:javascript
复制
...
    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,得到几乎一致的结果,似乎就能得到只要是生成本类实例的方法都不会做优化的结论? 并不能。

将上面的代码换成:

代码语言:javascript
复制
[[TestObject new] copy];

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

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

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

写这样一句代码:

代码语言:javascript
复制
[TestObject foo];

汇编代码为:

代码语言:javascript
复制
...
    bl  _objc_msgSend
Ltmp9:
    mov x29, x29    ; marker for objc_retainAutoreleaseReturnValue
    bl  _objc_unsafeClaimAutoreleasedReturnValue
...

调用方的逻辑

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

代码语言:javascript
复制
id objc_unsafeClaimAutoreleasedReturnValue(id obj) {
    if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;
    return objc_releaseAndReturn(obj);
}

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

代码语言:javascript
复制
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,直接分析汇编代码看是否有什么奇怪操作:

代码语言:javascript
复制
...
    bl  _objc_msgSend
    ldp x29, x30, [sp], #16
    b   _objc_autoreleaseReturnValue
...

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

代码语言:javascript
复制
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,也就是省去了加入自动释放池的时间消耗、避免对象对自动释放池的内存占用。

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

代码语言:javascript
复制
id obj = [TestObject foo];

汇编代码有些变化:

代码语言:javascript
复制
...
    bl  _objc_msgSend
    mov x29, x29    ; marker for objc_retainAutoreleaseReturnValue
    bl  _objc_retainAutoreleasedReturnValue
    bl  _objc_release
...

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

代码语言:javascript
复制
id objc_retainAutoreleasedReturnValue(id obj) {
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
    return objc_retain(obj);
}

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

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

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

看关键的一个判断:

代码语言:javascript
复制
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) ...

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

代码语言:javascript
复制
mov x29, x29    ; marker for objc_retainAutoreleaseReturnValue

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

代码语言:javascript
复制
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上的定义却有所变化:

代码语言:javascript
复制
__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);
}
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 编译器代码才能真正的有说服力。不过从理解原理的角度来说按图索骥是个非常好的学习方式,希望本文能给读者朋友带来一些帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、alloc / new / copy / mutableCopy 方法
  • 二、自定义带返回值的方法
    • 调用方的逻辑
      • 被调用方的逻辑
        • 核心逻辑分析
          • 如何判断 autorelease 是否需要优化?
            • MRC 模式下是否开启了优化
              • 为什么要双方协商 autorelease 优化?
                • 为什么使用线程局部存储?
                • 后语
                相关产品与服务
                SSL 证书
                腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档