iOS Developer

Objective-C高级编程之内存管理篇

2017-02-24  本文已影响135人  还是不够辣

iOS的内存管理是采用引用计数的方式,引用计数分为手动引用计数和自动引用计数(ARC)。前者要求开发者手动管理内存,自己负责内存的申请与释放,后者是苹果推出的自动管理内存的方式,但其实质只是编译器帮助开发者做了内存管理的工作。理解引用计数的内存管理机制有助于我们写出更加内存安全的代码。

内存管理/引用计数

1. 引用计数的思考方式

引用计数的思考方式遵循以下四个原则:

2. 引用计数的实现

键值为内存块地址哈希值的引用计数表

使用散列表来管理引用计数的好处是
1)散列表中存有内存块地址,可通过表中引用计数追溯到出问题的内存块地址(这在调试是很有帮助的)
2)对象内存分配时无需再考虑引用计数所占用的内存

autorelease实现过程

代码表示如下:

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];

cocoa框架中,使用NSRunloop来管理NSAutoreleasePool的生成、持有和释放,runloop开始时会创建自动释放池,睡眠和退出时会销毁自动释放池,图解如下:


NSRunloop生成、持有,废弃NSAutoreleasePool对象

很多时候,我们并不需要主动使用NSAutoreleasePool来管理内存,但是某些时候如果产生了大量的autorelease对象,而NSAutoreleasePool没释放前,这些对象便依旧存于内存中,有可能会引发内存不足的情况,此时我们可以考虑创建NSAutoreleasePool来及时释放不需要的对象。当我们创建了多个自动释放池时,苹果又是怎么管理它们的呢?答案是栈!

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool3 = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool3 drain];
[pool2 drain];
[pool drain];

很显然,对象obj应该会加入到pool3中,因为pool3是当前正在使用的自动释放池。我们来看下苹果底层相关的方法

class AutoreleasePoolPage 
{
...
public:
    static inline id autorelease(id obj) {}  //将一个对象添加到pool中

    static inline void *push() {}  //将新创建的pool压入栈

    static inline void pop(void *token) {}  //将当前的pool出栈
...
  }

ARC

1. 所有权修饰符

ARC下由编译器帮助开发者自动加入内存管理代码,因此编译器必须知道对象何时在被持有,何时应该被释放,故苹果引入了4个所有权修饰符:
__strong, __week, __unsafed_unretained, __autoreleasing

id obj = [NSObject alloc] init];
id __strong obj = [NSObject alloc] init];

带有__strong修饰符的变量在超出其作用域时,即变量被废弃时,其持有的对象也随之被释放,代码角度看类似这样

{
  id obj = [NSObject alloc] init];
  [obj release];
}

在超出大括号作用域后,obj被废弃,其持有的对象也因强引用的失效而被释放。

对象相互强引用

有时对象引用了自身,也会发生循环引用的现象。

自身强引用

循环引用的后果是会发生内存泄漏(不再被需要的应该废弃的对象却无法被释放),那么__week是如何解决的呢?带有__week修饰符的变量无法持有对象的实例,换句话说,强引用会使对象的引用计数增加,而弱引用不会。
id __week obj = [NSObject alloc] init];
上述代码编译器会产生警告,原因是对象被生成后,由于obj持有其弱引用,导致对象立即被释放。带有__week修饰符的变量在对象被释放后自动变成了nil,故上述代码最终得到的obj是nil,将代码改为如下即可消除警告。

id __strong obj0 = [NSObject alloc] init];
id __week obj = obj0;

obj0持有对象的强引用,所以对象不会被释放,obj可以正确使用对象。当obj0超出了作用域,强引用失效,对象被释放,此时obj自动变为nil。如此便很容易明白,当循环引用的两个对象相互持有对方的弱引用时(或者其中一个持有的是弱引用),并不会影响到对象的释放,也就不再会发生内存泄露了。
和引用计数表类似,苹果对于week变量的管理也是通过散列表来实现的。将赋值对象的地址作为键值,由于同一对象可能被多个week变量弱引用,故同一键值可能对应一组week变量。由于__week修饰的变量会占用一定的CPU资源,因此除了解决循环引用的问题,尽量避免过多的使用week变量。

ARC与非ARC下代码对比

事实上,像__strong修饰符一样,大多数时候,我们并不需要显示对一个变量指定__autoreleasing修饰符。比如在取得非自己生成的对象引用时(使用除alloc/new/copy/mutablecopy以外的方法取得对象),变量被__week修饰符修饰时,取得id或对象类型的指针时(例如 id **obj),对象均会被自动注册到自动释放池中。

2. ARC规则

ARC有如下8个规则:

id obj = [NSObject alloc] init];
void * var = (void *)obj;
id obj2 = (id)var;

但是在ARC下,由于内存管理交给了编译器,因此编译器需要明确每个对象的所有者,该对象是否还有所有者,当无所有者时,编译器应当负责释放该对象。因此我们在转换类型时往往还需要考虑对象所有权问题。倘若只是想单纯的赋值,那么我们可以使用__bridge修饰符来完成转换。

id obj = [NSObject alloc] init];
void * var = (__bridge void *)obj;
id obj2 = (__bridge id)var;

与__bridge修饰符相关的两个修饰符是__bridge_retained和__bridge_transfer,这两个修饰符在完成转换的同时,还会对对象的所有权转移做处理。
__bridge_retained修饰符修饰的变量在被赋值时,还会获得被赋值对象的所有权,换句话说,会使对象引用计数增加。假定var是void *类型的变量,obj是id类型的变量,那么

var = (__bridge_retained void *)obj;

这等价于

var = (void *)obj;
[(id)var retain];

__bridge_transfer修饰符则正好和__bridge_retained相反,在赋值后被赋值对象随即被释放。同样的,假定var是void *类型的变量,obj是id类型的变量,那么

obj = (__bridge_transfer id)var

这等价于

obj = (id)var;
[obj retain];
[(id)var release];

这种转换常见于core Foundation对象与Foundation对象之间。前者是C语言类型对象,后者则是OC类型对象。

3. 属性

ARC下,类属性声明和对应的所有权修饰符是同样的作用。如下所示

属性声明的属性与所有权修饰符的对应关系
上一篇 下一篇

猜你喜欢

热点阅读