《Objective-C高级编程:iOS与OS X多线程和内存管理》是iOS开发中一本经典书籍,书中有关ARC、Block、GCD的梳理是iOS开发进阶路上必不可少的知识储备。笔者读完此书后为了加强理解,特以笔记记之。本文为开篇,围绕ARC谈起Objective-C中的内存管理。
鉴于本书翻译自日文原版且翻译偏向书面,笔者希望采用通俗的语言记录,文章结构略有调整。
本文首发于Rachal's blog。
内存管理
ARC(自动引用计数)是iOS5、macOS10.7引入的内存管理技术,为了循序渐进的方式了解这项技术,本书先从ARC无效的环境说起,也就是常指的MRC(手动引用计数)环境。
本书开篇没有直接提及引用计数的概念,而是以办公室开灯关灯的例子引出内存管理的思考方式。作者认为理解内存管理时把注意力落在“生成”、“持有”、“释放”等管理操作上更为客观。
内存管理的思考方式
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
这里的“自己”理解为编程人员自身。与“生成”、“持有”、“释放”操作并列的还有“废弃”,分别对应以下方法:
对象操作 | Objective-C方法 |
---|---|
生成并持有对象 | alloc /new /copy /mutableCopy 等方法 |
持有对象 | retain 方法 |
释放对象 | release 方法 |
废弃对象 | dealloc 方法 |
- 注意:以上方法包含在Cocoa框架中而非Objective-C语言中。
自己生成的对象,自己所持有
以下面名称开头的方法生成的对象为自己持有:
alloc
new
copy
mutableCopy
id obj1 = [[NSObject alloc] init];// 自己生成并持有
id obj2 = [NSObject new];// 自己生成并持有
另外,根据以上原则,下列方法也意味着自己生成并持有对象:
allocMyObject
newThatObject
copyThis
mutableCopyYourObject
id obj = [MyObject allocMyObject];
// 内部实现
+ (MyObject *)allocMyObject {
MyObject *obj = [[MyObject alloc] init];
return obj;
}
非自己生成的对象,自己也能持有
alloc
/new
/copy
/mutableCopy
以外方法取得对象,非自己生成,自己不持有对象。可以通过retain
方法为自己所持有。
id obj = [NSMutableArray array];// 取得对象,但自己不持有
[obj retain];// 自己持有对象
不再需要自己持有的对象时释放
自己持有的对象不再需要时,持有者有义务将其释放。释放使用release
方法。
id obj = [[NSObject alloc] init];// 自己生成并持有对象
[obj release];// 释放对象
用retain
方法持有对象,一旦不再需要,务必要用release
方法释放。
id obj = [NSMutableArray array];// 取得对象,但自己不持有
[obj retain];// 持有非自己生成对象
[obj release];// 释放对象
类似[NSMutableArray array]
方法取得的对象存在,但自己不持有对象,内部如何实现?以object
这个方法名为例:
- (id)object {
id obj = [[NSObject alloc] init];// 自己持有
[obj autorelease];// 适当时机自动释放
return obj;// 取得对象存在,但自己不持有
}
autorelease
提供这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放。
使用NSMutableArray
类的array
类方法等可以取得谁都不持有的对象,这些方法是通过autorelease
实现的。
非自己持有的对象无法释放
用alloc
/new
/copy
/mutableCopy
方法生成并持有的对象,或用retain
方法持有的对象,在不需要时要将其释放。倘若在应用程序中释放了非自己持有的对象会造成崩溃。
id obj = [[NSObject alloc] init];// 自己生成并持有对象
[obj release];// 释放对象
[obj release];// 重复释放对象,崩溃
id obj1 = [obj0 object];// 取得对象,但自己不持有
[obj1 release];// 释放非自己持有的对象,崩溃
alloc/retain/release/dealloc及其实现
Cocoa是macOS的系统框架,在iOS上被称为Cocoa Touch。Cocoa框架虽然没有公开,但是可以通过Cocoa框架的互换框架GNUstep来推测苹果的实现。
alloc
调用allocWithZone
,那么这里的参数类型NSZone
是什么?
它是为了防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化的管理,根据使用对象的目的、对象的大小分配内存,从而提高内存管理的效率。
现在运行时系统中的内存管理已经极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂等问题。
GNUstep的实现
GNUstep源码里alloc
类方法用obj_layout
结构体中的整数变量retained
来保存引用计数retainCount
,并将其写入对象内存头部。
执行alloc
后对象的实例方法retainCount
获得数值是1,retain
使变量retained
值+1,release
使变量retained
值-1。release
使tetained
变量大于0时-1,等于0时调用dealloc
实例方法,废弃对象。
具体总结如下:
- 在Objective-C的对象中存有引用计数这一整数值。
- 调用
alloc
或是retain
方法后,引用计数值+1。 - 调用
release
后,引用计数值-1。 - 引用计数值为0时,调用
dealloc
方法废弃对象。
苹果的实现
alloc
过程设置断点追踪调用的方法和函数:
+alloc
+allocWithZone:
class_createInstance
calloc//分配内存块
苹果对alloc
的实现与GNUstep并无多大差异。
retainCount
/retain
/release
调用的方法和函数分别如下:
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CEBasicHashRemoveValue返回0时,-release 调用dealloc)
可以从__CFDoExternRefOperation
函数以及一些CFBasicHash
开头的函数名看出,苹果的实现大概就是采用散列表(又称哈希表)来管理引用计数。
在引用计数表中,key
为内存块地址,value
为对应的引用计数,苹果这样实现的优势在于:
- 为对象分配内存块时无需考虑内存块头部。
- 对象占用内存块损坏时,可以根据引用计数表来确认内存块的位置。
- 检测内存泄露时,根据引用计数表中的记录检查对象的持有者是否存在。
autorelease及其实现
autorelease
会像C语言的自动变量那样对待对象实例。当超出其作用域时,对象实例的release
实例方法被调用。
autorelease
具体使用方法如下:
- 1.生成并持有
NSAutoreleasePool
对象; - 2.调用已分配对象的
autorelease
实例方法; - 3.废弃
NSAutoreleasePool
对象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj release];
[pool drain];
Cocoa框架中程序主循环的NSRunLoop
对NSAutoreleasePool
对象进行生成、持有和废弃处理。在大量产生autorelease
对象时,若不废弃NSAutoreleasePool
对象,那么生成的对象就不能被废弃,会产生内存不足现象。
GNUstep的实现
autorelease
实例方法的本质就是调用NSAutoreleasePool
对象的addObject
类方法。
[obj autorelease];
源码:
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
GNUstep在实现NSAutoreleasePool
时使用连接列表,可以理解为数组。若调用NSObject
类的autorelease
方法,该对象就会被追加到正在使用的NSAutoreleasePool
对象的数组中。drain
实例方法废弃正在使用的NSAutoreleasePool
对象,会对数组中的所有对象调用release
方法。
苹果的实现
autoreleasepool以数组的形式实现,主要通过以下3个函数:
obj_autoreleasePoolPush()
obj_autorelease(obj)
obj_autoreleasePoolPop(pool)
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/* 等同于objc_autoreleasePoolPush */
id obj = [[NSObject alloc] init];
[obj autorelease];
/* 等同于 objc_autorelease(obj) */
[pool drain];
/* 等同于 objc_autoreleasePoolPop(pool) */
以上是MRC环境下的内存管理及实现。
ARC
ARC概述
ARC(Auto Reference Counting)是iOS5、macOS10.7(OS X Lion)引入的内存管理技术。
ARC的出现解决了原来需要手动键入
retain
或release
操作的问题。这在降低程序崩溃、内存风险的同时,很大程度上减少了开发程序的工作量。
内存管理的思考方式
“引用计数式内存管理”的本质在ARC中并没有改变,ARC只是自动地帮我们处理“引用计数”的相关部分。
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
所有权修饰符
ARC环境下其类型必须附加所有权修饰符(有省略的情况),所有权修饰符有以下4种:
- __strong
- __weak
- __unsafe_unretained
- __autorelease
书中此处提到id
类型做一下记录:
Objective-C中为了处理对象,可将变量定义为
id
类型,id
类型用于隐藏对象类型的类名部分,相当于C语言中常用到的void *
。
__strong修饰符
id
和对象类型默认使用__strong
修饰,由于是默认情况,可省略不写。
__strong
表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃。
__strong
同__weak
、__autoreleasing
一样,可以保证被修饰的变量在初始化时为nil
。
id obj = [[NSObject alloc] init];
//等同于
//id __strong obj = [[NSObject alloc] init];
__weak修饰符
循环引用容易引起内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。使用
__weak
修饰符可以避免循环引用。
__weak
表示弱引用,弱引用不能持有对象实例。
id __weak obj = [[NSObject alloc] init];//编译器会警告
__weak
修饰符还有另一个优点。在持有对象的弱引用时,若对象被废弃,则此弱引用将失效且处于nil
被赋值的状态。
通过检查__weak
修饰的变量是否为nil
可以判断被赋值的对象是否已废弃。
__weak
只能用于iOS5和macOS10.7以上版本,在iOS4和macOS10.6及以前用__unsafe_unretained
代替。
__autoreleasing修饰符
ARC下指定@autoreleasepool
块来替代NSAutoreleasePool
类生成、持有及废弃这一范围。_autoreleasing
修饰变量等价于对象调用autorelease
方法,即可将对象注册到autoreleasepool中。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
- 提问:前文提到
__weak
修饰的变量必须注册到autoreleasepool中,为什么? - 答:因为
__weak
修饰的变量只能持有对象的弱引用,在访问对象的过程中,该对象可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool
块结束之前能确保该对象存在。
_autoreleasing
同__strong
一样,显式使用罕见。
ARC的规则
ARC环境下编译源代码遵循一定规则:
- 不能使用
retain
/release
/retainCount
/autorelease
- 不能使用
NSAllocateObject
/NSDeallocateObject
ARC有效时,以上方法会导致编译器报错。
- 必须遵守内存管理的方法命名规则
对象的生成、持有的方法必须遵循命名规则:alloc
/new
/copy
/mutableCopy
。以init
开头的方法更严格:必须是实例方法且必须返回对象,返回对象的类型必须是id
类型或该方法声明类的对象类型。
- 不要显式调用
dealloc
dealloc
方法无需显式调用,但C语言库需要在dealloc
中free
,以及删除已注册的通知观察者。
- 使用
@autorelease
块代替NSAutoreleasePool
ARC有效时,使用@autoreleasepool
块代替NSAutoreleasePool
。
- 不能使用区域(
NSZone
)
不管ARC是否有效,区域在现在运行时系统中已单纯地被忽略。
- 对象型变量不能作为C语言结构体(
struct
/union
)的成员
C语言的规约上没有方法来管理结构体成员变量的生存周期。
- 显式转换“
id
”和“void *
”
/* ARC无效 */
id obj = [[NSObject alloc] init];
void *p = obj
ARC有效时需要通过__bridge
来显式转换:
/* ARC有效 */
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;
id o = (__bridge id)p;
属性和数组
- 声明属性所用的关键词与所有权修饰符的对应关系:
声明属性的关键词 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained |
copy | __strong |
retain | __strong |
strong | __strong |
unsafe_unretained | __unsafe_unretained |
weak | __weak |
- 动态数组中操作
__strong
修饰的变量与静态数组有很大差异,需要自己释放所有元素。静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理。
ARC的实现
__strong的实现
- 自己生成并持有
{
id __strong obj = [[NSObject alloc] init];
}
/* 编译器的模拟代码 */
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
- 非自己生成持有
id __strong obj = [NSMutableArray array];
/* 编译器的模拟代码 */
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
objc_retainAutoreleasedReturnValue
函数用于持有对象,注册到autoreleasepool中并返回。与之对应的函数是objc_autoreleaseReturnValue
。
+ (id)array {
return [[NSMutableArray alloc] init];
}
/* 编译器的模拟代码 */
+ (id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
通过
objc_retainAutoreleasedReturnValue
函数和objc_autoreleaseReturnValue
函数的协作,可以不将对象注册到autoreleasepool中而直接传递,以达到最优化程序运行。
__weak的实现
使用
__weak
修饰的变量,就是使用注册到autoreleasepool中的对象。
{
id __weak obj1 = obj;
}
/* 编译器模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
objc_destroyWeak(obj1);
__weak
同引用计数一样通过散列表(哈希表)实现,大致流程如下:
- 1.
objc_initWeak(&obj1, obj)
函数初始化__weak
修饰的变量,通过执行objc_storeWeak(&obj1, obj)
函数,以第一个参数(变量的地址)作为key
,把第二个参数(赋值对象)作为value
存入哈希表。 - 2.由于弱引用不能持有对象,函数
objc_loadWeakRetained(&obj1)
取出所引用的对象并retain
。 - 3.
objc_autorelease(tmp)
函数将对象注册到autoreleasepool
中。 - 4.
objc_destroyWeak(&obj1)
函数释放__weak
修饰的变量,通过过程执行objc_store(&obj1, 0)
函数,在weak表中查到变量地址并删除。废弃对象调用objc_clear_deallocating
函数,这个过程会将weak表记录中__weak
修饰的变量地址赋值为nil
。
如果大量使用
__weak
修饰的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用__weak
修饰符。
__autoreleasing的实现
_autoreleasing
修饰变量,等同于ARC无效时对象调用autorelease
方法。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelese(obj);
objc_autoreleasePoolPop();
以上为ARC篇的学习内容。