基础知识提要
对于 Objective-C 对象来讲,手动内存管理主要是通过以下方法来进行的:
- retain : 使得对象的引用计数+1
- release: 使得对象的引用计数-1
- autorelease: 使得对象的引用计数在 autorelease pool 释放的时候再-1
- dealloc: 当对象的引用计数为0的时候自动调用,表明对象被释放
因为只有 OC 对象是分配在堆上的(其他如 C 语言对象是分配在栈上的),因此也只有 OC 对象在未开启 ARC 的时候需要我们手动管理内存。
对象的计数器,用来表示当前有多少个事物想令此对象继续存活下去。
对象的内存管理
简单内存管理示例
简单的手动内存管理:
|
|
对应的 ARC 自动管理:
因为开启 ARC 后,编译器会自动对 OC 对象进行内存管理,所以,ARC 有效时,不能调用 retain /release /autorelease /dealloc /retainCount 方法,其中,dealloc 方法可以覆写,但依然不能显示调用。
|
|
ARC 对象所有权修饰符
- __strong: 默认修饰符,表示对对象的“强引用”,该修饰符修饰的变量在超出其作用域时被废弃,随着强引用的失效,自动 release 自己所持有的对象;
- __weak: 弱引用。不持有对象,若该对象被废弃,则弱引用变量将自动赋值为 nil;
- __unsafe_unretained: 同 __weak 一样不持有对象,但对象废弃时,不会自动为 nil,容易出现悬挂指针;
- __autoreleasing: 相当于调用 autorelease 方法,即对象被注册到 autorelease pool 中。
什么叫做持有对象?
我们知道 OC 对象的变量类型其实是指针变量,这些指针存储在栈上,指针指向的对象存储在堆中。
指针 X1 指向对象 A,并使得对象 A 的引用计数+1,则我们说指针变量 X1 持有对象 A,或者 X1 持有该对象的强引用。
指针 X2 虽然指向对象 A,但是对对象 A 的引用计数没有任何影响,即 X2 不指向对象 A,对象 A 的引用计数也不会减1;X2 指向对象 A,对象 A 的引用计数也不会加1,则我们说指针变量 X2 不持有对象 A。
|
|
修饰符番外
id 的指针或对象的指针会默认加上 __autoreleasing 修饰符,如NSError **error
,实际上为NSError *__autoreleasing* error
。
对象被废弃时,含有 __weak 修饰符的变量将会有以下动作:
1) 从 weak 表中获取以废弃对象的地址为键值的记录;
2) 将包含在记录中的所有 __weak 修饰符变量的地址,赋值为 nil;
3) 从 weak 表中删除记录;
4) 从引用计数表中删除以被废弃对象的地址为键值的记录。
也就是说含有 __weak 修饰符的变量所指的对象被废弃时,会比其他修饰符多执行前3步,如果大量使用 weak 修饰符,则会消耗相应的 CPU 资源,因此最好是在需要避免循环引用的时候才使用 __weak 修饰符。
在访问有 __weak 修饰符的变量时,其实会访问注册到 autorelease pool 的对象。
1 | { |
1 | { |
在 @autoreleasepool 块结束之前,使用多少次 weak 变量,就会注册到 autorelease pool 中多少次,所以在使用 __weak 修饰符的变量时,最好赋值给 __strong 修饰符的变量后使用。
可通过
_objc_autoreleasePoolPrint()
函数打印出注册到 autorelease pool 中的对象。
方法的内存管理
方法命名规则:
- alloc/new/copy/mutableCopy 使用这些名称开头的方法,意味着生成的对象自己持有;
- 以上名称之外的其他方法取得的对象,自己不持有。
注:以 init
开始的方法必须是实例方法,且必须要返回对象,该返回对象不注册到 autorelease pool 上,基本上只是对 alloc 方法返回值的对象进行初始化处理并返回该对象。
|
|
也就是说对于非自己持有的方法,比如 [NSMutableArray array] 方法,在其方法内部,自动为返回值添加了 autorelease,我们可以使用这个返回值,但并不持有返回值所指的对象。在其对应的 autorelease pool 释放时(在主线程中,就是 RunLoop 循环一次之后),返回值所指的对象即被释放,如果没有对返回值执行 retain 操作,则对象没有持有者,自动调用 dealloc 方法,被废弃。
我们常说,ARC 有效时,编译器会自动插入 retain/release/autorelease 方法。但实际上,ARC 在调用这些方法时,并不是通过普通的 OC 消息派发机制,而是直接调用底层 C 语言版本,比如 ARC 会调用与 retain 等价的底层函数 objc_retain。这样做更能优化性能,也是不能覆写 retain、release、autorelease 方法的原因,因为这些方法不会被直接调用。
ARC 的优化还体现在很多方面,如使用非自己持有的方法,我们可以看到,在方法内部的返回对象调用 autorelease,与方法返回后,在调用方对返回对象 retain,两个操作实际上是可以抵消的,ARC 会自动做这方面的优化。以 [NSMutableArray array] 为例:
1 | // ARC 代码 |
objc_autoreleaseReturnValue
函数会检查使用该函数的方法,或函数调用方的执行命令列表,如果方法的调用方在调用了该方法后,紧接着调用了 objc_retainAutoreleasedReturnValue()
函数,那么就不将返回的对象注册到 autorelease pool 中,而是直接传递到方法的调用方。
Block 的内存管理
Block 的内存管理主要涉及到循环引用的问题。
Block 的创建一般是在栈上,但以下情况会被复制到堆上:
1) 调用 Block 的 copy 方法时;
2) Block 作为函数返回值时;
3) 将 Block 赋值给 __strong 修饰符的 id 类型或 Block 类型的成员变量时;
4) 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。
我们知道 Block 会在声明时截获在 Block 内部将会用到的变量,如:
|
|
__strong 变量
对于 OC 对象而言,当 Block 从栈上被复制到堆上时,会对将要用到的带有 __strong 修饰符的变量执行 retain 操作,也就是 Block 会持有这个变量所指向的对象。如:
|
|
- 开启 ARC 时,对于修饰符为 __strong 且捕获了外部变量(无论是否是 OC 对象)的 Block,会自动进行 copy 操作,将 Block 从栈上复制到堆上,由 NSStackBlock 转换为 NSMallocBlock。
- 修饰符为 __strong 但未捕获外部变量的 Block,或者通过声明全局变量来声明 Block,都会自动创建为 NSGlobalBlock 类型。
- 无法手动创建堆上的 Block,即 NSMallocBlock。
上述代码中 array 变量超出了作用域因此被废弃,但是 blk 调用的时候仍可以使用 array 是为什么呢?
是因为 Block 在进行 copy 操作的时候,会在自身结构体中添加一个同类型的 __strong 修饰符的 array 变量,所以访问的并不是之前我们所定义的 id array
,而是 block->array
。
__block 变量
__block 修饰符的变量可以在 Block 中更改变量,Block 在捕获变量时,会对有该修饰符的变量生成 __Block_byref_val 结构体。
1 |
|
__Block_byref_val 结构体的成员变量 __forwarding 是指向该结构体实例自身的指针。Block 在访问__block 修饰的变量时,是通过这个指针来的:
block \->val(block结构体中的成员变量)->__forwarding->val
。
当 Block 从栈复制到堆上是,该变量也会复制到堆上,栈上的原来指向自身的 __forwarding 指针会改变为指向堆上的 __block 变量。
因此 __block 修饰的变量在 ARC 和非 ARC 中是有差别的。
ARC 有效时,__block 变量除了可以在 Block 内部修改之外,无其他用处,是否 retain 取决于变量的 ARC 修饰符(__strong 持有、__weak 不持有等)。那么由于可修改,因此可以在 Block 内部对造成循环引用的变量赋值为 nil,释放掉自身的对象持有权,从而打破循环。
ARC 无效时,在 [block copy] 之后,没有__block 修饰符的变量对象会被自动后台 retain,从而被 Block 持有;而有__block 修饰符的变量反而不会被 retain,不会被 Block 持有。因此对变量添加 __block 修饰符可以在非 ARC 情况下打破循环引用。
循环引用
既然 Block 也会持有对象,那么就很容易出现不易发现的循环引用问题了。如下:
|
|
正如之前所提到的 Blcok 被复制到堆上的情况,在使用方法名中含有 usingBlock 的 Cocoa 框架方法
时会被自动 copy 到堆上,从而对捕获到的 __strong 变量
执行 retain 操作,Block 持有该变量。
在本例中,self 的成员变量 _observer 会 copy 一份 Block
,从而持有 Block,而 Block 中用到了默认为 __strong 修饰符的 self变量
,从而持有 self,self 类本身又持有 成员变量 _observer
,从而导致循环引用,使得谁都无法被最终释放,导致内存泄漏。
所以,要打破这种循环引用,需要使得 [block copy] 时不会 retain 捕获到的 self 变量。
注意:函数的闭包和 block 如果没有引用任何实例或类变量,其本身也不会造成循环引用,另外在 GCD 中,一般不会造成循环引用。这个例子之所以会造成循环引用,是因为 _observer 是 self 的成员变量。
方法一: 在 ARC 中使用不持有对象的 __weak
或 __unsafe_unretain
修饰符
|
|
对于拥有 __weak 修饰符的 wself 变量,Block 复制时,不会对该变量指向的对象进行 retain,从而不持有该对象,对该对象的引用计数无影响。在 Block 内部又通过 __strong 修饰符的 sself 变量来持有对象,是为了避免在 Block 执行过程中,该对象被其他地方释放,从而造成访问错误。这实际上是一种延迟 self 的 retain 操作,使得它不在 Block 被 copy 的时候 retain,而是在执行的时候 retain。
因为如果在最初 copy 的时候 retain,那么只有等 Block 被废弃时,该变量才会被废弃,从而释放对对象 X 的持有权。但是由于循环引用,该变量始终直接或间接的持有 block 对象,所以 Block 永远不会被废弃,进而也不会释放对象 X 的持有权,从而造成这两块内存永远不会被回收,即内存泄漏。
而在执行的时候 retain,ARC会对 Block 的执行作用域的变量自动进行内存管理,执行完毕后即释放,不会等到 Block 被废弃时才能被释放,因此打破了循环引用。
方法二:在 ARC 中使用 __block 修饰符,并在 Block 中为其赋值为 nil
|
|
由于在 Block 执行时释放了对 self 所指向的对象的持有权,因此 Block 执行后即打破循环引用,同样不会等到 Block 被废弃时才能释放对象的持有权,因此没有内存泄漏。
这种方法的缺点是,一定要确保 Block 会执行。如果有多种分支,而某一条分支中的 Block 不会执行,那么这条分支同样会造成内存泄漏。
方法三:在非 ARC 中使用 __block 修饰符
|
|
由于在非 ARC 中,Block 不持有 __block 修饰符修饰的对象,因此也不会造成循环引用。
注1:在使用委托 delegate 时,属性要用 weak 关键字也是为了避免循环引用。
注2:在异常 NSException
处理过程中,也容易遗忘对象释放,从而造成内存泄漏,一般须在 @finally
中将未释放的资源释放掉。当然如果该异常直接造成程序崩溃,也就无所谓释放不释放了。
以上即是 ARC 与非 ARC 的内存管理区别,以及 ARC 是如何将手动管理转换为自动管理的。
Reference
[1] 《Objective-C 高级编程 - iOS 与 OS X 多线程和内存管理》
[2] 《Effective Objective-C 2.0》
[3] Objective-C中block实现和技巧学习 http://www.tuicool.com/articles/aQFV7bv