美文网首页iOSiOS 架构师之路将来跳槽用
Objective-C ARC内存管理的深入理解(一)

Objective-C ARC内存管理的深入理解(一)

作者: __微凉 | 来源:发表于2015-07-07 16:51 被阅读1027次

    引子

    这段时间一直在看关于内存管理的资料,把我整的够呛,尤其是看了
    <Objective-C高级编程>、雷纯锋大神的<Objective-C Autorelease Pool 的实现原理>和Sunny大神的<黑幕背后的Autorelease>,这几份作品后,知识体系始终是零零碎碎的,脑袋一团糊浆。直到帮人分析了一个程序的Bug后,恍然大悟。本文主要参考以上几份作品,对ARC下的内存管理进行了实践与梳理,本人是iOS开发新手,文章有误之处请轻拍。

    命名规则

    根据苹果官方文档<Memory Management Policy>中所述,总结规则如下:

    You take ownership of an object if you create it using a method whose name begins with “alloc”, “new”, “copy”, or “mutableCopy”.
    使用以下名称开头的方法名意味着自己生成并持有对象:

    • alloc
    • new
    • copy
    • mutableCopy

    这个命名规则并不是指命名规范之类的大家约定俗成的东西(譬如成员变量以_开头、驼峰拼写法等规范),编译器会自动判断方法名字的开头是否符合以上命名规则,不遵循命名规则会遭到编译器的警告。下面看看一个例子:

    上图中声明了一个以new开头的属性,编译器为我们生成了setter和getter方法,getter方法名如下:- (void)newPerson,根据命名规则,编译器认为我们要自己生成并持有一个对象,显然getter方法不产生新对象,于是编译错误。

    这里提到的命名规则,对后文所述内容有帮助。

    ARC规则

    下面从一个例子入手:

    在上图中,声明了两个以__weak所有权修饰符修饰的id类型对象。忽略obj1obj2都有的一个未使用的警告。obj1还有一个警告,说对象创建后即会被释放(当然啦,生成的对象没有强引用指着,结果就挂了,弱引用不能持有对象实例)。可是为什么obj2没有该警告呢?
    我们注意到obj2获取的对象是通过类方法获得的,根据命名规则,不是以alloc/new/copy/mutableCopy开头的方法,obj2取得的是非自己生成并持有的对象,那么显然,这个UIImage对象的持有者另有他人。

    下面利用lldb指令,我们跟踪这两个对象的生命周期,由于test方法内部声明的两个变量是在栈上产生的局部变量,对后续工作有影响,我们将两个变量定义成成员变量。

    ![](http:https://img.haomeiwen.com/i651640/6e3a08fbcd0f5c06.png)

    test开始前和开始后分别打上断点:

    接下来我们运行程序,当来到第一个断点处,在控制台输入watchpoint set v _obj1watchpoint set v _obj2,对这两个成员变量进行观测。

    由于test方法前_obj1_obj2都为nil,从控制台可以看到地址值0x00
    接下来点击继续:

    我们发现Watchpoint 1 hit,即obj1的值发生了变化,这是因为

    _obj1 = [[NSObject alloc] init];

    _obj1指向了一个对象,我们再来看看左边的堆栈信息:

    这里我们先留意这里有objc_storeWeak,后文会提到。
    然后我们继续运行。

    我们看到,_obj1挂掉了,它被置为了0x00,也就是nil。并且它是立即被释放了,为什么? 因为_obj2都还没被赋值,_obj1就被release了。这时再看看堆栈信息。

    可以看到几个关键词,"release(释放)"、"dispose(丢弃)"、"destructInstance(销毁实例)"。可以证明_obj1确实挂了,而且是立即被释放。
    再点继续。

    我们来到了42行的断点,这验证了刚创建就被释放的说法。
    不管_obj1了,我们继续点运行。

    这时,_obj2的值也被改变了,它指向的是UIImage实例对象。左边的堆栈信息和_obj1一样,就不截图了。下面再点运行。

    代码安全地到了44行,_obj2并没有被释放。
    我们继续运行,来到了_obj2被释放的时候:

    这时再来看看堆栈信息:

    这图与_obj1被释放时很相似,但请注意这里多了[UIImage dealloc][NSAutoreleasePool release]AutoreleasePoolPage等字眼。说明_obj2所指向的对象曾被加入了自动释放池并且在这个时候被释放了。

    事实上,“自动释放池并不是在test方法作用域结束后释放池中对象的,而是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池PushPop。”

    编译器做了什么?

    当我们用所有权修饰符去声明时,编译器为我们做了什么?
    先看看__strong下的情况。

    {
        id __strong obj = [[NSObject alloc] init];
    }
    

    编译器将以上代码转换成以下代码,注意大括号标明作用域:

    {
        /*编译器模拟代码*/
        id obj = obj_msgSend(NSObject,@selector(alloc));
        obj_msgSend(obj,@selector(init));
        obj_release(obj);
    }
    

    上面代码调用了两次obj_msgSend,分别调用了alloc方法和init方法,变量作用域结束时,通过obj_release释放了obj对象。__strong所修饰的局部变量的生命周期通常是花括号内,这与我们平常的认知相符。

    然后看看__weak

    {
        id __weak obj1 =  obj;
    }
    

    其中假设obj__strong修饰且有值。

    {
       /*编译器模拟代码*/
       id obj1;
       objc_initWeak(&obj1,obj);
       objc_destroyWeak(&obj1);
    }
    

    首先来看看objc_initWeak函数。它先初始化__weak修饰的变量。

       obj1 = 0;
       objc_storeWeak(&obj1,obj);
    

    objc_destroyWeak函数将0作为参数调用objc_storeWeak

       objc_storeWeak(&obj1,0);
    

    于是。前面那段代码与下面相同。

       obj1 = 0;
       objc_storeWeak(&obj1,obj);
       objc_storeWeak(&obj1,0);
    

    objc_storeWeak函数把第二参数的复制对象的地址作为键值,将第一参数的附有__weak修饰符的变量的地址注册到weak表中。如果第二参数为0,则把变量的地址从weak表中删除。weak表引用计数表实现相同。”

    回到前面的例子中!

    {
        _obj1 = [[NSObject alloc]init];
    }
    

    这段代码将被编译器解释为:

    {
       /*编译器模拟代码*/
       id obj1;
       id tmp = objc_msgSend(NSObject, @selecter(alloc));
       objc_msgSend(tmp, @selector(init));
       objc_initWeak(&obj1, tmp);
       objc_release(tmp);
       objc_destroyWeak(&obj1);
    }
    
    • id obj1,首先编译器为我们创建一个obj1变量
    • id tmp = objc_msgSend(NSObject, @selecter(alloc)),创建了一个临时变量tmp存放调用NSObjectalloc方法的返回的实例
    • objc_msgSend(tmp, @selector(init))tmp继续发送init方法的消息,这时tmp是一个初始化完毕的实例对象的持有者
    • 通过objc_initWeak(&obj1, tmp)函数初始化附有__weak修饰符的变量obj1objc_initWeak(&obj1, tmp)函数内部又进行了obj1 = 0;obj_storeWeak(&obj1, tmp)这两个步骤。
    • obj_storeWeak(&obj1, tmp)tmp的赋值对象的地址作为键值,将附有__weak修饰符的obj1变量的地址注册到weak表中。
    • 虽然通过alloc方法生成并持有了一个对象,但赋值给了附有__weak修饰符的变量,编译器判断这个对象不能继续被持有,通过objc_release(tmp)被立即释放和废弃了。
    • objc_destroyWeak(&obj1)obj_storeWeak(&obj1, 0),将weak表中所有包含obj1变量地址的记录赋值为nil。然后删除该记录。

    然后继续解释第二条_obj2 = [UIImage imageNamed:@"xxx"]。在这之前还需要补充一个知识点。

    最优化——省略autoreleasepool注册
    调用类方法,情况有些许不同。

    {
        id __strong obj =  [NSMutableArray array];
    }
    
    {
       /*编译器模拟代码*/
       id obj = objc_msgSend(NSMutableArray, @selector(array));
       objc_retainAutoreleasedReturnValue(obj);
       objc_release(obj);
    }
    

    这条objc_retainAutoreleasedReturnValue是什么呢?

    objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行。它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool中的对象的方法,或是函数的返回值。”

    要想理解以上这段话的含义,还得看一个函数。
    接下来看看NSMutableArrayarray类方法经过编译器怎样的转换。

    + (id) array
    {
       return  [[NSMutableArray alloc] init];
    }
    
    {
       /*编译器模拟代码*/
       id obj = objc_msgSend(NSMutableArray, @selector(alloc));
       objc_msgSend(obj, @selector(init));
       return objc_autoreleaseReturnValue(obj);
    }
    

    alloc/new/copy/mutableCopy开头的方法以外的一些方法上,编译器判断是否为函数的返回值,自动将其注册到autoreleasepool中。返回注册到autoreleasepool中对象的方法使用了objc_autoreleaseReturnValue函数。
    并且!:

    objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnValue函数,那么就不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的调用方。不将对象注册到autoreleasepool中,这一过程达到了最优化

    为了理解这段话,我又做了一个试验。
    与之前一样,这里的_objid类型的弱指针

    36行断点时,我们跟踪成员变量的值。

    继续运行。跟踪到了_obj1挂掉的时候。

    再看看堆栈。

    可以发现,它并不是通过自动释放池释放的!而是刚创建立刻被释放了
    但为什么通过+ (UIImage *)imageNamed:(NSString *)name产生的对象是在自动释放池中的呢?说明这个方法是注册到自动释放池的。

    回顾上面在模拟的代码

    + (id) array
    {
       /*编译器模拟代码*/
       id obj = objc_msgSend(NSMutableArray, @selector(alloc));
       objc_msgSend(obj, @selector(init));
       return objc_autoreleaseReturnValue(obj);
    }
    

    显然,在调用array方法的过程中,使用了objc_autoreleaseReturnValue使编译器做了最优化的处理,使得返回值并没有注册到自动释放池中。

    至于本文开头的例子为何+ (UIImage *)imageNamed:(NSString *)name方法产生的对象会被注册到自动释放池呢?

    这是因为在苹果中,有一大类构造方法(constructor method),由它们构造的对象直接就是自动释放的对象;这一类构造方法叫做便捷方法
    比如:

    1. NSArrayarrayWithObjects :arrayWithArray :
    2. UIImageimageNamed :
    3. NSNumbernumberWithBool
    4. NSStringstringWithFormat:

    参考资料

    相关文章

      网友评论

      • i顺颂时宜:关于objc_retainAutoreleasedReturnValue 和 objc_autoreleaseReturnValue,一句没看懂。
        “objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行。它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool中的对象的方法,或是函数的返回值。”

        什么叫注册在autoreleasepool中的对象的方法??
        __微凉:@赵先生Try 不好意思可能有歧义,这句话这样断句"但它持有的对象应为返回注册在autoreleasepool中的对象的方法(或函数)的返回值。"
      • G了个J:叶神,不知我可否理解为当便利类构造器内部调用了self的根类构造器?这时的self导致的强引用而不会销毁的?
        __微凉:@G了个J 不是很明白你说的 :joy:
      • G了个J:涨姿势了
      • 22ee0fa80171:很赞!从命名规范延伸到ARC内存管理机制,环环相扣,让人看了不禁拍案叫绝!

      本文标题:Objective-C ARC内存管理的深入理解(一)

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