返回值的 Autorelease 和 编译器优化

本文章基于 objc4-725 进行测试
objc4的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到

事情的起因: ARC下 的 @autoreleasepool

之前学习到自动内存管理下 @autoreleasepool {} 的用法, 这两天突然想起来, 又测试了一下, 写了如下的代码:

__weak NSArray * weakArray;

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 100; ++i) {
        {
            NSArray * array = [NSArray arrayWithObject:@"123"];
            weakArray = array;
        }
        NSLog(@"%@", weakArray);
    }
}

不过结果有点出乎了我的预料, 输出全部是 null, 说好的工厂方法返回 autorelease 属性的对象呢?
然后不知道为什么, 我把 weak 属性的对象 weakArray, 放到了 viewDidLoad 里面, 神奇的事情就发生了, 输出就都变成了 123.

于是就想了解一下 ARC, 想知道 ARC到底为返回值做了些什么. 目前得到了一个初步的认识, 想先分享给大家, 至于为什么全局的 weak 指针最后输出是 nil, 局部的 weak 指针最后输出有值这个原因, 是编译器优化的结果, 最终会在编译器优化部分解释.

经过后续测试, 如果使用 NSArray * array = [NSArray arrayWithObjects:@"123", nil] 初始化, 则本例中的编译器优化不会触发.

ARC 自动为返回值添加的 autorelease 相关操作

本部分是未进行编译器优化时的操作.
测试了很久, 试了很多就不一一列举了, 直接上结果.

- (void)test {
    __weak NSObject * weakObject;
    {
        NSObject * object = [self object];
        weakObject = object;
    }
    NSLog(@"%@", weakObject);
}

- (NSObject *)object {
    return [[NSObject alloc] init];
}

我这里最终输出了

2018-12-07 10:10:03.276297+0800 iOS-Test-Demo[62002:4059722] <NSObject: 0x600000b702e0>

接下来我在网络上找到了一个将 .m 文件编译成 llvm 中间语言的一个命令:

sudo xcrun -sdk iphonesimulator clang -S -fobjc-arc -emit-llvm TestObject.m

执行过这个命令后, 我得到了一个叫做 TestObject.ll 的文件, 这个文件里面是一种类似于汇编的语言. 我截取了最重要的部分, 并把它写成伪代码的形式便于观察:

- (void)test {
    __weak NSObject * weakObject;
    {
        NSObject * ret = objc_msgSend(self, @selector(object));  //调用object返回 autorelease 类型的 ret
        NSObject * object = objc_retainAutoreleasedReturnValue(ret);  //将 ret 进行一次 retain 并赋值给 object
        objc_storeWeak(weakObject, object);  //将 object 直接赋值给 weakObject
        objc_storeStrong(object, null);  //出作用域之前的置空
    }
    NSLog(@"%@", weakObject);
    //结束后等 autoreleasepool 回收
}

- (NSObject *)object {
    NSObject * object = objc_msgSend(NSObject, @selector(alloc));
    objc_msgSend(object, @selector(init));
    NSObject * ret = objc_autoreleaseReturnValue(object);  //返回之前, 用该方法加入到了 autoreleasepool 中
    return ret;
}

objc_autoreleaseReturnValue() 和 objc_retainAutoreleasedReturnValue() 是成对出现的, 前者将对象加入到 autoreleasepool, 后者将 autorelease 类型对象 retain. 初始化的对象引用计数是1, retain 后变为2, 所以 object 出作用域的时候, weakObject 还可以继续使用.

以上就是编译期为返回值做的 autorelease 相关的处理, 另外 init 和 copy 开头的函数, 不会进行这样的处理.

编译器优化

返回值优化主要靠 objc_autoreleaseReturnValue() 和 objc_retainAutoreleasedReturnValue() 两个函数, 而编译器优化也是针对这两个方法进行的优化. 我们看一下这两个函数的源码:

// 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);
}

// 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);
}

可以看到两个函数均有一个判断, 这个判断就是编译器优化的关键.
objc_autoreleaseReturnValue() 和 objc_retainAutoreleasedReturnValue() 两个函数成对出现就是因为各自内部调用的 prepareOptimizedReturn() 和 acceptOptimizedReturn(), 前者是提供一个优化过的返回值, 后者是判断该返回值是否优化过了.
prepareOptimizedReturn() 有 bool 类型的返回值, 代表是否优化完成. 还有一个 ReturnDisposition 类型的枚举:

enum ReturnDisposition : bool {
    ReturnAtPlus0 = false, ReturnAtPlus1 = true
};

代表优化设置, ReturnAtPlus0 即为优化时引用计数 +0, 而 ReturnAtPlus1 即为优化时引用计数 +1.

本次测试的例子, prepareOptimizedReturn() 是以 ReturnAtPlus1 为优化设置.
优化成功后调用 acceptOptimizedReturn() 将会返回对应的优化设置, 也就是 ReturnAtPlus1, 此时 objc_retainAutoreleasedReturnValue() 函数会直接返回 obj 不会对其 retain.
优化失败则 acceptOptimizedReturn() 将会返回 ReturnAtPlus0, 此时 objc_retainAutoreleasedReturnValue() 函数会对 obj 进行retain.

另外

// 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;
}

prepareOptimizedReturn() 函数中主要通过 callerAcceptsOptimizedReturn() 函数来判断是否需要或者是否可以进行优化, 该函数的参数 __builtin_return_address(0)) 代表当前函数地址. 这个函数涉及更底层的东西, 目前还没有去接触.
但是通过对该函数的注释, 大致得出了这个函数的实现就是通过一系列对帧指针寄存器的判断来判断该地址指向的函数是否可进行优化, 还有优化的方案叫做 Tread Local Storage, TLS, 线程本地存储, 是线程对应的一块地址空间, 对返回值的优化就是将返回值存入到该段空间中, 避免了加入到 autoreleasepool 中的一系列操作.

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