EffectiveObjective-C2.0 笔记 - 第五部分

EffectiveObjective-C2.0 笔记 - 第五部分

5 内存管理

5.1 理解引用计数

1、引用计数

Objective-C 语言使用引用计数来管理内存,每个对象都有个可以递增递减的计数器,用以表示当前有多少个事物想令此对象继续存活下去。当这个计数器归零那么这个对象就会被释放。

查看引用计数的方法叫做 retainCount 但是实际并不建议使用这个方法调试代码

NSObject 协议声明下面三个方法用于操作计数器,以递增或递减其值:

  • retain 递增保留计数
  • release 递减保留计数
  • autorelease 待稍后清理 “自动释放池” 时,再递减保留计数

在调用release 之后,对象所占的内存可能会被回收,这样子在调用对象的方法就可能使程序崩溃,这里 “可能” 的意思是对象所占的内存在 “解除分配” (deallocated)之后,只是放回 “可用内存池”(avaiable pool)。若果执行方法时尚未覆写对象,那么对象仍然有效。

为避免在不经意间使用无效对象,一般在调用完release 之后都会清空指针,保证不会出现可能指向无效对象的指针,这种指针通常被称为 “悬挂指针”(dangling pointer)。

所有的对象最终都间接或直接的被一个根对象所引用,macOS 应用是 NSApplication 对象,iOS 则是 UIApplication 对象,这两个对象都是应用启动时创建的单例

2、自动释放池 autorelease

调用release 会立刻递减对象的保留计数(这里可能会令系统回收此对象),调用autorelease 方法,并不会马上减少对象的引用计数,而是在下一次 Event Loop(事件循环)时减少,以达到延迟释放对象的效果。

autorelease 能延长对象声明周期,使其在跨越方法调用边界后依然可以存活一段时间。

调用 release 并不会使对象被释放,对象释放被释放取决于引用计数是否为 0

5.2 以 ARC 简化引用计数

1. 内存泄漏:

没有正确的释放已经不再使用的内存。

2. ARC自用引用计数

1) ARC 只是自动为代码添加内存管理相关的代码

ARC 是通过在编译时在我们的代码中插入对应的内存管理代码,并且只适用于 Objective-C 的代码,使用ARC 时,引用计数实际上还是要执行的,只是保留与释放操作是由ARC 自动添加的。

2) 在 ARC 下,不允许调用内存管理方法 retain,release,autorelease,dealloc

ARC会自动执行以下等操作,所以在ARC下调用这些内存管理方法是非法的

  • retain
  • release
  • autorelease
  • dealloc

ARC 在调用这些方法时,并不是普通的Objective-C 消息派发机制,而是直接调用其底层的C 语言函数,这样子性能会更好。

3. 使用ARC 时必须遵循的方法命名规则

将内存管理语义在方法名中表示出来,若方法名以下列词语开头,则返回的对象归调用者所有:

  • alloc
  • new
  • copy
  • mutableCopy

4. 变量的内存管理语义

1) ARC 也会处理局部变量与实例变量的内存管理。

2 )我们通常会给局部变量加上修饰符来打破 “块”(block)所引入的 “保留环”(retain cycle)。

变量内存管理语义修饰符

  • __strong:默认,强引用,表示需要保留这个值
  • __weak:弱引用,表示不保留这个值,并且如果系统回收这个对象,那么在获取此变量的值的时候会的到 nil
  • __unsafe_unretained:不安全的引用,不保留此值,系统回收这个对象的时候,不会清空变量的值
  • __autoreleasing:把对象“按引用传递”给方法时使用,表示此值在方法返回时自动释放

5. ARC 如何清理实例变量

对实例变量进行内存管理,必须在 “回收分配给对象的内存” 时生成必要的清理代码。凡事具备强引用的变量,都必须释放,ARC 会在dealloc 方法中插入这些代码。

  • ARC 只负责管理Objective-C 对象的内存

ARC 会借用Objective-C++ 的一项特性来生成清理代码,在回收对象时,待回收对象会调用所有C++ 对象的析构函数,编译器如果发现某个对象里含有C++ 对象,就会生成名为.cxx_desteuct 的方法,ARC 借助此特性,在该方法中生成清理内存所需的代码。

  • 对于非Objective-C 的对象,需要我们手动清理。

如 CoreFoundation 对象不归ARC 管理,开发者必须适时调用CFRetain/CFRelease。

6. 覆写内存管理方法

非ARC 时可以覆写内存管理方法,在ARC 下禁止覆写内存管理方法,会干扰到ARC 分析对象生命周期的工作。

5.3 在 dealloc 方法中只释放引用,并解除监听

对象在经历生命周期后,最终会为系统回收,这时候就要执行dealloc 方法。每个对象生命周期内,此方法只会调用一次,也就是保留计数为0 的时候,绝对不能自己调用dealloc 方法,运行期会在适当的时候调用,一旦调用,对象就不再有效了,后续的方法调用均是无效的。

dealloc 方法主要是释放对象所拥有的引用,也就是把Objective-C 对象都释放掉,ARC 会通过自动生成的.cxx_desteuct 方法,在dealloc 中为你自动添加这些释放代码。但是其他非Objective-C 对象就需要自己手动释放了。

1. dealloc 方法中需要做的事情:

  • 释放对象所拥有的引用,持有的对象(ARC 下自动加入施放代码)
  • 清理观察者
  • 清理通知
  • 如果不使用 ARC,那么需要调用 [super dealloc] 方法

2. dealloc 方法中不适合做的事情:

  • 释放开销较大或系统内稀缺的资源(文件描述符,套接字,大量内存等)

因为 dealloc 方法并不会在特定时机调用,一般对于使用这样资源的对象都需要提供名字类似 open 和 close 的方法处理申请和释放资源的行为

  • 执行异步任务

异步方法执行后,对象可能已经施放

  • 尽量不要去调用方法,包括属性的存取方法

在dealloc 里尽量不要去调用方法,包括属性的存取方法,因为在这些方法可能会被覆写,并在其中做一些无法在回收阶段安全执行的操作。

5.4 编写 “异常安全代码” 时留意内存管理问题

1. C++ 和 Objective-C 的异常互相兼容,可以相互抛出捕获

纯C 中没有异常,C++与Objective-C 都支持异常,在运行期系统中C++与Objective-C 异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序” 来捕获。

2. 捕获异常时,一定要注意将try 块内创建的对象清理干净。

Objective-C 错误模型表明,异常只应发生严重错误后抛出,发生异常如何管理内存很重要,在try 块中保留某个对象的,但是在释放它之前抛出异常了,这时候就无法正常释放了,这时候需要借助@finally 块来保证释放对象的代码一定会执行,且只执行一次。

3. 默认情况下,ARC 不生成安全处理异常所需的清理代码。

在ARC 不会自动生成处理异常中的代码,因为这样子需要加入大量的样板代码,以便追踪待清理的对象,从而在抛出异常时将其释放。可以这段代码会严重运行期的性能,还会增加应用程序的大小。

可以通过-fobjc-arc-exceptions 这个编译编织来开启这个功能,但是这个功能不应该作为生成这种安全处理异常所用的附加代码,应该是让代码处于Objective-C++模式。

5.5 以弱引用避免保留环

1. 相互引用和对象引用环

几个对象都以某种方式互相引用,从而形成 “环”,这种情况通常会泄漏内存,因为没有东西引用环中对象,这样子环里的对象互相引用,不会被系统回收,会导致内存泄漏。

2. 避免保留环的最佳方式就是弱引用

  • 非 ARC 的情况下使用 assign 或者 unsafe_unretained 来修饰弱引用属性
  • ARC 的情况下使用 weak 来修饰弱引用的属性,因为 weak 的属性在对象被释放后会自动设置为 nil

一般来说,如果不拥有某对象,就不要保留它,这条规则对collection 例外,collection 虽然不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。

5.6 以 “自动释放池块” 降低内存峰值

1. 释放对象有两种方式:

1) 一种是调用release 方法,使其保留计数立即递减

ARC下不能主动调用

2) 一种是调用autorelease 方法

将对象放入 “自动释放池” 中,自动释放池用于存放那些需要稍后某个时刻释放的对象,清空(drain)自动释放池时,系统会向其中的对象发送release 消息。

创建自动释放池,系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行 “事件循环”时,都会将其清空。

1
2
3
@autoreleasepool {
//...
}

2. 内存峰值:

是指应用程序在某个特定时段内的最大内存用量。如:循环创建大量对象的时候

  • 对象有可能会放在自动释放池里面,需要等到线程执行下一次事件循环才会清空,这里会导致应用程序所占内存会持续增加,等到临时对象释放的时候,内存用量又会突然下降。我们现在就想把这个内存峰值给降低下来。
1
2
3
4
5
for (int i = 0;i < 100000;i++){
@autorelease{
NSObject *object = [NSObject new];
}
}

5.7用 “僵尸对象” 调试内存管理问题

1. 僵尸对象用于调试代码是否会使用到已经被销毁的对象

向已回收的对象发送消息是不安全的,是否崩溃这个是看对象所占的内存有没有为其他内容所覆写。

  • Cocoa 提供 “僵尸对象”(Zombie Object)这个非常方便的功能,开启后,运行期系统会把已经回收的实例转换成特殊的 “僵尸对象”,而不会真正回收它们。这个对象所在的核心内无法重用,因此不可能遭到覆写,僵尸对象收到消息后,会抛出异常。

2. XCODE 设置

Xcode Scheme 中的Enable Zombie Objects 选项,打开会将NSZombieEnabled 环境变量设成YES。

  • 系统在即将回收时,会执行一个附加步骤,将对象转换成僵尸对象,而不彻底回收。僵尸类是从名为NSZombie 的模版类复制出来的。NSZombie 类并未实现任何方法,此类没有超类,因此跟NSObject 一样,也是一个 “根类”,该类只有一个实例变量,叫做isa,所以发给他的消息都要经过 “完整的消息转发机制” 。

  • 在完整的消息转发机制中,forwarding 是核心,检查接受消息的对象所属的类名,若是NSZombie ,则表示消息接受者是僵尸对象,需要特殊处理。

  • 系统在回收对象时,可以不将其真的回收,而是把它转化成僵尸对象。通过环境变量NSZombieEnabled 可开启此功能。

  • 系统会修改对象的isa 指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

5.8 不要使用 retainCount

1. retainCount 在反映调用者有多少对象引用,以及调试内存管理都没有任何帮助

  • 每个对象都有一个计数器,其表明还有多少个其他对象想令此对象继续存活。在ARC retainCount 这个方法已经废弃了,但是在非ARC 中也不应该调用这个方法,因为这个保留计数只是返回某个时间点的值,并不会联系上下文给出真正有用的值。

2. retainCount 在 ARC 环境下将会编译错误

  • retainCount 可能永远不返回0,因为系统有时候会优化对象的释放行为,在保留计数为1的时候就把它回收了。

  • 不应该依靠保留计数的具体址来编码。

  • 对象的保留计数看似有用,实则不然,因为任何给定时间点上的 “绝对保留计数”(absolute retain count)都无法反映对象生命期的全貌。

  • 引入ARC 之后,retainCount 方式就正式废止了,在ARC 下调用方法会导致编译器报错。

3. 发布版本时,一定关闭此功能