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

引子

这段时间一直在看关于内存管理的资料,把我整的够呛,尤其是看了
<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://upload-images.jianshu.io/upload_images/651640-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:

参考资料

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

推荐阅读更多精彩内容