美文网首页iOS学习开发iOS DeveloperiOS学习笔记
iOS内存管理初探 – 引用计数、AutoRelease与ARC

iOS内存管理初探 – 引用计数、AutoRelease与ARC

作者: ameerkat | 来源:发表于2017-03-29 19:25 被阅读257次

    引用计数式内存管理


    引用计数


    iOS通过引用计数管理对象的生命周期,每个对象有其引用计数。

    alloc返回对象内存图.png

    对象被强引用时引用计数加1,强引用解除时减1,引用计数为0时废弃对象。

    引用计数为0时废弃对象的实现原理:iOS通过引用计数表(散列表)来管理引用计数:计数表中以内存块为键值,引用计数为对应记录。由于引用计数表的记录中存有内存块地址,所以可以追溯到对应对象的内存位置。

    四个内存管理法则


    • 自己生成的对象自己持有
    • 非自己生成的对象,自己也可以持有
    • 不再需要持有对象时释放对象
    • 无法释放非自己持有的对象

    Objectice-C对应方法 | retainCount | 对象操作
    ------ | :----- :| :------
    alloc/new/copy/mutableCopy | +1 | 生成并持有对象
    retain | +1 | 持有对象
    release| -1 | 释放对象
    dealloc | - | 废弃对象

    MRC下,根据这四个法则,开发人员手动调用NSObject有关内存管理的方法,通过操作减引用计数来管理内存。

    autorelease


    将对象加入autorelease pool之后,废弃autorelease pool的时候对象的引用计数会-1 。

    为什么要有autorelease

    延迟内存的释放,即延长对象的生命周期,并且在合理的时机释放。比如返回值为指向对象的指针时:

    - (NSObject *)initObject() {
        NSObject *obj = [[NSObject alloc] init];
        return obj;
    }
    

    initObject函数的调用者期望得到一个object,但由于obj在返回之后超出作用域会自动释放,这样调用者只能得到一个悬垂指针。在这种情况下,可以通过将obj对象加入autorelease pool来解决问题。

    两种返回指针:返回指针有两种情况,官方进行了分类并给出了对指针拥有者的处理办法

    • retained return value: 调用者拥有返回值 ,负责释放,如alloc/copy/mutableCopy/new打头的方法。自定义的方法也应该遵守这样的命名规则。
    • unretained return value: 调用者不拥有,无需释放,如[NSString stringWithFormat:]

    autorelease原理与释放时机

    autoreleasePool实际是由若干个autoreleasePoolPage以双向链表的方式形成的。

    AutoreleasePage节点结构.png
    • next指针指向下一个加入autoreleasePool对象的位置
    • 一个page占满了之后会新开一个page并与上一个连接
    单线程下AutoreleasePage对象内存图.png
    • 释放时机:在当前的runLoop开始和结束时,系统加入了AutoreleasePool的创建(objc_autoreleasePoolPush)和释放(objc_autoreleasePoolPop),每次push的时候会在当前的AutoreleasePoolPage的next位置添加一个值为零的哨兵对象,之后作为objc_autoreleasePoolPop(哨兵对象)的入参,哨兵对象之后的对象即为要释放的对象。

    ARC的出现


    无论是MRC还是ARC,都是围绕引用计数来进行引用计数式内存管理。ARC下,只是将对引用计数的处理工作交给了编译器。

    所有权修饰符


    为了让编译器能够正确的接手内存管理的工作,ARC下引入了id类型和对象类型的所有权修饰符。

    __ strong:

    默认修饰符,表示强引用。修饰的变量在被废弃(变量超出作用域/成员变量所属对象被废弃/变量赋值nil)时,会释放被赋予的对象。

    __ weak:

    弱引用,用于解决循环引用问题。弱引用不会持有对象,当对象被废弃,弱引用会自动失效且被赋值nil。

    • weak引用能被赋值为nil的原因:系统实现了一个weak表(散列表),通过键值对的方式存储对象的地址与对应的__ weak引用变量的地址。对象被废弃时, 以废弃对象地址为键值查询找到其所有的weak引用并附为nil,然后删除记录。
    • 使用__ weak修饰的变量时,对应对象会被注册到autoreleasepool中::以防访问过程中对象被释放。

    __ unsafe_unretained:

    与weak很像,不持有对象,从用法上看,是weak的不安全版本。

    • 为什么不安全:此修饰符修饰的变量编译器实际上并不进行内存管理:当对象被废弃时,对应的被此修饰符修饰的变量并不会被置为nil,此时变量指向的内存内容是不确定的:如果这块内存没有被改写,代码可以正常运行;如果这块内存被部分改写,可能会出现很奇怪的 crash;如果这块内存正好被一个新对象覆盖,则会出现 unrecognized selector exceptions。
    • 为什么有了__weak还要用 __ unsafe_unretained:(1)历史版本(如iOS4及其之前)不支持__ weak(2)当对象有大量weak引用时,对weak引用的nil赋值会消耗CPU资源,对性能有一定损耗。

    __ autoreleasing:

    ARC下,不能使用autorelease方法,也不能使用NSAutoReleasePool类,但通过新的语法,autorelease功能仍起作用。

    /*ARC无效*/
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain];
    

    变成了

    /*ARC有效*/
    @autoreleasepool{
      id __autoreleasing obj = [[NSObject alloc] init];
    }
    

    _ _autoreleasing 的显式使用并不常见,且修饰的对象必须为自动变量。更多的是,__autoreleasing的隐式使用:

    1. retained return value类型的指针返回时:

    + (id)array
    {
      return [[NSMutableArray alloc] init];
    }
    /*编译器模拟代码*/
    + (id) array
    {
      id obj = obj_msgSend(NSMutableArray, @selector(alloc));
      objc_msgSend(obj, @selector(init));
      return objc_autoreleaseReturnValue(obj);  // 返回注册到自动释放池的对象  
    }
    
    {
      id __strong obj = [NSmutableArray array];
    }
    /*编译器模拟代码*/
    {
      id obj =  obj_msgSend(NSMutableArray, @selector(array));
      objc_retainAutoreleaseReturnValue(obj);  // 由于引用retain
      obj_release(obj);
    }
    

    此处可能出现runtime的优化:objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果之后紧接着调用retainAutoreleaseReturnValue,就不将对象注册到自动释放池,而是直接交付给retainAutoreleaseReturnValue。

    2. 访问__weak修饰的变量时

    {
      id __weak obj1 = obj0;
      NSLog(@"obj1's Class:%@",[obj1 class]);
    }
    /*编译器模拟代码*/
    {
      id __weak obj1 = obj0;
      id __autoreleasing tmp = obj1;
      NSLog(@"obj1's Class:%@",[tmp class]);
    }
    

    *3. id 或对象指针没有显示指定修饰符时编译器自动添加__autoreleasing:
    !!!有些方法隐式的使用autoreleasePool如:

    - (void) loopThroughDictionary: (NSDictionary *)dic, error: (NSError **) error {
      [dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, Bool *stop){
        if(there is some error){
          error = [NSError errorWithDomain:@"MyError" code:1 userinfo: nil];
        }
      }]
    }
    

    实际上是

    - (void) loopThroughDictionary: (NSDictionary *)dic, error: (NSError **) error {
      [dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, Bool *stop){
        @ autoreleasepool{  // 隐式创建
          if(there is some error){
            error = [NSError errorWithDomain:@"MyError" code:1 userinfo: nil];
          }
        }
      }]
    }
    

    由于error参数默认__autoreleasing修饰,在第一次迭代结束后error就被释放了,正确的写法:

    - (void) loopThroughDictionary: (NSDictionary *)dic, error: (NSError **) error {
      NSError *__block tempError; //加__block保证能在block内被修改
      [dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, Bool *stop){
        if(there is some error){
          *tempError = [NSError errorWithDomain:@"MyError" code:1 userinfo: nil];
        }
      }]
      if(error != nil){
        error = tmpError;
      }
    }
    

    或是参数显示强引用?(待验证)

    属性标示符


    • assign:对应到__unsafe_unretained, 表明setter仅做赋值,不增加对象的引用计数,用于基本数据类型
    • strong:对应到__strong,赋值时先对值retain,再对旧值release,最后赋值。ARC模式下对象属性的默认值
    • weak:对应到__weak
    • unsafe_unretained:对应到__unsafe_unretained, 用于历史版本。ARC模式下非对象属性的默认值
    • copy:对应到__strong,但是赋值操作比较特殊:赋值时进行copy而非retain操作,原来的值可变则深拷贝,不可变则浅拷贝

    参考书目与文章

    《Objective-C高级编程:iOS与OS X多线程和内存管理》
    黑幕背后的Autorelease

    相关文章

      网友评论

        本文标题:iOS内存管理初探 – 引用计数、AutoRelease与ARC

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