什么是自动引用计数(ARC)
在 Objective-C 中采用
Automatic Reference Counting (ARC)
机制,让编译器来进行内存管理。在新一代 Apple LLVM 编译器中设置ARC为有效状态,就无需再次键入retain
或者release
代码,这在降低程序崩溃、内存泄漏等风险的问时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。
苹果的实现
NSObject 类的源代码没有公开,此处利用 Xcode 的调试器和 iOS 大概追溯出其实现过程。
在 NSObject 类的 alloc 类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数。
- +alloc
- +allocWithZone:
- class_createlnstance
- calloc
alloc 类方法首先调用 allocWithZone: 类方法,然后调用 class_
createlnstance
函数,最后通过调用 calloc
来分配内存块。class_createlnstance
函数的源代码可以通过 objc4
库中的 runtime/objc-runtime-new.mm
进行确认。
retainCount/retain/release
实例方法又是怎样实现的呢?同刚才的方法一样,下面列出各个方法分别调用的方法和函数。
|
各个方法都通过同一个调用了 _CFDoExternRefOperation
函数,调用了一系列名称相似的函数。如这些函数名的前缀 “CF” 所示,它们包含于 CoreFoundation
框架源代码中,即是 CFRuntime.c
的 _CFDoExternRefOperation
函数。为了理解其实现,下面简化了CFDoExternRefOperation
函数后的源代码。
CF/CFRuntime.c
_CFDoExternRefOperation
|
_CFDoExtemRefOperation
函数按 retainCount/retain/release
操作进行分发,调用不同的函数。NSObject 类的 retainCount/retain/release
实例方法也许如下面代码所示:
|
可以从 _CFDoExternRefOperation
函数以及由此函数调用的各个函数名看出,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。如图所示:
通过引用计数表管理引用计数的好处如下:
- 对象用内存块的分配无需考虑内存块头部。
- 引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象的内存块。
这里特别要说的是,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置。如图所示:
另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。
通过以上解说即可理解苹果的实现。
autorelease
autorelease 就是自动释放,这看上去很像 ARC,但实际上更类似于 C 语言中的自动变量的特性。
autorelease 会像 C 语言的自动变量那样来对待对象实例。当超出其作用域时,对象实例的 release 方法就会被调用。但和 C 语言的自动变量不同的是,程序员可以设定变量的作用域。
autorelease 具体使用方法如下:
- 生成并持有 NSAutoreleasePool 对象
- 调用已分配对象的 autorelease 方法
- 废弃 NSAutoreleasePool 对象
NSAutoreleasePool 对象的生命周期相当于 C 语言变量的作用域。对于所有调用过 autorelease 实例方法的对象,在废弃 NSAutoreleasePool 对象时,都将调用 release 实例方法。
用源码表示如下:
|
上述代码中的 [pool drain]
相当于 [obj release]
。
在 Cocoa
框架中,相当于程序主循环的 NSRunLoop
或者在其他程序可运行的地方,对 NSAutoreleasePool
对象进行生成、持有和废弃处理。因此,程序开发者不一定非得使用 NSAutoreleasePool
对象进行开发工作。
尽管如此,但在大量产生 autorelease
的对象时,只要不废弃 NSAutoreleasePool
对象,那么生成的对象就不能释放,因此有时会产生内存不足的现象。典型的例子是读入大量图像的同时改变其尺寸。图像文件读入到 NSData
对象,并从中生成 UIImage
对象,改变其尺寸后生成新的 UIImage
对象。这种情况下,就会大量产生 autorelease
对象。
|
在此情况下,有必要在适当的地方生成、持有或废弃 NSAutoreleasePool
对象。
|
另外,Cocoa
框架中也有很多类似方法用于返回 autorelease
的对象。比如 NSMutableArray
类的 arrayWithCapacity
类方法。
|
此源代码等同于以下源代码:
|
苹果的实现
可通过 objc4
库的 runtime/objc-arr.mm
来确认苹果中 autorelease
的实现。
|
C++ 类中虽然有动态数组的实现,但其行为和 GNUstep 的实现完全相同。
我们使用调试器来观察一下 NSAutoreleasePool
类方法和 autorelease
方法的运行过程。如下所示,这些方法调用了关联于 objc4
库 autorelease
实现的函数。
|
另外,可通过 NSAutoreleasePool
类中的调试用非公开类方法 showPools
来确认己被 autorelease
的对象的状况。showPools
会将现在的 NSAutoreleasePool
的状况输出到控制台。
|
NSAutoreleasePool
类的 showPools
类方法只能在 iOS 中使用,作为替代,在现在的运行时系统中我们使用调试用非公开函数 _objc_autoreleasePoolPrint( )。
提高调用Objective-C方法的速度
GNUstep
中的 autorelease
实际上是用一种特殊的方法来实现的。这种方法能够高效地运行0S X、iOS 用应用程序中频繁调用的 autorelease
方法,它被称为”IMP Caching”。
在进行方法调用时,为了解决类名/方法名以及取得方法运行时的函数指针,要在框架初始化时对其结果值进行缓存。
|
实际的方法调用就是使用缓存的结果值。
|
这就是 IMP Caching 的方法调用。虽然同以下源代码完全相同,但从运行效率上看,即使它依赖于运行环境,一般而言速度也是其他方法的2倍。
|
autorelease NSAutoreleasePool 对象
提问:如果 autorelease NSAutoreleasePool 对象会如何?
|
回答:发生异常
|
通常在使用 Objective-C,也就是 Foundation 框架时,无论调用哪一个对象的 autorelease 实例方法,实现上是调用的都是 NSObject 类的 autorelease 实例方法。但是对于 NSAutoreleasePool 类,autorelease 实例方法已被该类重载,因此运行时就会出错。
ARC 规则
实际上”引用计数式内存管理”的本质部分在 ARC 中并没有改变。就像”自动引用计数”这个名称表示的那样,ARC 只是自动地帮组我们处理”引用计数”部分。
内存管理的思考方式
引用计数式内存管理的思考方式就是思考 ARC 所引起的变化。
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 自己持有的对象不再需要时释放。
- 非自己持冇的对象无法释放。
这一思考方式在ARC有效时也是可行的。只是在源代码的记述方法上稍有不同。
所有权修饰符
Objective-C
编程中为了处理对象,可将变量类型定义为 id 类型或各种对象类型。
所谓对象类型就是指向 NSObject 这样的 Objective-C 类的指针,例如 “NSObject“。id 类型用于隐藏对象类型的类名部分,相当于 C 语言中常用的 “void “。
ARC 环境时,id 类型和对象类型同 C 语言其他类型不同,其类型上必须附加所有权修饰符。
所有权修饰符一共有4种。
- _strong 修饰符
- _weak 修饰符
- _unsafe_unretained 修饰符
- _autoreleasing 修饰符
_strong修饰符
_strong
修饰符是 id 类型和对象类型默认的所有权修饰符。也就是说,以下源代码中的 id 变量,实际上被附加了所有权修饰符。
|
id 和对象类型在没有明确指定所有权修饰符时,默认为 _strong
修饰符。上面的源代码与以下相同。
|
该源码在 MRC 环境该如何表述呢?
|
该源代码一看则明,目前在表面上并没有任何变化。再看看下面的代码。
|
此源代码明确指定了 C 语言的变量的作用域。MRC 环境时,该源代码可记述如下:
|
为了释放生成并持有的对象,增加了调用 release
方法的代码。该源代码进行的动作同先前 ARC 环境时的动作完全一样。
如此源代码所示,附有 _strong
修饰符的变量 obj 在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。
如 “strong” 这个名称所示,_strong
修饰符表示对对象的 “强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
当然,附有 __strong
修饰符的变量之间可以相互赋值。
正如苹果宣称的那样,通过 __strong
修饰符,不必再次键入 retain
或者 release
,完美地满足了 “引用计数式内存管理的思考方式”:
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
前两项“自己生成的对象,自己持有”和“非自己生成的对象,自己也能持有”只需通过对带_strong
的修饰符的变景赋值便可达成。通过废弃带 _strong
修饰符的变量(变量作用域结束或是成员变量所属对象废弃)或者对变量赋值,都可以做到“不再需要自己持有的对象时释放”。
最后一项“非自己持有的对象无法释放”,由于不必再次键入 release
,所以原本就不会执行。这些都满足于引用计数式内存管理的思考方式。
因为 id 类型和对象类型的所有权修饰符默认为 _strong
修饰符,所以不需要写上 “__strong”。使ARC 环境及简单的编程遵循了 Objective-C 内存管理的思考方式。
_weak修饰符
看起来好像通过 _strong
修饰符编译器就能够完美地进行内存管理。但是遗憾的是,仅通过 _strong
修饰符是不能解决有些重大问题的。
这里提到的重大问题就是引用计数式内存管理中必然会发生”循环引用”的问题。
例如,前面出现的带有 _strong
修饰符的成员变量在持有对象时,很容易发生循环引用。
|
|
以下为循环引用。
|
为便于理解,下面写出了生成并持有对象的状态。
|
循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。此代码的本意是赋予变量 test0 的对象 A 和赋予变量 test1 的对象 B 在超出其变量作用域时被释放,即在对象不被任何变量持有的状态下予以废弃。但是,循环引用使得对象不能被再次废弃。
像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)。
|
怎么样才能避免循环引用呢?看到 _strong
修饰符就会意识到了,既然有 strong
,就应该有与之对应的 weak
。也就是说,使用 _weak
修饰符可以避免循环引用。
_weak
修饰符修饰符相反,提供弱引用。弱引用不能持有对象实例。
|
变景 obj 上附加了 _weak
修饰符。实际上如果编译以下代码,编译器会发出聱告,
|
此源代码将自己生成并持有的对象赋值给附有 _weak
修饰符的变量 obj
。即变量 obj
持有对持有对象的弱引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会立即被释放。编译器对此会给出警告。如果像下面这样,将对象赋值给附有 _strong
修饰符的变量之后再赋值给附有 _weak
修饰符的变量,就不会发生警告了。
|
下面确认对象的持有状况。
|
因为带 _weak
修饰符的变量(即弱引用)不持有对象,所以在超出其变最作用域时,对象即被释放。如果像下面这样将先前可能发生循环引用的类成员变量改成附有 _weak
修饰符的成员变量的话,该现象便可避免。
|
_weak
修饰符还有另一优点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil 被赋值的状态(空弱应用)。如以下代码所示。
|
此源代码执行结果如下:
|
像这样,使用 _weak
修饰符可避免循环引用,通过检查附有 _weak
修饰符的变量是否为nil,可以判断被赋值的对象是否己废弃。
规则
在 ARC 的环境下编译源代码,必须遵守一定的规则。下由就是具体的 ARC 的规则
- 不能使用 retain/release/retainCount/autorelease
- 不能使用 NSAllocateObject/NSDeallocateObject
- 须遵守内存管理的方法命名规则
- 不要显式调用 dealloc
- 使用 @autoreleasepool 块替代 NSAutoreleasePool
- 不能使用区域(NSZone)
- 对象型变量不能作为C语言结构体(struct/union)的成员
- 显式转换“id”和“void *”
不能使用 retain/release/retainCount/autorelease
内存管理是编译器的工作,因为没有必要使用内存管理的方法。
不能使用 NSAllocateObject/NSDeallocateObject
一般通过调用 NSObject 类的 alloc 类方法来生成并持有 Objective-C 对象。
|
但是就如 GNUstep 的 alloc 实现所示,实际上是通过直接调用 NSAllocateObject
函数来生成并持有对象的。
在 ARC 环境时,禁止使用 NSAllocateObject
函数。同 retain
等方法一样,如果使用便会引起编译错误。
|
同样地,也禁止使用用于释放对象的 NSDeallocateObject
函数。
须遵守内存管理的方法命名规则
在 MRC 环境时,用于对象生成/持有的方法必须遵守以下的命名规则。
- alloc
- new
- copy
- mutableCopy
以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在 ARC 环境时也一样,返回的对象完全没有改变。只是在 ARC 环境下要追加一条命名规则。
- init
以 init 开始的方法的规则要比 alloc/new/copy/mutableCopy
更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应为 id 类型或该方法声明类的对象类型,抑或是该类的超类型或子类型。该返回对象并不注册到 autoreleasepool
上。基本上只是对 alloc
方法返回值的对象进行初始化处理并返回该对象。
以下为使用该方法的源代码。
|
如此源代码所示,init 方法会初始化 alloc 方法返回的对象,然后原封不动地返还给调用方。下面我们来看看以init开始的方法的命名规则。
|
该方法声明遵守了命名规则,但下面这个方法虽然也以init开始,却没有返回对象,因此不能使用。
|
另外,下例虽然也是以 init 开始的方法但并不包含在上述命名规则里。请注意。
|
不要显式调用 dealloc
无论 ARC 还是 MRC,只要对象的所有者都不持有该对象,该对象就被废弃。对象被废弃时,都会调用对象的 dealloc 方法
|
在 MRC 环境下必须像下面这样调用 [super dealloc]
|
ARC 会自动对此进行处理,因此不必写 [super dealloc]。
属性
在 ARC 环境下,Objective-C 类的属性也会发生变化。
|
在 ARC 环境下,以下可作为这种属性声明中使用的属性来用。
属性声明的属性 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained 修饰符 |
copy | __strong修饰符(但是赋值的是被复制的对象) |
retain | __strong 修饰符 |
strong | __strong 修饰符 |
unsafe_unretained | __unsafe_unretained 修饰符 |
weak | __weak 修饰符 |
以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。只有 copy
属性不是简单的斌值,它赋值的是通过 NSCopying
接口的copyWithZone:
方法复制赋值源所生成的对象。
另外,在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误,比如下面这种情况。
|
在声明 id 型 obj 成员变量时,像下面这样,定义其属性声明为weak。
|
编译器出现如下错误。
|
此时,成员变量的声明中需要附加 __weak 修饰符。
|
或者使用 strong
属性来代替 weak
属性。
|
数组
以下是将附有 _strong
修饰符的变量作为静态数组使用的情况。
|
_weak
修饰符,_autoreleasing
修饰符以及 _unsafe_unretained
修饰符也与此相同。
|
_unsafe_unretained
修饰符以外的 _strong/_weak/ autorelcasing修饰符保证其指定的变量初始化为 nil。同样地,附有 _strong/_weak/_autoreleasing 修饰符变量的数组也保证其初始化为 nil。下面我们就来看看数组中使用附有 _strong 修饰符变量的例子:
|
数组超出其变量作用域时,数组中各个附有 _strong
修饰符的变量也随之失效,其强引用消失,所赋值的对象也随之释放。这与不使用数组的情形完全一样。
将附有 _strong 修饰符的变量作为动态数组来使用时又如何呢?在这种情况下,根据不同的目的选择使用 NSMutableArray、NSMutableDictionary、NSMutableSet 等 Foundation 框架的容器。这些容器会恰当地持有追加的对象并为我们管理这些对象。
像这样使用容器虽然更为合适,但在 C 语言的动态数组中也可以使用附有 _strong
修饰符的变量,只是必须要遵守一些事项。以下按顺序说明。
声明动态数组用指针。
|
如前所述,由于 “id“ 类型默认为 “id_autoreleasing“ 类型,所以有必要显式指定为 _strong
修饰符。另外,虽然保证了附有 _strong
修饰符的 id 型变量被初始化为 nil,但并不保证附有 _strong
修饰符的 id 指针型变量被初始化为 nil。
另外,使用类名时如下记述。
|
其次,使用 calloc
函数确保想分配的附有 _strong
修饰符变量的容量占有的内存块。
|
该源代码分配了 entries
个所需的内存块。由于使用附有 _strong
修饰符的变量前必须先将
其初始化为 nil,所以这里使用使分配区域初始化为0的 calloc
函数来分配内存。不使用 calloc
函
数,在用 malloc
函数分配内存后可用 memset
等函数将内存填充为0。
但是,像下面的源代码这样,将 nil 代入到 malloc 函数所分配的数组各元素中来初始化是非常危险的。
|
这是因为由于 malloc 函数分配的内存区域没有被初始化为0,因此 nil 会被赋值给附有 _strong
修饰符的并被赋值了随机地址的变量中,从而释放一个不存在的对象。在分配内存时推荐使用 calloc 函数。
像这样,通过 calloc 函数分配的动态数组就能完全像静态数组一样使用。
|
但是,在动态数组中操作附有 _strong
修饰符的变量与静态数组有很大差异,需要自己释放所有的元素。
如以下源代码所示,在只是简单地用 free
函数废弃了数组用内存块的情况下,数组各元素所赋值的对象不能再次释放,从而引起内存泄漏。
|
这是因为在静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理。如以下源代码所示,一定要将 nil 赋值给所有元素中,使得元素所赋值对象的强引用失效,从而释放那些对象。在此之后,使用 free
函数废弃内存块。
|
同初始化时的注意事项相反,即使用 memset 等函数将内存填充为0也不会释放所赋值的对象。这非常危险,只会引起内存泄漏。对于编译器,必须明确地使用赋值给附有 _strong
修饰符变量的源代码。所以请注意,必须将 nil 赋值给所有数组元素。
另外,使用 memcpy
函数拷贝数组元素以及 realloc
函数重新分配内存块也会有危险,由于数组元素所赋值的对象有可能被保留在内存中或是重复被废弃,所以这两个函数也禁止使用。
再者,我们也可以像使用 _strong
修饰符那样使用附有 _weak
修饰符变量的动态数组。在 _autoreleasing
修饰符的情况下,因为与设想的使用方法有差异,所以最好不要使用动态数组。由于_unsafe_unretained
修饰符在编译器的内存管理对象之外,所以它与 void* 类型一样,只能作为C语言的指针类型来使用。
本文是根据Objective-C高级编程第一章整理,作学习和参考之用
关于内存管理可以查看一下唐巧大神博客的讲解:
理解 iOS 的内存管理-唐巧