Effective Objective-C 2.0 读书笔记——内存管理(上)
Effective Objective-C 2.0 读书笔记——内存管理(上)
文章目录
- Effective Objective-C 2.0 读书笔记——内存管理(上)
- 引用计数
- 属性存取方法中的内存管理
- autorelease
- 保留环
- ARC
- ARC必须遵循的方法命名原则
- ARC 的自动优化:消除冗余的 autorelease 与 retain
- 变量的内存管理语义
- __strong(默认)
- __weak
- __unsafe_unretained
- __autoreleasing
- ARC清理实例变量
- 覆写内存管理方法
在Objective-C中,内存管理是程序开发中不可或缺的一部分,而自引用计数( ARC)是一种自动化的内存管理技术。本文是对自动引用计数的简单学习,特此进行记录
引用计数
引用计数是一种技术,用于管理对象的引用计数,即对象被引用的次数。当一个对象的引用计数大于0时,表示该对象被持有,不可被释放;当引用计数为0时,表示对象需要被释放。
自引用计数是一种技术,用于管理对象的引用计数,即对象被引用的次数。当一个对象的引用计数大于0时,表示该对象被持有,不可被释放;当引用计数为0时,表示对象需要被释放。
文章用一个办公室关灯的例子十分贴切:
- 第一个人进入办公室,“需要照明的人数” 加1。计数值从0 变成了1,因此要开灯。
- 之后每当有人进入办公室,“需要照明的人数” 就加1。如计数值从1变成2。
- 每当有人下班离开办公室,“需要照明的人数” 就减1。如计数值从2 变成1。
- 最后一个人下班离开办公室时,“需要照明的人数” 减1。计数值从1变成了0,因此要 关灯。
// 生成并持有对象
id obj = [[NSObject alloc] init];
// 持有对象
[obj retain];
// 释放对象
[obj release];
// 废弃对象
[obj dealloc];
属性存取方法中的内存管理
在手动计数,我们要使用以下方法对属性进行手动计数
- (void)setFoo:(id)foo {
if (_foo != foo) {
[foo retain];//对传入的新对象 foo 调用 retain,使其引用计数加 1
[_foo release];//释放当前实例变量 _foo 中原来存储的对象
_foo = foo;//传入的新对象 foo 赋值给实例变量 _foo
}
}
当执行完 _foo = foo;
后,_foo
和 foo
都指向同一个对象,因此对 _foo
进行 release
就相当于对该对象(也就是 foo
指向的对象)进行 release
。
autorelease
关于这个方法,书中给出了这个例子
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return str;
}
由于我们内存管理的原则——谁创建谁释放,在这个方法之中被创建的对象str
必须在stringValue
之中得到release
,但是很明显如果在return语句执行之前进行release
的话,则没有返回值。在return之后写release语句则根本不会执行,那么怎么办呢?我们可以将程序修改成以下形式
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease]; // 放入自动释放池中
}
它会在稍后释放对象,从而给调用者留下了足够长的时间 ,使其可以在需要时先保留返回值。实际上,释放操作会在清空最外层的自动释放池 (参见第 34 条)时 执行,除非你有自己的自动释放池 ,否则这个时机指的就是当前线程的下一次事件循环
保留环
保留环顾名思义——就是呈环状相互引用的多个对象,这将导致内存泄漏,因为循环中的对象其保留计数不会降为0。对于循环中的每个对象来说,至少还有另外 一个对象引用着它 。图里的每个对象都引用了 另外两个对象之中的一个。在这个循环里,所有对象的保留计数都是1。
在垃圾收集环境中,通常将这种情况认定为"孤岛" (island of isolation),通常我们使用弱引用解决这个问题
ARC
由于ARC 会自动执行retain
、release
、autorelease
等操作,所以直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:
• retain
• release
• autorelease
• dealloc
直接调用上述任何方法都会产生编译错误,因为ARC要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。
ARC必须遵循的方法命名原则
拥有(retained)返回值
如果方法名以 alloc
、new
、copy
或 mutableCopy
开头,ARC 默认认为该方法返回的对象是“拥有的”(即调用者获得一个 +1 的所有权),调用者负责在不需要时释放它。
示例:
EOCPerson *person = [[EOCPerson alloc] init];
// person 的 retain count +1,调用者需要 release
[person release];
如果方法名 不以 这些前缀开头(如 somePerson
),那么返回的对象会被 autorelease,调用者不需要手动 release,否则可能会导致程序崩溃(过度释放)。
2. 示例代码解析
(1) newPerson 方法
objc
复制编辑
+ (EOCPerson*) newPerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}
- 该方法的名字 以
new
开头,所以它返回的对象是 归调用者所有的。 alloc
使person
的 retain count +1,但调用者仍然负责在适当的时候释放person
。
调用方式:
EOCPerson *personOne = [EOCPerson newPerson];
// 由于 newPerson 返回的是 "owned" 对象,调用者需要释放:
[personOne release];
(2) somePerson 方法
+ (EOCPerson*) somePerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}
- 该方法的名字 没有以
new
、alloc
、copy
或mutableCopy
开头,所以它返回的对象 不归调用者所有。 - ARC 会自动在返回对象上调用
autorelease
,确保对象在方法返回后仍然有效,但在适当的时候自动释放。
在 MRC(手动引用计数) 下,它等价于:
return [person autorelease];
调用方式:
EOCPerson *personTwo = [EOCPerson somePerson];
// personTwo 被自动 autorelease,调用者不需要手动 release
- 如果手动调用
[personTwo release]
,可能会导致程序崩溃!
但是如果调用此类方法想要获取一个长时间持有的对象的话(例如用这类方法赋值给一个属性),我们还是需要对这个进行retain操作
EOCPerson *tmp = [EOCPerson personWithName: @"Bob Smith"];
_myperson = [tmp retain];
3. 调用代码分析
-(void) doSomething {
EOCPerson *personOne = [EOCPerson newPerson];
EOCPerson *personTwo = [EOCPerson somePerson];
}
在 doSomething
方法里:
personOne
通过newPerson
方法创建,归调用者所有,所以doSomething
结束时,ARC 需要释放它(如果是 MRC,调用者需要手动release
)。personTwo
通过somePerson
方法创建,它是 autorelease 对象,不需要手动释放,ARC 会自动管理它。
在 ARC 下,这段代码执行后:
personOne
在作用域结束后被 ARC 释放。personTwo
在合适的时间点自动释放。
ARC 的自动优化:消除冗余的 autorelease 与 retain
除了自动调用 retain 与 release 之外,ARC 还能进行一些手工难以实现的优化。书中举了这样一个例子:
假设有一个方法 personWithName:
,它内部是这样写的:
+ (EOCPerson*) personWithName:(NSString*) name {
EOCPerson *person = [[EOCPerson alloc] init];
person.name = name;
return objc_autoreleaseReturnValue(person);
}
关于objc_autoreleaseReturnValue(person)
这个函数,是一个用于优化的函数,其具体作用如下:
检测调用者是否马上会对返回的对象调用 retain:
在某些情形下(例如调用方在赋值时,因为属性是 strong,所以会自动执行一次 retain 操作),实际上调用者会对返回的对象立即执行 retain。这种情况下,原先的 autorelease 操作就显得“多余”了。
优化过程:
如果检测到调用者马上会执行 retain,那么 objc_autoreleaseReturnValue
会设置一个标志位,并不真正执行 autorelease;这样就避免了不必要的 autorelease 和随后的 retain 操作。
那么对于这个方法的调用者来说,我们在调用者侧的代码如下
EOCPerson *tmp = [EOCPerson personWithName:@"Mat Galloway"];
_myPerson = objc_retainAutoreleasedReturnValue(tmp);
这里 objc_retainAutoreleasedReturnValue
的作用与前面的函数相对应:
- 检测返回对象上是否设置了标志(表明前面已经发现“将被 retain”的情况),
- 如果标志已置位,则直接返回对象而不执行额外的 retain 操作;
- 否则,就调用普通的 retain。
对于这两个特殊的优化方法,书中给出他们的伪代码实现
id objc_autoreleaseReturnValue(id object) {
if (/* caller will retain object */) {
set_flag(object); // 标记此对象,表明将会被 retain
return object;
} else {
return [object autorelease];
}
}
id objc_retainAutoreleasedReturnValue(id object) {
if (get_flag(object)) {
clear_flag(object);
return object; // 已经标记,直接返回而不需要额外的 retain
} else {
return [object retain];
}
}
设置与检测一个标志位的操作通常比调用 autorelease 和 retain 更高效。ARC 利用这种技术可以使得内存管理的开销降低,从而提升程序整体性能。
变量的内存管理语义
ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。一定要理解这个问题,尤其要注意实例变量的语义,因为对于某些代码来说,其语义和手动管理引用计数时不同。例如,有下面这段代码:
@interface EOCClass : NSObject (
id _object;
}
@implementation EOCClass
- (void) setup (
_object = [EOCotherClass new];
}
@end
在手动管理引用计数时,实例变量_object 并不会自动保留其值,而在ARC环境下则会 这样做。也就是说,若在ARC 下编译setup 方法,则其代码会变为:
-(void) setup {
id tmp = [EOCOtherClass new];
_object = [tmp retain];
[tmp release];
}
如果不用ARC,那么需要像下面这样来写:
-(void) setobject: (id) object {
[_object release];
_object = [object retain];
}
在 ARC 下,Objective‑C 引入了几个修饰符来标识变量对对象的所有权,这些修饰符直接影响编译器如何管理变量所引用对象的内存。常见的修饰符包括:
__strong(默认)
-
语义:
对象变量默认是 __strong 的,也就是说,当一个对象被赋值给一个 __strong 变量时,该变量会持有对象,使得对象的引用计数增加。 -
作用:
保证只要变量存在,对象不会被销毁。当该变量离开作用域或被赋予新值时,原先引用的对象会自动释放(编译器会自动插入 release 操作)。 -
示例
:
NSObject *obj = [[NSObject alloc] init]; // obj 是 __strong 的,retain count 自动 +1 // 当 obj 离开作用域后,编译器会自动调用 release
__weak
-
语义:
声明为 __weak 的变量不会对所引用的对象进行所有权保持,即不会增加对象的引用计数。 -
作用:
主要用于打破循环引用(例如在 delegate 或块(block)中),当所引用的对象被释放时,__weak 变量会自动置为 nil,防止野指针问题。 -
示例:
NSURL *url = [NSURL URLWithString:@"http://www.example.com/"]; EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url]; __weak EOCNetworkFetcher *weakFetcher = fetcher; [fetcher startWithCompletion:^(BOOL success) { NSLog(@"Finished fetching from %@", weakFetcher.url); }];
__unsafe_unretained
-
语义:
与 __weak 类似,也不增加引用计数,但不同的是 __unsafe_unretained 变量不会在所引用对象被销毁时自动置为 nil,因此存在野指针风险。 -
作用:
主要用于兼容旧代码或在性能上要求极高且确信生命周期管理正确的场景中。 -
示例:
__unsafe_unretained NSObject *unsafeObj = someStrongObj; // 如果 someStrongObj 被释放,unsafeObj 不会自动置 nil,继续访问会导致崩溃
__autoreleasing
-
语义:
这种修饰符通常用于方法参数,表示传入的对象在方法返回时将被放入自动释放池。 -
作用:
用于处理输出参数,使得返回给调用者的对象不必立即释放,而是在当前自动释放池清空时被释放。 -
示例:
- (BOOL)error:(NSError * __autoreleasing *)error;
ARC清理实例变量
在使用MRC时,我们会在- (void)dealloc
之中手动释放所有被持有的实例变量
- (void)dealloc {
[_foo release];
[_bar release];
[super dealloc];
}
尽管 ARC 可以自动管理所有 Objective‑C 对象的内存,但对于非 Objective‑C 对象仍需要开发者手动清理。例如:
- Core Foundation 对象:这些对象不受 ARC 管理,需要在 dealloc 中调用 CFRelease。
- 由 malloc 分配的内存:这类内存同样需要手动调用 free 来释放。
在 ARC 环境下,如果你需要清理这些资源,你可以自己实现 dealloc 方法,但注意不要调用 [super dealloc],因为 ARC 会自动为你调用超类的 dealloc。示例代码可能如下:
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
// 不要调用 [super dealloc],ARC 会自动调用超类的 dealloc
}
覆写内存管理方法
在 MRC 下,有时我们会覆写 release 方法(例如在单例中为了防止对象被释放,将 release 改成空操作),但在 ARC 下我们不被允许重写或者是直接调用内存管理方法,因为我们前面有说到ARC会执行各项的相关优化,重写或者直接调用会产生问题。ARC 通过特殊函数(如 objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue)来优化那些成对出现的 autorelease 与 retain 操作,以减少不必要的调用和提升性能。