ARC下的内存管理与优化

作者: mysteryemm | 来源:发表于2017-07-21 17:59 被阅读457次

    前言

    最近闲来无事,温习下ARC下内存管理方面的知识,在此做个学习笔记,若有不对之处,还望各位童鞋不吝指正。

    方法命名与内存的关系

    通常情况下,为了体现更好的代码规范,方法的命名尽可能地达到见名知意的效果。如果我们声明一个创建对象的方法,那么有四个关键字需要额外注意下,就是allocnewcopymutableCopy。这里要分为两种情况进行讨论,暂且称之为方法命名的两种规则。
      1. 若方法名字是以allocnewcopymutableCopy这四个关键作为前缀,则方法返回的对象归调用者(caller)所有,也就是说caller要负责对创建的对象进行释放。
      2. 若方法名字不是以上述四个关键字作为前缀,则方法返回的对象不归caller所有,也就是说caller不需要负责对创建的对象进行释放,对象会被加到AutoreleasePool中自动释放。如果caller不想让对象被自动释放,那么就需要对其进行强引用(ARC下默认为strong相当于执行一次retain操作),然后再在适当的时候执行release操作对其释放。
      可能有的童鞋没有看明白是什么意思,不过没有关系,下面会通过具体的例子来进行阐述。如果到最后还是没有看明白,那么我可能会用三天时间来反思下我的文字表达能力[皱眉]~

    解释与说明

    1. 我们先来看第一种情况,即上述规则1。首先新建一个Person类,为了能够使用内存管理关键字来模拟ARC下编译器为我们设置的内存管理语义,故通过设置该编译单元(一个.m文件相当于一个编译单元)的Compiler Flags为-fno-objc-arc来禁用ARC机制,同时覆盖releasedealloc等方法。然后我们再在ARC文件中创建一个person对象,代码如下:

    Caller
    // ARC
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        if (YES) {  // if语句目的是让person对象的生命周期仅在当前作用域有效.
            Person *_person = [Person newPerson]; // 这里只对`new`关键字开头的方法进行测试, 其他三种方法可自行验证.
            NSLog(@"if语句执行中 %@", _person);
        }
        
        NSLog(@"if语句执行完");
        
    }
    
    @end
    
    Callee
    // MRC
    @implementation Person
    
    + (Person *)newPerson {
        Person *person = [[Person alloc] init];
        return person;
    }
    
    - (instancetype)retain {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        return [super retain];
    }
    
    - (oneway void)release {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        [super release];
    }
    
    - (void)dealloc {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        [super dealloc];
    }
    
    @end
    

    运行程序,控制台输出如下:

    2017-07-24 00:13:33.889 MemoryManagerUnderARC[5499:456138] if语句执行中 <Person: 0x608000018010>
    2017-07-24 00:13:33.889 MemoryManagerUnderARC[5499:456138] release
    2017-07-24 00:13:33.889 MemoryManagerUnderARC[5499:456138] dealloc
    2017-07-24 00:13:33.890 MemoryManagerUnderARC[5499:456138] if语句执行完
    

    Caller中,由于创建对象方法的名字是以new作为前缀,所以我们创建的对象归_person所有 (_person实际上是一个指针变量,OC中我们常称之为对象,具体请参考这里)。而_person是个局部变量,所以作用域仅在if语句中有效,当if语句结束的时候,需要将其释放,如果不释放,那么在if语句之外就会因为无法访问该对象而引起内存泄露。�通过打印结果可看出,ARC下为我们自动执行release操作将对象释放。这里要注意一下,虽然ARC下变量的内存管理语义默认为strong,但是_person并没有执行retain操作,因为这种情况下创建的对象已经归它所有,所以它无需再进行一次retain操作。如果我们此时将_person的内存管理语义改成__weak或者是__unsafe_unretained,那么_person就不会拥有这个对象,该对象创建完就会立即释放,当然,编译器也会给我们警告提示。


    2. 我们再来看第二种情况,即上述规则2。我们对代码进行修改,如下:

    Caller
    // ARC
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        if (YES) {  // if语句目的是让person对象的生命周期仅在当前作用域有效.
            Person *_person = [Person createPerson]; // 除上述四个关键字之外,随便使用一个前缀或者不用前缀.
            NSLog(@"if语句执行中 %@", _person);
        }
        
        NSLog(@"if语句执行完");
        
    }
    
    @end
    
    Callee
    // MRC
    @implementation Person
    
    + (Person *)createPerson {
        Person *person = [[Person alloc] init];
        return [person autorelease]; //模拟ARC下代码,其实ARC下真正执行的并不是`autorelease`方法,而是`objc_autoreleaseReturnValue`,这里稍后会进行介绍。
    }
    
    - (instancetype)retain {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        return [super retain];
    }
    
    - (oneway void)release {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        [super release];
    }
    
    - (void)dealloc {
        NSLog(@"%@", NSStringFromSelector(_cmd));
        [super dealloc];
    }
    
    @end
    

    再次运行程序,控制台输出结果如下:

    2017-07-24 00:39:52.693 MemoryManagerUnderARC[5557:475107] retain
    2017-07-24 00:39:52.693 MemoryManagerUnderARC[5557:475107] if语句执行中 <Person: 0x600000015550>
    2017-07-24 00:39:52.693 MemoryManagerUnderARC[5557:475107] release
    2017-07-24 00:39:52.694 MemoryManagerUnderARC[5557:475107] if语句执行完
    2017-07-24 00:39:52.697 MemoryManagerUnderARC[5557:475107] release
    2017-07-24 00:39:52.697 MemoryManagerUnderARC[5557:475107] dealloc
    

    如规则2所描述的那样,创建的对象会被加到AutoreleasePool当中,_person无需负责该对象的释放操作。但是由于ARC下变量的内存管理语义默认为strong,所以_person执行了一次retain操作,表示不想让对象自动释放,那么当if语句执行完的时候,即_person作用域结束的时候,为了避免内存泄露,就必须得执行一次release操作来抵消刚才的那次retain操作。至于上述打印结果最后执行的那次release操作是由于当前runloop进入休眠时AutoreleasePool进行pop操作将自动释放池里的对象进行释放,通过设置断点可查看:

    autorelease.png
      通过静态分析工具Analyze和Instruments对内存进行检测分析,没有发现内存问题,说明我们代码没毛病。看似完美无缺,但是细心的童鞋可能会有这样的疑问:既然_person不想让对象自动释放,那在创建对象的时候,为什么还要将对象添加到AutoreleasePool中?如果不加到AutoreleasePool中,那么_person也就无需执行一次retain操作来持有它,而只需要在作用域结束的时候执行一次release操作将其释放即可。
      所以,ARC下为了免除autoreleaseretain这两步多余的操作来提高性能,使用函数objc_autoreleaseReturnValue来替代autorelease、函数objc_retainAutoreleasedReturnValue来替代retain。这两个函数runtime源码中有介绍。

    (1) objc_autoreleaseReturnValue函数内部实现如下:

    id objc_autoreleaseReturnValue(id obj)
    {
    #if SUPPORT_RETURN_AUTORELEASE
        assert(tls_get_direct(AUTORELEASE_POOL_RECLAIM_KEY) == NULL);
    
        if (callerAcceptsFastAutorelease(__builtin_return_address(0))) {
            tls_set_direct(AUTORELEASE_POOL_RECLAIM_KEY, obj);
            return obj;
        }
    #endif
    
        return objc_autorelease(obj);
    }
    

    该函数会通过检测caller的retain指令(至于如何检测该指令,大神孙源博客黑幕背后的Autorelease中有介绍),来判断caller是否想持有创建的对象。若是的话,就不会执行autorelease,而是将对象存储到TLS(线程局部存储)中。

    (2)** objc_retainAutoreleasedReturnValue**函数内部实现如下:

    id objc_retainAutoreleasedReturnValue(id obj)
    {
    #if SUPPORT_RETURN_AUTORELEASE
        if (obj == tls_get_direct(AUTORELEASE_POOL_RECLAIM_KEY)) {
            tls_set_direct(AUTORELEASE_POOL_RECLAIM_KEY, 0);
            return obj;
        }
    #endif
        return objc_retain(obj);
    }
    

    该函数会先判断方法返回的对象和TLS中存储的是否一样,若一样,表示对象没有被加到AutoreleasePool中,则可以直接使用,否则就要执行一次retain操作。如此一来,就可以免除autoreleaseretain这两步操作,从而提高了性能。


    还是在这种情况下,如果_person不想持有对象,我们将内存管理语义改为__unsafe_unretained,则对象就会因为AutoreleasePool的drain而自动释放。代码和打印结果如下:

      if (YES) {
            Person * __unsafe_unretained _person = [Person createPerson]; //ARC下这么写,相当于MRC下这么写Person * _person = [Person createPerson];
            NSLog(@"if语句执行中 %@", _person);
        }
        
        NSLog(@"if语句执行完");
    
    2017-07-24 01:46:31.583 MemoryManagerUnderARC[5648:526071] if语句执行中 <Person: 0x60000001efb0>
    2017-07-24 01:46:31.583 MemoryManagerUnderARC[5648:526071] if语句执行完
    2017-07-24 01:46:31.586 MemoryManagerUnderARC[5648:526071] release //自动释放
    2017-07-24 01:46:37.888 MemoryManagerUnderARC[5648:526071] dealloc
    

    至此,对文章开头所说的那两种规则阐述完毕。路漫漫其修远兮,希望正在看的你能为在下点个喜欢,作为鼓舞和激励,在此十分感谢!

    参考资料

    https://en.wikipedia.org/wiki/Thread-local_storage
    http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
    https://opensource.apple.com/source/objc4/objc4-532.2/runtime/NSObject.mm

    相关文章

      网友评论

        本文标题:ARC下的内存管理与优化

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