美文网首页
IOS之MRC ARC

IOS之MRC ARC

作者: cj3479 | 来源:发表于2019-10-17 14:41 被阅读0次

    作者是以前搞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 具体了解一下

    相关文章

      网友评论

          本文标题:IOS之MRC ARC

          本文链接:https://www.haomeiwen.com/subject/pwtmuctx.html