内存管理是iOS开发中的重点和难点,也是技能进阶的重要关口,也常常在面试问题中出现。内存管理涉及到的核心话题包括:内存的管理规则、垃圾回收机制、内存泄漏和循环引用等等。下面整理了iOS中涉及到的内存管理相关知识。
(一)iOS 开发中的内存管理规则
引用计数
OC 中通过引用计数来实现内存管理:当一个对象被持有的时候计数加一,不再被持有的时候引用计数减一,当引用计数为零的时候,则将这个对象释放。
- MRC:手动引用计数,iOS 5 之前的版本采用。
- ARC:自动引用计数,iOS 5 以及之后的版本采用。
MRC 和 ARC 之间的转换
- MRC 转 ARC :在控制开关中添加-fobjc-arc 以ARC进行编译。
- ARC 转 MRC :在控制开关中添加-fno-objc-arc 以MRC进行编译。
自动释放池
OC对象的生命周期取决于引用计数,我们有两种方式可以释放对象:一种是直接调用release释放;另一种是调用autorelease将对象加入自动释放池中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。
自动释放池中 release 和 drain 的区别
release 和 drain 的作用是一样的,都是清理自动释放池,区别是:drain 在支持 GC 的系统中(Mac)可以引起 GC 回收操作,而 release 不可以
(二)Objective-C 开发中的垃圾回收
垃圾回收(Garbage Collection, GC)简单地说就是程序中及时处理废弃不用的内存对象的机制,防止内存中废弃对象堆积过多造成内存泄漏。
平台局限性
Objective-C 语言本身是支持垃圾回收机制的,但是具有平台局限性,仅限于Mac 桌面系统开发中,在iPhone 和 iPad 等苹果移动端开发中是不支持垃圾回收机制的。
垃圾回收和引用计数的区别
垃圾回收是宏观的,对整体进行内存管理:将所有对象看作一个集合,然后在GC循环中定时监测活动对象和非活动对象,及时将永不倒的非活动对象释放以避免内存泄漏,也就是说将用不到的垃圾对象交给 GC 来管理释放,而无需开发者关心。
相比于 GC,引用计数是局部性的,是管理每个对象的引用计数,单个对象引用计数为0后会马上被释放。
(三)僵尸对象
产生原因
一个引用计数为0的Objective-C被释放后就变成僵尸对象,也就是过度释放的对象。
调试方法
遇到EXC_BAD_ACCESS 这类问题一般都是僵尸对象引起的,可以开启僵尸模式(Zombie Objects)定位。具体步骤这里不详细给出。
(四)野指针
产生原因
野指针又叫“悬挂指针”,野指针出现的原因是指针没有赋值,或者是指针指向的对象已经被释放掉了。开发中应该给野指针及时赋予零值,避免内存报错。
与空指针区别:向空指针发送消息不会报错。
调试方法
野指针指向一块随机的垃圾内存,向它们发送消息会报 EXC_BAD_ACCESS 错误导致程序崩溃。可以开启僵尸模式(Zombie Objects)定位。具体步骤这里不详细给出。
(五)空指针
定义
空指针不用于野指针,它是一个没有指向任何内容的指针。空指针是有效的指针,值为 nil、NULL、Nil 或者0等,给空指针发送信息不会报错,只是不会相应信息而已。
(六)nil、Nil、NULL 和 [NSNull null] 的区别
- nil:当一个对象置为nil时,这个对象的内存地址就会被系统收回。置空之后是不能进行retain,copy等跟引用计数有关的任何操作的。
- Nil:nil完全等同于Nil,只不过由于编程习惯,人们一般把对象置空用nil,把类置空用Nil。
- NULL :这个是从C语言继承来的,就是一个简单的空指针。
- [NSNull null]:[NSNull null]和nil的区别在于,nil是一个空对象,已经完全从内存中消失了,而如果我们想表达“我们需要有这样一个容器,但这个容器里什么也没有”的观念时,我们就用到[NSNull null],它就是为“值为空的对象”。如果你查阅开发文档你会发现NSNull这个类是继承NSObject,并且只有一个“+ (NSNull *) null;”类方法。这就说明NSNull对象拥有一个有效的内存地址,所以在程序中对它的任何引用都是不会导致程序崩溃的。
(七)内存泄漏
产生原因
内存泄漏指动态分配内存的对象在使用完后没有被系统回收,导致对象始终占用内存,又无法通过代码访问。大量内存泄漏会导致系统内存不足的问题。
解决与调试方法
- 对程序员而言,要深入理解内存管理原则,养成良好的编程习惯以减少内存泄漏情况的发生。
- 使用 Xcode 提供的检测调试工具 Instruments,检测可能导致内存泄漏的代码,并及时优化。
(八)循环引用
产生原因
当多个对象相互持有形成一个封闭的环时,循环引用问题随之出现,导致内存泄漏。
解决方法
- 自己明确知道这里会存在循环引用,在合理的位置主动断开环中的一个引用(置为nil),使得对象得以回收;
- 使用弱引用。
iOS 开发中常出现循环引用的地方
- 代理 delegate。详情可参考 (二)iOS 开发设计模式:代理、观察者、单例和工厂模式 中代理部分。
- block 容易导致循环引用。
- NSTimer 引起的循环引用。具体详情查看链接 iOS实录8:解决NSTimer/CADisplayLink的循环引用
(九)深拷贝和浅拷贝
浅拷贝只是复制了内存地址,也就是对内存空间的引用;深拷贝是开辟新的空间并且复制原空间相同的内容,新指针指向新空间内容。
对 immutable 对象进行 copy 操作,是浅拷贝,mutableCopy 操作是深拷贝;对 mutable 对象进行 copy 和 mutableCopy 都是深拷贝。