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

引用计数式内存管理


引用计数


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容