iOSiOS 架构师之路将来跳槽用

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

2015-07-07  本文已影响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”.
使用以下名称开头的方法名意味着自己生成并持有对象:

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

上图中声明了一个以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);
}

然后继续解释第二条_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:

参考资料

上一篇下一篇

猜你喜欢

热点阅读