从苹果的官方文档来看, OC 对应用程序的内存管理提供了 2 种方法.
第一种即 "manual retain-release"(MRR), 手动保留释放, 也可理解为手动引用计数.
第二种,"Automatic Reference Counting"(ARC), 自动引用计数. 但是 ARC 并不等同垃圾回收. 在苹果的官方文档有这样一句话,"You are strongly encouraged to use ARC for new projects." 意思是苹果强烈建议在项目中运用 ARC 机制来管理内存.
内存管理不当的话会出现以下 2 种问题:
1. 过早释放: 在某处程序用完某块内存之前, 就将该内存还给了 "堆".(这里的堆指的是, iOS 启动应用时, 会为应用保留一部分空闲的 RAM, 这部分空闲的 RAM 称为堆. 应用程序可随意使用堆, 不会影响 iOS 的其他部分, 也不会影响其他应用.)
2. 内存泄露: 不释放已经不使用的内存会导致内存泄露, 即使他从来没有被再次使用过. 内存泄露会导致你的应用程序的内存使用量日益增加, 这反过来有可能会导致系统性能较差或申请内存被终止.
要说明的一点是不管是 MRR 中的 "通过属性机制简化存取方法"(在 "存" 方法中涉及到了基本的内存管理), 还是 ARC. 本质上都是苹果帮助程序员在开发时减少了代码量, 把原来由程序员要完成的工作交给编译器去完成, 从而减少软件开发的繁琐程度.
接下来就 MRR 和 ARC 进行详细的说明.
在 OC 中所有的类均继承于基类 NSObject, 那么所有的类就都有一个类方法 alloc 和一个实例方法 dealloc. 当通过向类发送 alloc 方法来创建类实例时, 系统会从堆中分配出相应字节数的内存 (注: 指针类型的实例变量大小是 4 个字节, 这是保存堆中的对象地址所需的内存空间). 例如: UIView *view = [[UIView alloc] init]; alloc 会返回一个指针对象, 指向新分配的内存. 分配内存后, 在类完成其 "功能" 后, 还要将内存还给堆. 但是不可以直接向对象发送 dealloc 方法, 即这样写[view dealloc] 是不对的, 只能由对象自己向自己发送 dealloc 方法. 对于 dealloc, 苹果官方文档是这么解释的: The NSObject class also defines a method, dealloc, that is invoked automatically when an object is deallocated(NSObject 定义了一个方法 dealloc, 当对象被释放时自动调用). 那么对象何时释放? 释放时是否安全呢? 这个在 OC 中是通过引用计数来解决这个问题的.
一. 引用计数算法
对象创建后, 这个对象就有一个所有者. 对象在其生命周期可以有不同的所有者, 也可以同时有多个所有者, 引用计数既是用来记录所有者的数量. 当对象没有所有者时, 即引用计数为 0 时, 就会释放自己. 作为对象本身不需要知道所有者是谁, 只需知道所有者的个数. 对象通过 retain 计数跟踪所有者的数量. 这个是通过 NSObject 定义的协议与标准方法命名约定相结合的方法来实现的. 引用计数其实是在进行 "责任落实": 谁创建了对象(或保留了已经创建的对象), 谁就是该对象的所有者. 释放对象即放弃该对象的所有权. 谁有对象的所有权, 谁就要负责放弃该所有权. 在不能再向相应对象发送消息时, 即不再拥有指向该对象的指针时, 需要放弃该所有权. 但此算法无法回收循环引用的存储对象. Cocoa 目前采用的就是此种机制.(Cocoa 是苹果公司为 Mac OS X 所创建的原生面向对象的 API, 是 Mac OS X 上五大 API 之一, 其它四个是 Carbon,POSIX,X11 和 Java)
manual retain-release(MRR)
MRR 可以理解为当对象创建后, 会有一个所有者, 即新建对象的 retain 计数是 1. 当对象得到某个所有者时, retain 计数 + 1, 当对象失去某个所有者时, 调用 release 方法, retain 计数 - 1. 当对象没有任何所有者时, retain 计数为 0. 对象会自动调用 dealloc, 将所占用的内存还给堆. 用代码来实现就是:
- - (id)retain
- {
- retainCount++;
- return self;
- }
- - (void)release
- {
- retainCount--;
- if(retainCount == 0){
- [self dealloc];
- }
- }
何为所有者? 何为拥有该对象的所有权? 就是当你在 OC 中用 "alloc", "new", "copy", or "mutableCopy" 方法创建对象后, 即是此对象的拥有者. 还有就是当你在保留某一对象的值的时候, 也是拥有了该对象(属性中的 set 方法深刻的说明了这一点). 以 nane 属性为例, 它的 set 方法应该写为:
- - (void)setName:(NSString *)str
- {
- [str retain];
- [name release];
- name = str;
- }
这里必须先保留新对象, 再释放当前对象. 这是因为 name 和 str 有可能指向同一个对象. 如果颠倒顺序, 就有可能释放掉原本打算作为 name 保留的对象. 在类中, 当类拥有其它实例对象的时候, 要在 dealloc 方法中将其 release 掉.
引用计数的规则:
1. 如果用来创建对象的方法, 其方法名是以 alloc 或 new 开头的, 或是包含 copy 和 mutableCopy, 那么你已经拥有该对象的所有权. 你要负责在不需要该对象的时候将其释放.
2. 如果你不拥有某个对象, 但是要确保该对象继续存在, 那么可以通过向其发送 retain 消息来获得所有权(retain 计数 + 1).
3. 当你拥有某个对象并且不再需要该对象的时候, 要 release 或 autorelea 掉.(下面会详细介绍 autorelease)
4. 只要对象还有至少一个所有者, 该对象就会继续存在下去, 只有在 retain 计数为 0 时, 才会收到 dealloc 消息.
使用自动释放池(autorelease)
苹果官网是这样解释的: 自动释放池块提供了一种机制, 让你可以放弃对象的所有权, 但要避免它被立即释放 (例如, 当您返回一个对象的类方法) 的可能性. 通常情况下, 你并不需要创建自己的 autorelease 池块. 在 OC 中的类方法, 是为 "他人" 创建对象,"自己" 不拥有, 也不使用. 那类方法中对象的内存怎么管理呢? 这里需要某种解决方案, 能够暂时不释放对象, 但具备释放该对象的权利. 通过向对象发送 autorelease 消息, 可以将对象标记为 "稍后释放". 当对象收到 autorelease 后, 不会马上释放, 而是会加入一个 NSAutoreleasePool 实例. 该 NSAutoreleasePool 实例会记录所有标记为 "稍后释放" 的对象. 每隔一段时间, 这个 NSAutoreleasePool 实例会被 "排干(drain)", 这时它会向其包含的所有对象发送 release 消息, 然后移除这些对象.
标记为 autorelease 的对象有 2 种命运: 要么走完对象的生命周期, 直到被释放, 要么被另外一个对象保留. 当某一对象保留了标记为 autorelease 的对象后, 那么他的 retain count 计数会变成 2, 将来的某个时候, NSAutoreleasePool 实例会释放该对象, 使其 retain count 计数为 1. 何为 "将来的某个时候"?iOS 应用在运行时, 存在一个运行循环 (run loop). 该运行循环等待事件(event) 的发生, 例如触摸事件或定时器触发 (NSTimer) 等等, 当事件发生时, 应用会跳出运行循环并通过调用某个类方法来处理相应的事件. 代码执行完毕后, 应用将返回当前的运行循环. 每次循环结束, 所有标记为 autorelease 的对象都会收到 release 消息.
Automatic Reference Counting(ARC)
ARC 机制极大的减少了开发过程中常见的程序错误: retain 跟 release 不匹配. ARC 并不会消除对 retain 和 release 的调用, 而是把这项原本大都属于开发者的工作移交给了编译器. ARC 并不等同于垃圾回收. retain 和 release 仍然会被调用, 所以有一些开销, 在 release 的时候可能还会调用 dealloc 方法. 这段代码与程序员手动调用 retain 和 release 的代码在运行结果上是完全一致的. 垃圾回收机制是在运行时起作用的, 会影响运行效率, 而 ARC 是在编译时插入内存管理代码, 不影响运行时效率, 因此内存回收比垃圾回收时的效率要高, 能够提升系统性能. 这种编译器可以自由地以多种方式优化内存管理, 而让程序员手动去做这些工作是不现实的. 在多数情况下, 使用 ARC 生成内存管理代码的程序比程序员手工添加内存管理代码的对等程序运行更快!
ARC 不是垃圾回收, 尤其是它不能像 Snow Leopard 中的垃圾回收机制那样处理循环引用. 因此, 在 iOS 开发中, 必须要做好对强引用 (strong reference) 的跟踪管理以免出现循环引用. 属性关系有两种主要类型: strong 和 weak. 相当于非 ARC 环境里的 retain 和 assign. 只要存在一个强引用, 对象就会一直存在, 不会被销毁. OC 中一直存在循环引用的问题, 但在实际应用中很少出现循环引用. 对于过去那些使用 assign 属性的地方, 在 ARC 环境中要使用 weak 代替. 大部分引用循环是由委托 (delegate) 引起的, 所以应该总是把 delegate 属性声明为 weak. 当引用的对象被销毁之后, weak 引用会被自动设置为 nil, 与 assign 相比这是一个巨大的进步, 因为 assign 可以指向被释放掉的内存, 导致程序奔溃.
二. 可达性分析算法
近现代的垃圾回收实现方法, 通过定期对若干根储存对象开始遍历, 对整个程序所拥有的储存空间查找与之相关的存储对象和没相关的存储对象进行标记, 然后将没相关的存储对象所占物理空间回收. 既通过一系列的 GC Roots 的对象作为起始点, 从这些根节点开始向下搜索, 搜索所走过的路径称为引用链(Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连时, 则证明此对象是不可用的. 即是可回收的. 此算法可回收循环引用的存储对象.(Java 和 C# 语言采用的机制)
引申阅读: 深拷贝和浅拷贝
深拷贝: 简单说就是对指针指向的内容进行拷贝, 以字符串为例, 就是指创建一个新的指针在一个新的地址区域创建一个字符串, 这个字符串与原字符串值相同, 新的指针指向这个新创建的字符串. 而原字符串的引用计数没有 + 1
浅拷贝: 既指针拷贝, 例如一个指针指向一个字符串, 也就是说这个指针变量的值是这个字符串的地址, 那么对这个指针拷贝就是又创建了一个指针变量, 这个指针变量的值是这个字符串的地址, 也就是这个字符串的引用计数 + 1
关于深浅拷贝看源码一目了然, 以 NSString 和 NSMutableString 为例:
- - (id)copyWithZone:(NSZone*)zone
- {
- if (NSStringClass == Nil) NSStringClass = [NSString class];
- return RETAIN(self)
- }
- - (id)mutableCopyWithZone:(NSZone*)zone
- {
- return [[NSMutableString allocWithZone:zone] initWithString:self];
- }
看上面代码, 当属性设置 copy 时, 实际调用的就是 copyWithZone 方法, 而 copyWithZone 并没有创建新的对象, 而是使指针持有了原来的对象, 即浅拷贝. 而属性设置 mutableCopy 时, 调用的就是 mutableCopyWithZone 方法, 而这个方法创建了一个新的可变字符串对象, 即深拷贝.
来源: https://juejin.im/post/5bf907fc51882507e94b8b50