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

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

内存管理/引用计数

1. 引用计数的思考方式

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

  • 自己生成的对象,自己持有
    id obj = [NSObject alloc] init]
    alloc创建了一个对象(这里指内存),init对此对象做初始化操作,obj持有了这个对象。同样的,new/copy/mutablecopy也可用于生成并持有对象,除此四个关键字之外,以alloc/new/copy/mutablecopy开头的方法也可用于生成并持有对象。
  • 非自己生成的对象,自己也能持有
    id obj = [NSMutableArray array]
    obj取得了array对象,但此时并不持有它。(事实上,array方法返回的是一个autorelease对象)
    [obj retain]
    通过retain方法,obj便持有了array对象 。
  • 不再需要自己持有的对象时,释放
    持有的对象不再需要时,持有者应当负责释放它。
    id obj = [NSObject alloc] init]
    [obj release]
    通过调用release方法可释放obj持有的对象。
  • 非自己持有的对象,无法释放
    id obj = [NSObject alloc] init]
    [obj release]
    [obj release] //程序将崩溃
    当第二次调用release时,由于obj已不再持有对象,程序就会发生异常。

2. 引用计数的实现

  • alloc/retain/release/dealloc
    alloc方法和retain方法会使对象的引用计数值加1,release方法使对象的引用计数值减1,当对象的引用计数值为0时,调用dealloc方法释放对象。
    苹果是用散列表来管理引用计数的,键值为对象内存块地址,对应的值保存引用计数。图示如下
Objective-C高级编程之内存管理篇_第1张图片
键值为内存块地址哈希值的引用计数表

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

  • autorelease
    autorelease是自动释放对象的方法,它是通过NSAutoreleasePool来实现的。它在对象超出自身的作用域之后,调用release方法去释放对象,具体实现细节为:
    1)生成并持有NSAutoreleasePool对象
    2)调用autorelease方法,将对象添加到NSAutoreleasePool中
    3)待NSAutoreleasePool生命周期结束时,所有添加到自动释放池中的对象均会被发送release消息来释放自身
Objective-C高级编程之内存管理篇_第2张图片
autorelease实现过程

代码表示如下:

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

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


Objective-C高级编程之内存管理篇_第3张图片
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

  • __strong修饰符
    __strong表示对对象的强引用,是id类型和对象类型默认的所有权修饰符,以下两行代码实质是一样的
id obj = [NSObject alloc] init];
id __strong obj = [NSObject alloc] init];

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

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

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

  • __week修饰符
    看起来__strong修饰符已经能完美解决内存管理的诸多问题,但事实上有一种情况是强引用无法解决的,即循环引用,而__week修饰符,这种弱引用方式,则可以完美解决循环引用的问题。
    我们先来看一下什么是循环引用。举个例子,比如当A持有了B的强引用,B也持有了A的强引用,由于A和B相互持有对象的强引用,导致A和B均无法被释放,这便是出现了循环引用。
Objective-C高级编程之内存管理篇_第4张图片
对象相互强引用

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

Objective-C高级编程之内存管理篇_第5张图片
自身强引用

循环引用的后果是会发生内存泄漏(不再被需要的应该废弃的对象却无法被释放),那么__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变量。

  • \ __unsafe_unretained修饰符
    __unsafe_unretained修饰符修饰的变量既不持有对象的强引用,也不持有对象的弱引用,它和__week修饰符一样,实际上是获得了一个指向对象的指针,而和__week不同的是,当对象被释放后,__week修饰的变量自动变为nil,__unsafe_unretained修饰的变量则成为了野指针!带有__unsafe_unretained的变量不在编译器内存管理范围内,编译器是不对它做管理的,使用时应当谨慎确保其所指向的对象仍存在并未被释放。
  • __autoreleasing修饰符
    ARC下的__autoreleasing相当于调用对象的autorelease方法,并使用@autoreleasepool来替代非ARC下的NSAutoreleasePool功能。被__autoreleasing修饰的变量所持有的对象会被加入到自动释放池中。
Objective-C高级编程之内存管理篇_第6张图片
ARC与非ARC下代码对比

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

2. ARC规则

ARC有如下8个规则:

  • 不可使用retain/release/retainCount/dealloc方法
    由于ARC下编译器会自动在合适的位置帮助开发者插入内存管理的代码,因此不允许开发者再主动调用内存管理相关方法。
  • 不可使用NSAllocateObject/NSDeallocateObject方法
    事实上,alloc方法会调用NSAllocateObject来创建对象,并保存其引用计数,单独调用NSAllocateObject方法会对内存管理造成混乱,因此禁止使用该方法自然也是合理的。
  • 不可显示调用dealloc方法
    这里指的是开发者无需在dealloc方法中显示调用[super dealloc],只需要做在释放对象时一些必要的处理,比如移除之前注册的某些观察者。
  • 使用@autoreleasepool块代替NSAutoreleasePool
    这个是显而易见的,ARC下禁止使用NSAutoreleasePool,而采用@autoreleasepool块代替。
  • 不能使用NSZone
    事实上,无论是手动管理内存还是使用ARC,NSZone(区域)在现在的运行时系统中都是被忽略的。
  • 对象型变量不可作为C语言结构体成员
    在C语言的规约下,结构体成员的生命周期是无法管理的,而ARC下编译器必须能够正确的管理OC对象的生命周期,这显然是矛盾的,故对象型变量不可作为C语言结构体的成员。解决的方式有两种,其一将对象型变量转换为void *类型(指向不限定某一具体类型的指针),其二是用__unsafed_unretained来修饰变量,这相当于告诉编译器该对象不需要被编译器管理。
  • 遵循内存管理的方法命名规则
    一般而言,命名方法时,谨慎使用以alloc/new/copy/mutablecopy开头的方法名,这类方法应当返回给调用方应该持有的对象。以init开头的方法应该返回实例对象。
  • 显示转换id和void
    在非ARC下,我们可以方便的直接对这两种类型做转换,如下所示
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下,类属性声明和对应的所有权修饰符是同样的作用。如下所示

Objective-C高级编程之内存管理篇_第7张图片
属性声明的属性与所有权修饰符的对应关系

你可能感兴趣的:(Objective-C高级编程之内存管理篇)