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

引用计数式内存管理


引用计数


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

iOS内存管理初探 – 引用计数、AutoRelease与ARC_第1张图片
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以双向链表的方式形成的。

iOS内存管理初探 – 引用计数、AutoRelease与ARC_第2张图片
AutoreleasePage节点结构.png
  • next指针指向下一个加入autoreleasePool对象的位置
  • 一个page占满了之后会新开一个page并与上一个连接
iOS内存管理初探 – 引用计数、AutoRelease与ARC_第3张图片
单线程下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)