【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理

  1. 自动引用计数 (ARC,Automatic Reference Counting)

1.1 什么是自动引用计数?

ARC是指内存管理中对引用采用自动计数的技术。在LLVM编译器中设置ARC为有效状态,就无需再次键入retain或者是release代码。

1.2 内存管理/引用计数

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第1张图片
图1·照明设备动作对应于OC对象动作
【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第2张图片
图2·引用计数的内存管理

1.2.2 内存管理的思考方式

从上面的内容,我们可以得出这么一个结论:
1. 自己生成的对象,自己所持有。
2. 非自己生成的对象,自己也能持有。
3. 不再需要自己持有的对象时释放。
4. 非自己持有的对象无法释放。

除了以上四条提到的三个词:“生成”、“持有”、“释放”,还有另外一个词:“废弃”或者叫“销毁”。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第3张图片
图3·对象操作与OC方法的对应

这些有关OC内存管理的方法,实际上不包括在该语言中,而是包含在Cocoa框架中用于OS X、iOS 应用开发。Cocoa框架中 Foundation 框架类库的 NSObject 类担负内存管理的职责。OC内存管理中的alloc、retain、release、dealloc方法分别指代 NSObject 类的alloc类方法、retain实例方法、release实例方法和dealloc实例方法。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第4张图片
图4·Cocoa框架、Foundation框架和NSObject类的关系

自己生成的对象,自己所持有

使用以下名称开头的方法名意味着自己生成的对象只有自己持有:

  • alloc
  • new
  • copy
  • mutableCopy

copy -- 基于NSCopying协议实现协议的copyWithZone:方法生成并持有对象的副本。

mutableCopy -- 基于NSMutableCopying协议实现协议的mutableCopyWithZone:方法生成并持有对象的副本。

“自己”指的是“对象的使用环境”,但将之理解为编程人员“自身”也是没错的。

// [NSObject new]与[[NSObject alloc] init]是完全一致的。
id obj1 = [[NSObject alloc] init];
// or
id obj2 = [NSObject new];

非自己生成的对象,自己也能持有

本小节略有疑问,阅读原书查看更多内容。

不再需要自己持有的对象时释放

id obj = [[NSObject alloc] init];
[obj release];
// 指向对象的指针虽仍然保留在变量obj中,貌似能够访问,但对象已经被释放了,是决不可访问的,这时候的obj已经变成了*野指针*,为避免访问报错,可以将obj设置为空指针。
obj = nil;
// 因为给空指针发消息是不会报错的。

用alloc/new/copy/mutableCopy方法生成并持有的对象,或者用retain方法持有的对象,一旦不再需要,务必要用release方法进行释放。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第5张图片
图5·release和autorelease的区别

无法释放非自己持有的对象
即只有当你持有该对象时,你才有权利释放它,否则不要去执行释放操作;释放过一遍了,不要尝试再次释放。

1.2.3 alloc/retain/release/dealloc 实现

  • GNUstep的实现


    【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第6张图片
    图6·GNUstep中的实现机制
  • Apple的实现


    【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第7张图片
    图7·通过引用计数表追溯对象

1.2.5 autorelease

顾名思义,autorelease就是自动释放,这看上去很像ARC,但实际上它更类似于C语言中的自动变量(局部变量)的特性。自动变量的概念是程序执行时,若某自动变量超出其作用域,该自动变量将被自动废弃。autorelease会像C语言的自动变量那样来对待对象实例。当超出其作用域(相当于变量作用域)时,对象实例的release实例方法被调用。另外,同C语言的自动变量不同的是,编程人员可以设定变量的作用域。

autorelease的具体使用方法如下:

  • 生成并持有NSAutoreleasePool对象
  • 调用已分配对象的autorelease实例方法
  • 废弃NSAutoreleasePool对象
【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第8张图片
图8·NSAutoreleasePool对象的生命周期

NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。

值得注意的是,在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。因此,开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第9张图片
图9·NSRunLoop每次循环过程中 NSAutoreleasePool对象被生成或废弃

尽管如此,不是代表你就可以不用手动管理autorelease的调用了,在大量产生autorelease的对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。典型的例子就是读入大量图像的同时,改变其尺寸等等一些列的操作,不停的消耗内存,因此这时就需要我们手动创建NSAutoreleasePool对象,并适时废弃,来优化内存。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第10张图片
图10·释放地释放autorelease对象

另外,Cocoa框架中也有很多类方法用于返回autorelease的对象,比如NSMutableArray类的arrayWithCapacity类方法。

id array = [NSMutableArray arrayWithCapacity: 1];
// 等同于以下
id array = [[[NSMutableArray alloc] initWithCapacity: 1] autorelease];

1.2.6 autorelease 实现

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第11张图片
图11·autorelease的GNUstep实现源代码
【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第12张图片
图12·提高调用OC方法的速度

更多内容查看原书。

1.3 ARC 规则

1.3.1 概要

实际上“引用计数式内存管理”的本质部分在ARC中并没有改变。就像“自动引用计数”这个名词表示的那样,ARC只是自动地帮助我们处理“引用计数”的相关部分。

在编译单位上,可设置ARC有效或无效,这一点便能佐证上述结论。比如对每一个文件可选择使用或不使用ARC。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第13张图片
图13·同一程序中按文件单位可以选择ARC有效/无效

设置ARC有效的编译方法如下:

  • 使用clang(LLVM编译器)3.0或以上版本。
  • 指定编译器属性为“-fobjc-arc”

Xcode4.2默认设定为对所有的文件ARC有效。

1.3.2 内存管理的思考方式

1. 自己生成的对象,自己所持有。
2. 非自己生成的对象,自己也能持有。
3. 不再需要自己持有的对象时释放。
4. 非自己持有的对象无法释放。

思考方式与非ARC模式下的是一样的,只是在源代码的记述方法上稍有不同。首先要理解ARC中追加的所有权声明。

1.3.3 所有权修饰符

OC编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。

所谓对象类型就是指向NSObject这样的OC类的指针,例如“NSObject *”。id类型用于隐藏对象类型的类名部分,相当于C语言中常用的“void *”。

ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符。所有权修饰符一共有四种。

  • __strong 修饰符
  • __weak 修饰符
  • __unsafe_unretained 修饰符
  • __autoreleasing 修饰符

__strong 修饰符
__strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说,以下源代码中的id类型的变量obj,实际上被附加了所有权修饰符。

id obj = [[NSObject alloc] init];
// 上面的代码与下面是相同的
id __strong obj = [[NSObject alloc] init];

如同“strong”这个名词所示,__strong 修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

{
    // 自己生成并持有对象
    id __strong obj = [[NSObject alloc] init];
    // 因为变量obj为强引用,所以自己持有对象
}
// 此时变量obj超出其作用域,强引用失效,所以自动的释放了自己持有的对象。
// 对象的所有者不存在了,因此废弃该对象。

附有__strong修饰符的变量之间可以相互赋值。

id __strong obj0 = [[NSObject alloc] init];
id __strong obj1 = [[NSObject alloc] init];
id __strong obj2 = nil;

obj0 = obj1;
obj2 = obj0;

obj1 = nil;
obj0 = nil;
obj2 = nil;

上面的代码,三个变量所指向的2个对象都能够被正确的释放(详细解释参考原书)。可以看出,__strong修饰符的变量,不仅只在变量作用域中,在赋值上也能够正确地管理其对象的所有者。

也可以在方法参数上,使用附有__strong修饰符的变量。

Test.h

@interface Test : NSObject {
    id __strong obj_;
}
-(void)setObject:(id __strong)obj;
@end

Test.m

@implementation Test

- (instancetype)init
{
    self = [super init];
    return self;
}
- (void)setObject:(id)obj {
    obj_ = obj;
}

main.m

{
    id __strong test = [[Test alloc] init];
    [test setObject: [[NSObject alloc] init]];
}

下面来解释一下上面的代码执行结果。

  1. test变量,持有Test对象的强引用。
  2. Test对象的obj_成员,持有NSObject对象的强引用。
  3. 因为test变量超出作用域,强引用失效,所以自动释放Test对象。
  4. Test对象的所有者不存在,因此废弃该对象。
  5. 废弃Test对象的同时,Test对象的obj_成员也被废弃,NSObject对象的强引用失效,自动释放NSObject对象。
  6. NSObject对象的所有者不存在,因此废弃该对象。

因此,__strong修饰符,使得无需额外的工作便可使用于类成员变量和方法参数中。
另外,__strong修饰符同后面将要讲到的__weak修饰符和__autoreleasing修饰符一起,可以保证将附有这些修饰符的自动变量初始化为nil。

id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;
// 等同于如下:
id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;

总结:


【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第14张图片
图14·对__strong修饰符的总结

__weak 修饰符

__weak修饰符的引入是为了解决引用计数式内存管理中必然会发生的“循环引用”的问题。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第15张图片
图15·循环引用

我们来看一个循环引用的例子:

{
    id test0 = [[Test alloc] init];
    id test1 = [[Test alloc] init];
    [test0 setObject: test1];
    [test1 setObject: test0];
}
【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第16张图片
图16·类成员变量的循环引用

循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。

一个对象持有其自身时,也会发生循环引用。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第17张图片
图17·自引用

这时候就该__weak修饰符上场了,__weak修饰符与__strong修饰符相反,提供弱引用。弱引用不能持有对象实例。

看看这个修改后的实例:

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第18张图片
图18·__weak修饰符避免循环引用

__weak修饰符还有另一优点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态(空弱引用)。

id __weak obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    obj1 = obj0;
    NSLog(@"A: %@", obj1);
}
NSLog(@"B: %@", obj1);

// 执行结果如下:
A: 
B: (null)

__weak修饰符只能用于iOS5以上及OS X Lion以上版本,在iOS4及OS X Snow Leopard中,可以使用__unsafe_unretained修饰符来代替。

更多详细内容查看原书。

__unsafe_unretained 修饰符

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。尽管ARC式的内存管理是编译器的工作,但附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。这一点在使用时要注意。

id __unsafe_unretained obj = [[NSObject alloc] init];

编译器会给出警告,提示和使用__weak修饰符时类似。警告,自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。

id __unsafe_unretained obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    obj1 = obj0;
    NSLog(@"A: %@", obj1);
}
NSLog(@"B: %@", obj1);

// 执行结果如下:
A: 
B: 

运行到NSLog(@"B: %@", obj1);时,其实obj1所指向的对象已经被释放了,最后一行NSLog只是碰巧正常运行而已。虽然访问了已经被废弃的对象,但应用程序在个别运行状况下才会崩溃。所以,有可能程序在执行到那一行时,发生野指针错误,引发崩溃。野指针:变量地址指向了一块已经被废弃了的内存块,试图访问方法、变量,从而引发崩溃。

__unsafe_unretined的存在是为了解决在iOS4及OS X Snow Leopard中的应用程序下使用,避免循环引用的修饰符,当然,使用时要注意,在访问__unsafe_unretained修饰符变量时要确保对象存在,不然程序会崩溃。

小结:__weak和__unsafe_unretained的区别就在于,__weak属于ARC范畴,ARC自动管理由__weak修饰符修饰的变量,在指向的对象被废弃时,自动将变量置为nil,而__unsafe_unretained不属于ARC管理范畴,不会自动将变量置为nil,需要使用者自己判断。

__autoreleasing 修饰符

ARC有效时autorelease会如何?实际上,后面讲到的原则也会说明(参考1.3.3节),不能使用autorelease方法。另外,也不能使用NSAutoreleasePool类。这样一来,虽然autorelease无法直接使用,但实际上,ARC有效时autorelease功能是起作用的。

ARC无效时的写法

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[[NSObject alloc] init] autorelease];
[pool drain];

ARC有效时的写法

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

在ARC中,通过“@autoreleasepool块”来替代非ARC中的“NSAutoreleasePool类对象生成、持有以及废弃”这一范围。

可以这么理解,在ARC有效时,用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第19张图片
图19·@autoreleasepool 和 附有 __autoreleasing 修饰符的变量

但是要知道,显式地附加 __autoreleasing 修饰符同显式地附加 __strong 修饰符一样罕见。

我们通过实例来看看为什么非显式地使用 __autoreleasing 修饰符也可以。

取得非自己生成并持有的对象时,如同一下源代码,虽然可以使用alloc/new/copy/mutableCopy以外的方法来取得对象,但该对象已被注册到了 autoreleasepool。这同在ARC无效时取得调用了 autorelease 方法的对象是一样的。这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到 autoreleasepool。

另外,根据后面要讲到的遵守内存管理方法命名规则(参考1.3.4节),init方法返回值的对象不注册到 autoreleasepool。

@autoreleaspool {
    id __strong obj = [[NSMutableArray array];
}

来看看到底发生了什么:


【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第20张图片
图20·非自己生成并持有的对象在自动释放池中的生命周期

再来看看源代码的调用过程:

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第21张图片
图21·NSMutableArray类方法array的内部实现和原理

虽然__weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool的对象。

看下面的例子:

id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);

以下源代码与此相同:

id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

为什么在访问附有__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢?这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在。

id的指针或对象的指针在没有显式指定时会被附加上__autoreleasing修饰符,这部分重点内容较多,也较难以理解,查看原书。

注意,可借助强大的非公开函数_objc_autoreleasePoolPrint()方法来查看自动释放池的状态

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第22张图片
图22·借助_objc_autoreleasePoolPrint()方法查看自动释放池状态

专栏:__strong修饰符/__weak修饰符


【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第23张图片
图23·__strong修饰符/__weak修饰符

1.3.4 规则

在ARC有效的情况下编译源代码,必须遵守一定的规则。下面就是具体的ARC的规则。

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必须遵守内存管理的方法命名规则
  • 不能显式调用dealloc
  • 使用@autoreleasepool块替代NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体(struct/union)的成员
  • 显式转换“id”和“void *”

下面详细介绍各项。

不能使用retain/release/retainCount/autorelease
内存管理是编译器的工作,因此没有必要再使用内存管理的方法。这段话摘自苹果的官方说明:“设置ARC有效时,无需再次输入retain或者是release代码。”实际上,如果在ARC有效时,调用这些方法,编译器是会报错的,因此可以把这句话更准确的描述为:“设置ARC有效时,禁止再次键入retain或者是release代码。”

总之,只能在ARC无效且手动进行内存管理时使用retain/release/retainCount/autorelease方法。

不能使用NSAllocateObject/NSDeallocateObject

NSAllocateObject函数用于ARC无效时的NSObject类的alloc类方法,用于生成并持有对象的。同样的,也禁止使用用于释放对象的NSDeallocateObject函数。

须遵守内存管理的方法命名规则

如1.2.2节所示,在ARC无效时,用于对象生成/持有的方法必须遵守以下命名规则。

  • alloc
  • new
  • copy
  • mutableCopy
    以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC有效时也一样,返回的对象完全没有改变。只是在ARC有效时要追加一条命名规则。
  • init
    init开始的方法的规则要比 alloc/new/copy/mutableCopy 更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应为 id类型或该方法声明类的对象类型,抑或是该类的超类型或子类型。该返回对象并不注册到autoreleasepool上。基本上只是对alloc方法返回值的对象进行初始化处理并返回该对象。

其他拓展自定义的命名规则举例如下:

-(id)initWithObject:(id) obj;
-(id)initWithName:(NSString *)name age:(int)age;

不要显式调用dealloc
无论ARC是否有效,只要对象的所有者都不持有该对象,该对象就被废弃。对象被废弃时,不管ARC是否有效,都会调用对象的dealloc方法。

- (void) dealloc {
    /* 此处运行该对象被废弃时必须实现的代码 */
    ...
    [super dealloc];// 必须放在最后调用(ARC无效时才手动调用,ARC有效时不允许调用)
}

比如使用C语言库时,在该库内部分配缓存时,如以下所示,dealloc方法需要通过free来释放留出的内存。

- (void) dealloc {
    free(buffer_);
}

dealloc方法在大多数情况下还适用于删除已注册的代码或者观察者对象。

- (void) dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver: self];
}

另外一定注意一点,在ARC无效时,重写了dealloc方法后,[super dealloc];必须放在最后调用。

使用@autoreleasepool块替代NSAutoreleasePool:参考1.3.3节

不能使用区域(NSZone)
虽说ARC有效时,不能使用区域(NSZone)。正如前所述(参考1.2.3节),不管ARC是否有效,区域在现在的运行时系统(编译器宏OBJC2被设定的环境)中已单纯地被忽略。

对象型变量不能作为C语言结构体的成员

下面的代码会引发编译错误:

struct Data {
    NSMutableArray *array;
};
编译错误:error: ARC forbids Objective-C objcs in structs or unions
      NSMutableArray *array;

要把对象型变量加入到结构体成员中时,可强制转换为 void * 或是附加前面所述的 __unsafe_unretained 修饰符。

struct Data {
    NSMutableArray __unsafe_unretained *array;
};

如前所述,附有 __unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便有可能遭遇内存泄漏或程序崩溃,在使用时要多加注意。

显式转换 idvoid*
在ARC无效时,像以下代码这样将id变量强制转换void *变量并不会出问题。

/* ARC无效 */
id obj = [[NSObject alloc] init];
void *p = obj;
id o = p;
[o release];

但是在ARC有效时这便会引起编译错误,这种操作不被允许。

id型或对象型变量赋值给void *或者逆向赋值时都需要进行特定的转换。如果只想单纯地赋值,则可以使用“__bridge 转换”。

id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;

像这样,通过“__bridge 转换”,id和void *就能通过互相转换。
但是转换为 void * 的__bridge 转换,其安全性与赋值给 __unsafe_unretained 修饰符相近,甚至会更低。如果管理时不注意赋值对象的所有者,就会出现悬垂指针(野指针)而导致程序崩溃。

__bridge转换中还有另外两种转换,分别是“__bridge_retained 转换”和“__bridge_transfer 转换”。

id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;

__bridge_retained 转换可使要转换赋值的变量也持有所赋值的对象。
__bridge_transfer 转换提供与此相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随之释放。相当于是做了“转移”(乾坤大挪移 ̄□ ̄||)。

同__bridge_retained转换与retain类似,__bridge_transfer转换与release相似。在给id obj赋值时retain即相当于 __strong修饰符的变量。

如果使用以上两种转换,那么不使用id型或对象型变量也可以生成、持有以及释放对象。虽然可以这么做,但在ARC中并不推荐这种方法。

void *p = (__bridge_retained void *)[[Person alloc] initWithName:@"CJL" age:29];
NSLog(@"class=%@", [(__bridge id)p class]);
(void)(__bridge_transfer id)p;
// 打印Person的dealloc方法,发现方法被调用了

这些转换多数使用在OC对象与CoreFoundation对象之间的相关变换中。

专栏:OC对象与CoreFoundation对象


【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第24张图片
图24·OC对象与CoreFoundation对象

更多内容查看原书。

1.3.5 属性
当ARC有效时,OC类的属性也会发生变化。

@property(nonatomic, strong) NSString *name;

当ARC有效时,以下可作为这种属性声明中使用的属性来用。

【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理_第25张图片
图25·属性声明的属性与所有权修饰符的对应关系

1.3.6 数组
...

你可能感兴趣的:(【iOS】笔记·OC高级编程·iOS与OS X 多线程与内存管理)