ARC环境下编译器到底对autorelease对象做了怎样的优化

如何将OC的源文件编译成C++文件

<br /> 如何用clang -rewrite-objc重写OC源文件

在执行完命令clang -rewrite-objc ViewController.m之后,相信很多人和我一样会报如下错误:

./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
import <UIKit/UIKit.h>
^
1 error generated.

那么如何解决呢,其实上面的链接里也有提到:

xcrun -sdk iphonesimulator10.0 clang -rewrite-objc ViewController.m

执行了这行命令之后,OK,没有报错。在同级文件夹中会看到一个ViewController.cpp的C++文件,这就是编译之后的C++代码。里面的代码非常多,但是关于我们自己实现的几个方法的代码其实并不多。比如重写之前的OC方法:

+ (instancetype)createFoo {
    return [self new];
}

重写之后的C++方法:

static instancetype _C_Foo_createFoo(Class self, SEL _cmd) {
    return ((id (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("new"));
}

是不是有点失望,是不是有这种想法:不就是objc_msgSend这个方法嘛,这个我早就知道了...

不过不要着急,看了孙源的黑幕背后的Autorelease,由于好奇对于autorelease对象编译器到底做了怎样的优化,顺便对于一个对象不同创建方式以及外部是否对其有引用的不同状况下(ARC环境),编译器到底帮我们插入了哪些代码,做了一些探究。

回忆一下MRC下创建对象的两种方式

1.自己生成的对象,自己持有
alloc/new/copy/mutableCopy方法或者以alloc/new/copy/mutableCopy开头的驼峰式方法名,也生成并持有对象,形如:

id object = [NSObject alloc] init];

2.不是自己生产的对象,自己持有
alloc/new/copy/mutableCopy方法或者以alloc/new/copy/mutableCopy开头的驼峰式方法之外的其他方法生成的对象,形如:

id array = [NSMutableArray array];
[array retain];

这种方法new出来的对象因为不能return之后立马释放,这样外部引用者根本没有引用到它的机会,但是又不能不释放,那样会造成内存泄漏,所以这种方式new出来的对象会被加入到AutoreleasePool中,主线程是默认在当前runloop结束的时候统一对加入到自动释放池中的对象发送release消息,如果当前这个对象引用计数为1的话那么它就将被销毁。这样就巧妙的解决了上面提到的两个问题

那么这两种方式在ARC下有什么区别呢?有没有外部引用(局部变量或者本类的实例变量)也是一样的么?

从汇编代码里寻找蛛丝马迹

首先新建一个OC的源文件,其中有Foo和ViewController两个类,源码如下:

@interface Foo : NSObject

+ (instancetype)createFoo;
+ (instancetype)newFoo;

@end

@implementation Foo

+ (instancetype)createFoo {
    return [self new];
}

+ (instancetype)newFoo {
    return [self new];
}

@end

@interface ViewController ()

@property (nonatomic, strong) Foo *foolish;

@end

@implementation ViewController

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self testFoo];
}
- (void)testFoo {
    //to be tested
}

通过Xcode的Product->Perform Action->Assemble“ViewController.m”,我们可以看到OC源文件最终被编编译生产的汇编代码,这里就能详细的查看到底编译器在我们的代码背后插入了哪些代码:

1.创建时不持有,外部不引用
形如:

- (void)testFoo {
    [Foo createFoo];
}

对于Foo类的方法 createFoo

"+[Foo createFoo]":
Lfunc_begin0:
 .loc 1 21 0
 .cfi_startproc
@ BB#0:
 push {r7, lr}
 mov r7, sp
 sub sp, #8
 @DEBUG_VALUE: +[Foo createFoo]:self <- [%SP+4]
 @DEBUG_VALUE: +[Foo createFoo]:_cmd <- [%SP+0]
 str r0, [sp, #4]
 str r1, [sp]
 .loc 1 22 13 prologue_end    
Ltmp0:
 ldr r0, [sp, #4]
 .loc 1 22 12 is_stmt 0       
 movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
 movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
LPC0_0:
 add r1, pc
 ldr r1, [r1]
 bl _objc_msgSend//调用new方法
Ltmp1:
 .loc 1 22 5
 add sp, #8
 pop.w {r7, lr}
 b.w _objc_autoreleaseReturnValue//孙源博客里提到的优化,下面详细说 
Ltmp2:
Lfunc_end0:

然后再看ViewController类的testFoo方法:

"-[ViewController testFoo]":
Lfunc_begin3:
 .loc 1 44 0                  
 .cfi_startproc
@ BB#0:
 push {r7, lr}
 mov r7, sp
 sub sp, #12
 movw r2, :lower16:(L_objc_msgSend$non_lazy_ptr-(LPC3_0+4))
 movt r2, :upper16:(L_objc_msgSend$non_lazy_ptr-(LPC3_0+4))
LPC3_0:
 add r2, pc
 ldr r2, [r2]
 movw r3, :lower16:(L_OBJC_SELECTOR_REFERENCES_.7-(LPC3_1+4))
 movt r3, :upper16:(L_OBJC_SELECTOR_REFERENCES_.7-(LPC3_1+4))
LPC3_1:
 add r3, pc
 movw r9, :lower16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC3_2+4))
 movt r9, :upper16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC3_2+4))
LPC3_2:
 add r9, pc
 str r0, [sp, #8]
 str r1, [sp, #4]
 .loc 1 45 6 prologue_end
Ltmp7:
 ldr.w r0, [r9]
 ldr r1, [r3]
 blx r2
 @ InlineAsm Start
 mov r7, r7
 @ InlineAsm End
 bl _objc_unsafeClaimAutoreleasedReturnValue
 .loc 1 46 1                  
 str r0, [sp]                @ 4-byte Spill
 add sp, #12
 pop {r7, pc}
Ltmp8:
Lfunc_end3:

简化后编译器做的优化为:

+ (instancetype)createFoo {
    id tmp = [self new];  
    return objc_autoreleaseReturnValue(tmp); 
} 

- (void)testFoo { 
    objc_unsafeClaimAutoreleasedReturnValue([Foo createFoo]); 
}

其中objc_autoreleaseReturnValue方法的作用相当于代替我们手动调用autorelease,在TLS(Tread Local Storage),其实就是系统为当前线程分配的一块栈空间,以K/V的形式存放一些数据,比如这里的ARC对autorelese对象的优化标记,还有AutoreleasePool中双向链表结构中最后一页的hotPage等。这个方法其实就是先判断一下调用方外部是不是ARC环境,如果是的话就不对当前对象调用autorelease方法,而是直接返回,同时在TLS中做一个已优化的标记

因为外部没有对象持有,所以被创建出来的Foo对象应该被销毁,objc_unsafeClaimAutoreleasedReturnValue的作用就是先对被new出来的Foo对象发送 objc_release方法,同时把TLS里关于autorelease已优化的标记清空,相当于MRC下我们手动对foo对象调用release方法。具体实现可以看最新版的runtime源码

2.创建时不持有,外部局部变量引用

形如:

- (void)testFoo {
    id foo = [Foo createFoo];
}

鉴于汇编代码比较冗长,之后都省略,只发简化之后的伪代码

编译器优化为:

+ (instancetype)createFoo  {
    id tmp = [self  new]; 
    return objc_autoreleaseReturnValue(tmp); //解释同上
} 

- (void)testFoo { 
    id tmp = objc_retainAutoreleasedReturnValue([Foo createFoo]); 
    objc_storeStrong(&tmp,nil); 
}

其中objc_storeStrong方法的实现如下:

void objc_storeStrong(id *location, id obj) {
    id prev = *location;   
    if (obj == prev) {
        return;    
    }    
    objc_retain(obj);    
    *location = obj;    
    objc_release(prev);
}

objc_retainAutoreleasedReturnValue 的作用就是在TLS里查询关于autorelease优化的标记,如果有就直接返回不再对对象再retain

objc_storeStrong(&tmp,nil) 则相当于对Foo对象发送release消息,同时将指针置空

想想在MRC下我们如果想引用一个autorelease的对象是如何做的,是不是先retain,然后再在自身作用域结束的时候release一下,从整体来看,假如外部只有当前这一处在引用它的话,这个对象需要经历new->autorelease->retain->release这样四步流程,而ARC环境下编译器优化的思路其实很简单,既然如此,那中间的autorelase和ratain这两步是不是都可以省略呢,这样显然会减少开销,提高效率。

3.创建时不持有,外部实例变量引用

形如:

- (void)testFoo {
    self.foolish = [Foo createFoo];
}

编译器编译为:

+ (instancetype)createFoo  {
    id tmp = [self  new]; 
    return objc_autoreleaseReturnValue(tmp); //解释同上
}

- (void)testFoo {
    id tmp = _objc_retainAutoreleasedReturnValue([Foo createFoo]); 
    [self setFoolish:temp];
    objc_release(temp);
}

其中setFoolish方法的实现如下:

- (void)setFoolish:(Foo *foo) {
    objc_storeStrong(&_foolish,foo);
}

objc_storeStrong(&_foolish,foo)相当于retain foo,这样foo对象的所有权就转交给当前vc对象的_foolish这个实例变量了,只有在当前vc对象被销毁的时候,这个引用才会被断开,foo对象才能够被销毁

4.创建时持有,外部不引用

形如:

- (void)testFoo {
    [Foo newFoo];
}

这个时候编译器编译后的代码就比较简单了:

+ (instancetype)newFoo {
    return [self new];
}

- (void)testFoo {
    objc_release([Foo newFoo]) ;
}

5.创建时持有,外部局部变量引用

形如:

- (void)testFoo {
    id foo = [Foo newFoo];
}

编译后代码为:

+ (instancetype)newFoo {
    return [self new];
} 

- (void)testFoo {
    id temp = [Foo newFoo];
   objc_storeStrong(&tmp,nil);//相当于release
}

6.创建时持有,外部实例变量引用

形如:

- (void)testFoo {
    self.foolish = [Foo newFoo];
}

编译器编译为:

+ (instancetype)newFoo {
    return [self new];
} 

- (void)testFoo {
    id temp = [Foo newFoo];
    [self setFoolish:temp];//含义见第3个case中的解释
    objc_release(temp);
}

总结

<br />对于ARC下编译器到底对autorelease对象做了怎样的优化,现在我们有了比较深刻的了解了,在本方法中判断外部调用本方法的环境来决定要不要对调用方做一些事情,这也算是一种黑魔法了。不过话说这个优化其实与OC内存管理“谁生产谁销毁谁持有谁释放”的黄金法则有所违背,因为是在一个方法内部自身new了一个对象,但是release却是外部引用方来做的,不过因为编译器能通过语法分析知道了太多,所以一切也都在其掌握之中了。

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

推荐阅读更多精彩内容