阅读 161

iOS 内存管理:从 MRC 到 ARC

笔者作为一个刚接触 iOS 不久的新手小白,被何时需要使用 weak 弱引用折磨了许久,看了许多文章和书以后总结了 iOS 内存管理的一些相关知识。本文讲解比较浅显,不涉及源码实现,若想深入了解 ARC 建议阅读《Objective-C 高级编程》这本书。

程序内存分布

首先,我们从 C 语言开始简单了解程序内存分布。一个由 C 语言编写的程序内存主要由以下5个部分组成:

其中代码段、数据段、BSS 段在编译时由编译器分配空间,而堆和栈是在程序运行时系统分配的空间。

栈是用于存放本地变量,局部变量,函数参数值的内存区域。程序在运行时,操作系统会通过压栈和弹栈的方式来自动的分配和释放,不需要我们手动干预。堆是用于存放除了栈里的东西之外所有其他东西的内存区域,一般由程序员手动分配和释放,若程序员不释放,程序结束时可能由操作系统回收。在 iOS 中,所有的值类型是放在栈空间的,内存分配和回收不需要我们关心,系统会处理。而所有继承了 NSObject 的对象都存放在堆里,需要我们自己负责分配和释放。

引用计数器

Object-C 和 Swift 为我们提供了基于引用计数的内存管理方式。从字面上理解,引用计数器代表“对象被引用的次数”,也可以理解为有多少人正在使用这个对象。每个对象都有自己的引用计数器,系统是根据对象的引用计数器来判断什么时候需要回收其所占用的内存空间的。

  • 任何一个对象,刚创建的时候引用计数为1(alloc/new/copy)
  • 当且仅当对象的引用计数为0时,系统才会回收这个对象(dealloc)

因此,当我们需要持有对象时,为了保证对象的存在,需要使引用计数器+1;当我们不再需要持有对象时,需要使引用计数器-1,以便系统可以回收其内存空间。

MRC 手动管理引用计数

在 OC 中,NSObject 提供了 alloc 类方法,retain/release/dealloc实例方法用于内存管理,MRC 就是通过手动调用这些方法来实现对象引用计数的增加和减少。手动管理内存需要遵守以下几个原则:

  • 自己生成的对象,自己所持有

当程序调用方法名以 alloc/new/copy/mutableCopy 开头的方法来创建对象时,意味着自己生成的对象只有自己持有,该对象的引用计数+1。

  • 非自己生成的对象,自己也能持有

当程序调用对象的 retain 方法时,意味着程序持有了非自己生成的对象,该对象的引用计数+1。

  • 不再需要自己持有的对象时释放

自己持有的对象,一旦不再需要,无论是否是自己生成的,持有者都有义务释放该对象。程序通过调用对象的 release 方法释放该对象,引用计数-1。

但是有时候我们不知道到底什么时候可以将对象释放,例如作为函数返回值返回时。为了解决这个问题,OC 提供了 autorelease 方法。

autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池 autorelease pool 中,而对象本身的引用计数并不增加,类似于 C 语言中局部变量的特性。在程序主循环的 RunLoop 或在其他程序可运行的地方,会对 release pool 对象进行生成、持有和废弃处理。当自动释放池被销毁时,会对池子里面的所有对象做一次release操作,从而达到管理引用计数的目的。

  • 非自己持有的对象不能释放

对于既不是以 alloc/new/copy/mutableCopy 开头的方法生成并持有的对象也不是用 retain 方法持有的对象,绝对不能使用 release 方法释放对象,否则就会造成程序崩溃。

ARC 自动管理引用计数

管理引用计数的本质部分在 ARC 中其实并没有发生改变,ARC 只是会自动地帮我们处理引用计数。ARC 是编译器特性,而不是运行时特性,可以对每个文件选择使用或不使用 ARC,若使用 ARC,编译器在编译时会帮我们自动的插入上一节提到的相关代码,包括retain/release/copy/autorelease/autoreleasepool等等,通过自动生成的代码去自动释放或保持对象。

ARC 中,我们不再使用 retain/release/autorelease 方法来管理内存,而是为每个变量添加所有权修饰符,系统通过变量的所有权修饰符判断如何处理引用计数。

strong

__strong 修饰符是 id 类型和所有对象类型默认的所有权修饰符,表示对对象的强引用,对应属性声明中的 strong/retain/copy 属性。在 ARC 中,给被 __strong 修饰符修饰的变量赋值即可达成对对象的持有,而该变量在超出其变量作用域后被废弃,随着强引用的失效,其引用的对象也会随之释放,从而达到管理引用计数的目的。

weak

__weak 修饰符表示对对象的弱引用,对应属性声明中的 weak 属性。在 ARC 中,弱引用不能持有对象实例,所以在超出其变量作用域时,对象即被释放,可以用来避免出现循环引用的问题。在持有某对象的弱引用时,若该对象被废弃,则此弱引用会自动失效,且处于赋值 nil 的状态。

unsafe_unretained

__unsafe_unretained 修饰符是不安全的所有权修饰符,对应属性声明中的 assign/unsafe_unretained 属性。在 ARC 中,被 __unsafe_unretained 修饰的变量不属于编译器的内存管理对象。

autorelease

ARC 中虽然不能调用 autorelease,但是可以通过将对象赋值给附加了 __autorelease 修饰符的变量来替代调用 autorelease 方法,将对象注册到 autorelease pool。

空指针与野指针

空指针是指没有存储任何内存地址的指针,或是被赋值为 nil 的指针。在 OC 中,通过空指针访问空对象或是给空指针发消息都是安全的,不会产生异常或引起崩溃。

而野指针是不安全的。一个已经被释放的对象,叫做僵尸对象,指向僵尸对象的指针被称为野指针,野指针的存在是十分危险的。如果我们通过野指针去访问僵尸对象,在原对象空间还没有重新分配出去之前,是不会出现问题的,但是如果空间已经被重新分配(有很大的可能),那么就会引起程序崩溃。因此对于野指针,我们需要小心处理。

循环引用

引用计数式内存管理中必然会发生的问题,就是循环引用问题。如果两个对象 A 和 B 互相强引用,那么它们永远不会被释放,即使外界已经没有任何指针能够访问到它们了,它们依然存在,并且互相引用,无法被释放。循环引用容易发生内存泄漏,因为应当废弃的对象在超出其生存周期后继续存在。

解决循环引用问题主要有两个方法:

1. 主动断开循环引用

当我们知道这里会产生循环引用时,在合理的位置主动断开环中的一个引用,打破循环,使得对象可以被回收。例如 GCD 和 YTKNetwork 网络请求等,由于持有 block 造成了循环引用,在运行结束后会采取这种方法在 block 执行完成后,主动释放对于 block 的持有,将其赋值为 nil,主动打破循环引用,避免内存泄漏。

// YTKNetwork 中执行完成后对于 block 的处理
- (void)clearCompletionBlock {
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}复制代码

不过,主动断开循环引用这种操作依赖于程序员自己手工显式地控制,相当于回到了以前 “谁申请谁释放” 的 MRC 内存管理年代,它依赖于程序员自己有能力发现循环引用并且知道在什么时机断开循环引用回收内存,所以这种解决方法并不常用,更常见的办法是使用弱引用的办法。

2. 使用弱引用

弱引用并不持有对象,也就不会增加引用计数,这样就避免了循环引用的产生。在 iOS 开发中,最常见的弱引用就是 delegate。举个例子来说,ViewController 持有某个 View, 同时 View 的一些点击事件需要通过 delegate 的方式交给 VC 来处理。这个时候,View 的 delegate 成员变量通常是一个弱引用,以避免相互引用对方造成循环应用的问题。

除了 delegate,还有一个弱引用最常出现的地方是 block。由于 block 会捕获 block 中使用的变量,稍一不注意就有可能会出现循环引用的问题。例如,ViewController 持有 block,同时 block 中的代码又用到了 self,那么这个时候就会造成循环引用。为了避免循环引用就需要使用弱引用 weakself,最常见的写法是 weak-strong-dance。

__weak typeof(self) weakself = self;
self.completionHandler = ^(void){
    __strong typeof(weakself) strongself = weakself;
    // do something with strongself
}复制代码

需要注意的是,weak-strong-dance 有时会导致 block 中的代码并没有执行,因为有可能在需要执行 block 中的代码时 self 已经被释放,弱引用 weakself 被置为 nil。并且,并不是所有的 block 中使用 self 都会造成循环引用,像上文提到的,一些类会在执行完成后主动释放,因此我们在使用前需要特别注意是否会引起循环引用,再来使用 weak-strong-dance。


关注下面的标签,发现更多相似文章
评论