IOS之MRC ARC

2019-10-17  本文已影响0人  cj3479

作者是以前搞Android的,用的是java语言,对象的释放都是由虚拟机完成,IOS用的是Object C对象需要开发者自己管理MRC(Mannul Reference Counting),自己创建的对象需要自己释放,之后因为开发者太容易遗忘释放,导致出错,所以出现了ARC(Automic Reference Counting)机制,即系统自动管理机制,其实就是在OC编译的时候插入一些内存管理的代码,比如说retain release之类的

提到MRC和ARC必然要提到引用计数,即retainCount,如果一个对象的有引用计数是0,那么该对象就会被系统回收,释放内存空间
废话不多说直接上代码

#ViewController.m  ARC环境下编译
#define RC(obj) CFGetRetainCount((__bridge CFTypeRef)(obj))
__weak NSDictionary *testWeak1 = nil;
__weak NSDictionary *testWeak2 = nil;
__weak NSDictionary *testWeak3 = nil;
__weak NSDictionary *testWeak4 = nil;
__weak NSDictionary *testWeak5 = nil;
@implementation ViewController
- (void)handleGesture
{
    [self testReference];
    [self testWeak];
}

 - (void)testReference{ 
        TestARC *arc  = [[TestARC alloc]init];
     TestMRC *mrc1  = [[TestMRC alloc]init];
        NSDictionary *adExtraDic1 = [mrc1 copyTest];
        NSDictionary *adExtraDic2 = [mrc1 testReturnDic];
        NSDictionary *adExtraDic3 = [arc testReturnDic];
        NSDictionary *adExtraDic4 = [arc copyTestARCDic];
        //NSDictionary *adExtraDic5 = [[NSDictionary alloc]init];
        NSDictionary *adExtraDic5 = @{@"pull_time": @(1),
                             @"pull_time_1": @(2)
                             };
       //RC是在ARC模式下,对象的引用计数方法
        NSLog(@"viewDidLoad 111 adExtraDic1 111 count = %ld", RC(adExtraDic1));
        NSLog(@"viewDidLoad 111 adExtraDic2 111 count = %ld", RC(adExtraDic2));
        NSLog(@"viewDidLoad 111 adExtraDic3 000 count = %ld", RC(adExtraDic3));
        NSLog(@"viewDidLoad 111 adExtraDic4 000 count = %ld", RC(adExtraDic4));
        NSLog(@"viewDidLoad 111 adExtraDic5 000 count = %ld", RC(adExtraDic5));
        testWeak1 = adExtraDic1;
        testWeak2 = adExtraDic2;
        testWeak3 = adExtraDic3;
        testWeak4 = adExtraDic4;
        testWeak5 = adExtraDic5;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"testReference delay testWeak1 = %p,testWeak2=%p,testWeak3=%p,testWeak4=%p,testWeak5=%p", testWeak1,testWeak2,testWeak3,testWeak4,testWeak5);
135.        });
136. }


- (void)testWeak{
    NSLog(@"testWeak  testWeak1 = %p,testWeak2=%p,testWeak3=%p,testWeak4=%p,testWeak5=%p", testWeak1,testWeak2,testWeak3,testWeak4,testWeak5);
}


#TestMRC.m  MRC环境下编译
-(NSDictionary *)copyTest
{
    NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
    [adExtraDic setValue:@("abcd") forKey:@"c2s_switch"];
    return  adExtraDic;
}

-(NSDictionary *) testReturnDic
{
     NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
     [adExtraDic setValue:@("abcd") forKey:@"c2s_switch"];
    return  [adExtraDic autorelease];
}

- (void)dealloc
{
    NSLog(@"TestMRC dealloc obj=%p",self);
}

#TestARC.m  ARC环境下编译
49.-(NSDictionary *) testReturnDic
50.{
51.   NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
52.    return  adExtraDic ;
53.}

-(NSDictionary *) copyTestARCDic
{
    NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
//    NSLog(@"TestARC copyTestARCDic adExtraDic=%p",adExtraDic);
    return  adExtraDic ;
}

- (void)dealloc
{
    NSLog(@"TestARC dealloc");
}

打印结果
2019-09-23 16:31:33.574273+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic1 111 count = 1
2019-09-23 16:31:33.574285+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic2 111 count = 2
2019-09-23 16:31:33.574293+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic3 000 count = 1
2019-09-23 16:31:33.574302+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic4 000 count = 1
2019-09-23 16:31:33.574319+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic5 000 count = 2
2019-09-23 16:31:33.574382+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic7 = __NSDictionaryM adExtraDic6=__NSFrozenDictionaryM
2019-09-23 16:31:33.574414+0800 TestMac[46711:3554859] handleGesture testWeak1 = 0x6000002e3620,testWeak2=0x6000002e35e0,testWeak3=0x6000002e3640,testWeak4=0x6000002e3660,testWeak5=0x6000017b4440
2019-09-23 16:31:33.581273+0800 TestMac[46711:3554859] handleGesture testWeakObj0 = 0x60000000e980
2019-09-23 16:31:33.581358+0800 TestMac[46711:3554859] TestNSObject dealloc obj=0x60000000e980
2019-09-23 16:31:33.581381+0800 TestMac[46711:3554859] TestMRC dealloc
2019-09-23 16:31:33.581393+0800 TestMac[46711:3554859] TestARC dealloc
2019-09-23 16:31:33.581418+0800 TestMac[46711:3554859] TestNSObject dealloc obj=0x60000000e8b0
2019-09-23 16:31:33.581438+0800 TestMac[46711:3554859] testWeak testWeak1 = 0x0,testWeak2=0x6000002e35e0,testWeak3=0x0,testWeak4=0x0,testWeak5=0x6000017b4440
2019-09-23 16:31:35.581492+0800 TestMac[46711:3554859] testReference delay testWeak1 = 0x0,testWeak2=0x0,testWeak3=0x0,testWeak4=0x0,testWeak5=0x0

下面来一个个分析下
PS:汇编代码和源码已上传,可以对照汇编和源码的行号来理解汇编语言
先看第1行和第2行日志 都是mrc为什么一个是1,一个是2
先来看第2行日志,直接上汇编语言吧
TesrMRC中testReturnDic的汇编如下

image.png

testReturnDic会调用
[[NSMutableDictionary alloc] init];这个会让对象的retainCount+1,此时对象的引用计数是1
再来看看ViewControll中调用[mrc1 testReturnDic]的汇编语言

image.png
在文章的开头说过,ARC会在编译的时候插入一些内存管理的代码,这里就可以看到
callq _objc_retainAutoreleasedReturnValue
这句话是什么意思呢,其实就是尝试retain一个return的value,为什么是尝试,这里不说,稍后再说
看看_objc_retainAutoreleasedReturnValue的源码
1.id objc_retainAutoreleasedReturnValue(id obj)
2. {
3.   if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

4 .   return objc_retain(obj);
5.}

先忽略第3行代码,直接跳到第4行代码,这里做了一次retain,引用计数加+1,此时对象的引用计数是2,所以说第2行打印的日志是2

在回过头来看看第一行日志
TesrMRC中copyTest的汇编如下


image.png

跟上面的testReturnDic的汇编差不多,这里不多说,主要看看
ViewControll中调用[mrc1 copyTest]的汇编语言


image.png
看上去很简单,并没有调用_objc_retainAutoreleasedReturnValue的代码
为什么会这样同样是MRC,几乎同样的函数实现,就是函数名不一样
这是因为根据苹果的命名规定,调用以alloc/new/copy/mutableCopy等开头的方法,表示调用者自己生成并持有对象,所以不需要retian,即在编译器识别这些方法时,不会自动加上_objc_retainAutoreleasedReturnValue,所以对应的引用计数是1

再来看看第三行日志,即[arc testReturnDic]对应的引用计数
是1,为什么呢?再一次看看arc testReturnDic对应的汇编语言


image.png

可以看出这个明显要比[mrc1 testReturnDic]的汇编语言要复杂一些,这也就说明了ARC机制会在编译的时候自动插入了一些代码来自动管理内存,下面来看看主要插入了哪些代码
312-316行是函数调用,在ios中函数调用实际就是消息机制,所以都是_objc_msgSend开头的
,这里面两个_objc_msgSend对应的就是alloc和init函数
继续往下看321有个retain调用,这就是ARC自动插入来的代码增加引用计数,这里对应的代码
是return adExtraDic ;其实可以理解为

NSMutableDictionary *adExtraDic1 = adExtraDic;
return adExtraDic1;

这样会更容易理解插入retain的代码
继续看汇编语言的第53行调用了一个
_objc_storeStrong这行汇编对应的是函数结束的位置代码53行,可以认为是栈帧出栈,需要释放局部变量,objc_storeStrong的实现如下


image.png

此时经调试runtime发现obj为nil,prev就是adExtraDic,第10行,调用了release(prev),在这里objc_storeStrong就是释放adExtraDic的,即局部变量,此时因为NSMutableDictionary的对象先init了一次,引用计数是1
,然后又retain了一次,引用计数+1,变为2,最后函数结束的时候又release了一次,引用计数-1,变为1了,
最后接着看汇编代码333行,调用了
_objc_autoreleaseReturnValue,先看这个方法的实现


image.png

看第三行如果prepareOptimizedReturn为true,直接返回该对象,否则加入自动释放池里面,待下一个runloop到来时释放,那么这个prepareOptimizedReturn是什么意思呢
看下prepareOptimizedReturn的实现


image.png

上面注释写的很清楚,尝试优化,否则返回的值必须retain,autorelease,尝试这个词是不是很熟悉,上面提到过,其实这个优化就是ARC在运行时的优化,就是调用_objc_autoreleaseReturnValue是会检查,接下来是否会调用_objc_retainAutoreleasedReturnValue,如果会,那么就直接返回对象,直接跳过autorelease和retain,如果不会,则会autorelease
网上的一个例子时候说的很清楚
当方法全部基于 ARC 实现时,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue() 以替代 MRC 下的 autorelease。在 MRC 下需要 retain 的位置,ARC 会调用 objc_retainAutoreleasedReturnValue()。因此下面的 ARC 代码:

+ (instancetype)createSark {
    return [self new];
}
// caller
Sark *sark = [Sark createSark];
实际上会被改写成类似这样:

+ (instancetype)createSark {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release

有了这个基础,ARC 可以使用一些优化技术。在调用 objc_autoreleaseReturnValue() 时,会在栈上查询 return address 以确定 return value 是否会被直接传给 objc_retainAutoreleasedReturnValue()。 如果没传,说明返回值不能直接从提供方发送给接收方,这时就会调用 autorelease。反之,如果返回值能顺利的从提供方传送给接收方,那么就会直接跳过 autorelease 过程,并且修改 return address 以跳过 objc_retainAutoreleasedReturnValue()过程,这样就跳过了整个 autorelease 和 retain的过程。

核心思想:当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要做就好了。

另外,当函数的调用方是非 ARC 环境时,ARC 还会进行更多的判断,在这里不再详述,详见 《黑幕背后的 Autorelease》

对应的objc_retainAutoreleasedReturnValue方法也是一样

1.id objc_retainAutoreleasedReturnValue(id obj)
2. {
3.   if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

4 .   return objc_retain(obj);
5.}

这里第3行代码也会判断是否优化,如果优化就直接返回对象,没有必要在retain了,多余
跟objc_autoreleaseReturnValue对应,objc_autoreleaseReturnValue判断如果可以优化,则把标志位存到Threadlocal里面,objc_retainAutoreleasedReturnValue则调用acceptOptimizedReturn从ThreadLocal里面取出来
所以到[arc testReturnDic]方法结束,NSMutableDictionary对象的引用计数依然是1
,在继续看
再来看看ViewControll中调用[arc testReturnDic]的汇编语言


image.png

可以看出第483行确实调用了_objc_retainAutoreleasedReturnValue,上面说过了,如果是优化就直接返回返回,不retain,所以引用计数依然是1,至此[arc testReturnDic]的分析结束

再来看看[arc copyTestARCDic]的分析,同样,先看汇编代码


image.png

跟[arc testReturnDic]很像,只是这里缺少_objc_autoreleaseReturnValue的调用,那么这里就有疑问了,那缺少_objc_autoreleaseReturnValue说明,这里不存在优化,那最终在ViewController里面_objc_retainAutoreleasedReturnValue,引用计数岂不是变为2了,其实不然,如果引用计数是2的话,函数结束只会释放一次局部变量,那依然存在1个引用计数啊,岂不是内存泄漏把,醒醒吧,IOS怎么可能出现这种低级错误,那么怎么解释这个问题,那就继续看汇编了

ViewControll中调用[arc copyTestARCDic]的汇编语言


image.png

答案揭晓了,这里面并没有调用_objc_retainAutoreleasedReturnValue,为什么呢?其实上面分析MRC的时候提过
这是因为根据苹果的命名规定,调用以alloc/new/copy/mutableCopy等开头的方法,表示调用者自己生成并持有对象,所以不需要retian,即在编译器识别这些方法时,不会自动加上
到此前4行日志分析完毕,接下来分析第5行日志

 NSDictionary *adExtraDic5 = @{@"pull_time": @(1),
                             @"pull_time_1": @(2)
                             };

为什么它的引用计数是2
这个字典的赋值语句,实际上在编译的时候调用
[NSDictionary dictionaryWithObjects:forKeys:count:],这个通过debug或者汇编语言都可以知晓,然后接着会[__NSSingleEntryDictionaryI __new:::]一下,所以此时引用计数是1,
这个为什么不用汇编语言来分析呢?因为NSDictionary是系统类,无法看到汇编的代码
这是赋值语句的调用栈


image.png

下面再看看ViewControll中调用 NSDictionary *adExtraDic5 = @{@"pull_time": @(1),@"pull_time_1": @(2) }发生了什么

首先通过debug发现它会调用autorelease
调用栈如下


image.png

然后会调用objc_retainAutoreleasedReturnValue
调用栈如下


image.png

这样因为前面没有调用_objc_autoreleaseReturnValue,也就是说没有优化,所以这里的的objc_retainAutoreleasedReturnValue会retain对象,导致引用计数+1,所以此时引用计数为2

附上ViewControll中该段代码的汇编


image.png

里面也有_objc_autoreleaseReturnValue的调用,但是没有autorelease的调用,不明白什么原因

至此第5行的日志也分析完毕,
通过上面的分析基本对ARC和MRC有个大致的了解,所以接下来的日志分析也比较好理解了,
先看这个日志


image.png

这些个weak引用的是前面分析的那些对象,当这些对象释放内存时,weak就为nil,这就可以判断对象什么时候释放了,这行日志的打印的时候,很明显这些都没有被释放,所以都不为nil

再看看这个日志

image.png
可以看到除了testWeak2和testWeak5之外,其他都为nil,这说明除了adExtraDic2和adExtraDic5其他对象都被释放了
分析之前先贴一张testReference函数结束时的汇编语言 image.png 136行对应的就是函数结束的位置
可以看出里面很多_objc_storeStrong,这就是对象释放局部变量的操作

下面来一个个分析,为什么被释放,为什么没被释放
testWeak1对象的是adExtraDic1,引用计数是1,当testReference结束时,函数出栈,释放一次局部变量,导致引用计数是0,释放内存,
思考个问题,如果TestMRC copyTest在返回是添加了autorelease即 [adExtraDic autorelease]
会出现什么情况.....,30秒过去了...

直接给出答案吧,会崩溃,堆栈如下 image.png

这是因为adExtraDic1的引用计数是1,它在testReference结束的时候,因为释放局部变量,导致,引用计数变为0了,内存已经释放,此时因为有autorelease,它会等到下一次loop的到来时,尝试再一次释放该对象,但是对象在已经释放完毕,所以会崩溃

再来看看testWeak2对应的adExtraDic2为啥在这个testweak方法中没被释放
跟adExtraDic1,当testReference结束时,函数出栈,释放一次局部变量,导致引用计数-1,但是因为adExtraDic2本身的引用计数为2,即使-1,剩下1,所以不会释放

testWeak3 testWeak4 testWeak5的情况都是类似,就不一一分析

再来继续看下面的日志 image.png

这段日志实际上就是模拟下一次runloop的到来,会发生什么情况,这里的代码dispatch_after就是模拟下一个runloop
可以看到testWeak2和testWeak5都为nil了,其实就是testWeak2和testWeak5的autorelease的作用,前面也说过autorelease会延迟对象的释放,等下次runloop时才会释放,所以这里又释放了一次,

至此所有的日志分析结束

再来看看特殊的case吧
NSDictionary *adExtraDic5 = [[NSDictionary alloc]init];
这个adExtraDic5对象的引用计数是-1或者无穷大,不管怎么样都不会被释放,猜测是因为NSDictionary是不变的字典,这样的创建实际上没有任何意义

NSString *ff = @"dsdsddwdasdsdaddasssa";
这个adExtraDic5对象的引用计数是-1或者无穷大,这个字符串位于常量区,不是堆区,没有引用计数,不存在释放

NSString *str = [NSString stringWithFormat:@"123456789"];
NSString *longStr = [NSString stringWithFormat:@"1234567890"];
NSLog(@"str %s %p", object_getClassName(str), str);
NSLog(@"longStr %s %p", object_getClassName(longStr), longStr);
str沒有引用计数,longStr的引用计数是1
为什么呢
在网上搜索了一下,一般人给出的答案是:当字符串长度小于10时,字符串是保存在常量区,没有引用计数。如果长度大于等于10呢,就会被复制到堆去,有引用计数。
参考链接
Tagged Pointer 具体了解一下

最后在扩展一些小知识
关于ARC和MRC属性赋值的问题,
大家知道ios中属性的赋值,实际上是调用set方法
比如说有个TestARC.h文件
@interface TestARC : NSObject
@property (nonatomic,retain)NSObject * obj;

调用testArc.obj =[[ NSObject alloc]init]
实际上是调用[testArc setObj]方法

下面直接上图ARC中属性赋值的时序图 image.png 属性区分原子性,两者调用的方法不一样

objc_storeStrong的实现

image.png 可以看出,对于ARC的nonatomic属性来说,先把以前的该属性的对象prev取出来,然后赋予新值,最后释放对象,那如果多个线程同时赋值,就会有同步问题了。比如 image.png 这段代码在nonatomic属性下必crash,崩溃日志 image.png 这个很好理解,多线程导致同一个对象被释放了多次

下面再看看如果是atomic属性呢》它调用的是objc_setProperty_atomic->reallySetProperty,看看reallySetProperty的实现 image.png

第89行判断如果是非原子性,直接赋值,不加锁,否则
枷锁,这样就解决了的线程安全问题,所以上面的例子如果是atomic就没问题

再来看看

MRC的属性赋值 image.png ,其中atomic的属性跟ARC流程一直,只是nonatomic实现不一致,但大同小异,所以MRC的nonatomic属性也会出现多线程同步问题

总结:没啥总结的,多看汇编,多看源码

参考链接
《黑幕背后的 Autorelease》
NSString 引用计数
Tagged Pointer 具体了解一下

上一篇下一篇

猜你喜欢

热点阅读