iOS && Android内存管理

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

2016-10-18  本文已影响1597人  东野浪子

如何将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却是外部引用方来做的,不过因为编译器能通过语法分析知道了太多,所以一切也都在其掌握之中了。

上一篇下一篇

猜你喜欢

热点阅读