iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)

iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-中篇(Blocks)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-下篇(GCD)

本章主要介绍从OS X Lion和iOS 5 引入的内存管理新功能---自动引用计数。
目录

  • 1.1 什么是自动引用计数
  • 1.2 内存管理/引用计数
    • 1.2.1 概要
    • 1.2.2 内存管理的思考方式
    • 1.2.3 alloc/retain/release/dealloc 实现
    • 1.2.4 苹果的实现
    • 1.2.5 autorelease
    • 1.2.6 autorelease 实现
    • 1.2.7 苹果的实现
  • 1.3 ARC规则
    • 1.3.1 概要
    • 1.3.2 内存管理的思考方式
    • 1.3.3 所有权修饰符
    • 1.3.4 规则
    • 1.3.5 属性
    • 1.3.6 数组
  • 1.4 ARC的实现
    • 1.4.1 __strong修饰符
    • 1.4.2 __weak修饰符
    • 1.4.3 __autoreleasing修饰符
    • 1.4.4 引用计数

1.1 什么是自动引用计数

顾名思义,自动引用计数(ARC,Automatic Reference Counting) 是指内存管理中对引用采取自动计数的技术。让编译器来进行内存管理。无需再次键入retain/release代码,可降低程序崩溃、内存泄露等风险,减少程序员工作量。编译器完全清楚目标对象,并能立刻释放那些不再使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。

1.2 内存管理/引用计数

1.2.1 概要

1.2.2 内存管理的思考方式

首先来学习引用计数式内存管理的思考方法:

  • 自己生成的对象,自己持有;
  • 非自己生成的对象,自己也能持有;
  • 不再需要自己持有的对象时,释放;
  • 非自己持有的对象,无法释放;

上文出现了“生成”、“持有”、“释放”三个词。还要加上“废弃”一词。各个次表示的OC方法如下:

对象操作 OC方法
生成并持有对象 alloc/new/copy/mutableCopy等
持有对象 retain
释放对象 release
废弃对象 dealloc

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

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

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

  • alloc
  • new
  • copy
  • mutableCopy

本文所说的自己将之理解为编程人员“自身”。下面写出了自己生成并持有对象的源代码,为生成并持有对象,我们使用alloc方法。

id obj = [[NSObject alloc] init]; //自己生成并持有对象

使用NSObject类的alloc类方法就能让自己生成并持有对象。指向生成并持有对象的还真被赋给变量obj。另外,使用如下new类方法也能生成并持有对象。[[NSObject alloc] init] 与 [NSObject new]是完全一致的。

id obj = [NSObject new]; //自己生成并持有对象

copy方法利用基于NSCopying方法约定,由各类实现的copyWithZone;方法生成并持有对象的副本。与copy方法类似,mutableCopy方法利用基于NSMutableCopying方法约定,由各类实现的mutableCopyWithZone:方法生成并持有对象的副本。两者的区别在于,copy方法生成不可变更的对象,而mutableCopy方法生成可变更的对象。这类似于NSArray类对象与NSMutableArray类对象的差异。用这些方法生成的对象,虽然是对象的副本,但同alloc、new方法一样,在“自己生成并持有对象”这点上没有改变。

另外,根据上述“使用以下名称开头的方法名”,下列名称也意味着自己生成并持有对象。

  • allocMyObject
  • newThatObject
  • copyThis
  • mutableCopyYourObject

但是对于以下名称,既使用alloc/new/copy/mutableCopy名称开头,并不属于同一类别的方法。

  • allocate
  • newer
  • copying
  • mutableCopyed
    这里用驼峰拼写法来命名。

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

用上述项目之外的方法取得的对象,既用alloc/new/copy/mutableCopy以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者,我们来使用alloc/new/copy/mutableCopy以外的方法看看,这里试用一下NSMutableArray类的array类方法。

id obj = [NSMutableArray array];//取得非自己生成并持有的对象:取得的对象存在,但自己不持有对象。

该源代码中,NSMutableArray类对象被赋给变量obj,但变量obj自己并不持有该对象,使用retain方法可以持有对象。

id obj = [NSMutableArray array];//取得非自己生成并持有的对象:取得的对象存在,但自己不持有对象。
[obj retain]; //自己持有对象

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

自己持有的对象,一旦不再需要,持有者有义务释放该对象。释放使用release方法。

id obj = [[NSObject alloc] init]; //自己生成并持有对象
[obj release];//释放对象:指向对象的职责仍然被保留在变量obj中,貌似能够访问,但对象一经释放绝对不可访问。

自己生成而非自己持有的对象,若用retain方法变为自己持有,也同样可以用release方法释放。

id obj = [NSMutableArray array]; //取得非自己生成并持有对象
[obj retain];//自己持有对象
[obj release];//释放对象,对象不可再被访问。

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

如果要用某个方法生成对象,并将其返回给该方法的调用方,那么它的源代码又是怎样的呢?

- (id)allocObject {
    id obj = [[NSObject alloc] init]; //自己生成并持有对象
    return obj;
}

如上例所示,原封不动的返回用alloc方法生成并持有的对象,就能让调用放也持有该对象。请注意allocObject这个名称是符合前文命名规则的。

id obj1= [obj0 allocObject];

allocObject名称符合前文的命名规则,因此它与用alloc方法生成并持有对象情况完全相同,所以使用allocObject方法也意味着“自己生成并持有对象”。
那么,调用[NSMutableArray array]方法使取得的对象存在,但自己不持有对象,又是如何实现的呢?根据上文命名规则,不能使用以alloc/new/copy/mutableCopy开头的方法名,因此要使用object这个方法名。

- (id)object {
    id obj = [[NSObject alloc] init]; //自己持有对象
    [obj autorelease];//取得的对象存在,但自己不持有对象
    return obj;
}

上例中,我们使用了autorelease方法。用该方法,可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,使对象在超出指定的生存范围时能够自动并正确的示范(调用release方法)。

id obj1= [obj0 object];

使用NSMutableArray类的array类方法等可以取得谁都不持有的对象,这些方法都是通过autorelease而实现的。此外,根据上文的命名规则,这些用来取得谁都不持有的对象的方法名不能以alloc/new/copy/mutableCopy开头,这点需要注意。

当然也能通过retain方法将调用autorelease方法取得的对象变为自己持有。

id obj1 = [obj0 object];//取得的对象存在,但自己不持有对象
[obj1 retain];// 自己持有对象

无法释放非自己持有的对象

对于用alloc/new/copy/mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。倘若在营业程序中释放了非自己所持有的对象就会造成崩溃。例如自己生成并持有对象后,在释放完不再需要的对象之后再次释放。
比如:释放之后再次释放已非自己持有的对象,应用程序崩溃!
原因:再度废弃已经废弃了的对象时崩溃,访问已经废弃的对象的崩溃
或者在“取得的对象存在,但自己不持有对象”时释放

id obj1 = [obj0 object];//取得的对象存在,但自己不持有对象
[obj1 release];// 释放了非自己持有的对象,这肯定会导致应用程序崩溃!

如这些例子所示,释放非自己持有的对象会造成程序崩溃。因此绝对不要去释放非自己持有的对象。
以上四项内容,就是“引用计数式内存管理”的思考方式。

1.2.3 alloc/retain/release/dealloc 实现

接下来,以OC内存管理使用的alloc/retain/release/dealloc方法为基础,通过实际操作来理解内存管理。
Foundation框架并没有公开,但是Foundation使用的Core Foundation框架的源代码,以及通过调用NSObject类进行内存管理部分的源代码是公开的。但是,没有NSObject类的源代码,就很难了解NSObject类的内部实现细节。为此,我们首先使用开源软件GNUstep来说明。GNUstep是Cocoa框架的互换框架,它的源代码虽不能说与苹果的Cocoa实现完全相同,但是从使用者角度来看,非常相似。我们来看看GNUstep源代码中NSObject类的alloc类方法。
NSObject类的alloc类方法

id obj = [NSObject alloc];

上述调用NSObject类的alloc类方法在NSObject.m源代码中的实现如下:

+ (id)alloc {
    return [self allocWithZone:NSDefaultMallocZone()];
}

+ (id) allocWithZone:(NSZone*)z  {
    return NSAllocateObject(self,0,z);
}

通过allocWithZone:类方法调用NSAllocateObject函数分配了对象。下面看看NSAllocateObject函数

struct obj_layout {
    NSUinteger retained;
}

inline id

NSAllocateObject (Class aClass,NSUinteger extraBytes,NSZone *zone) {
    int size = 计算容纳对象所需内存大小;
    id new = NSZoneMalloc(zone,size);
    memset(new,0,size);
    new = (id)&((struct obj_layout *)new)[1];
}

NSAllocateObject函数通过调用NSZongMalloc函数来分配存放对象所需的内存空间,之后将该内存空间置0,最好返回作为对象而使用的指针。

NSDefaultMallocZone、NSZoneMalloc 等名称中包含的NSZone是什么呢?它时为防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的,对象的大小分配内存,从而提高了内存管理的效率。
但是,现在的运行时系统之上简单忽略了区域的概念。运行时系统中的内存管理本身已极具效率,使用曲艺来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。

alloc类方法用struct_obj_layout中的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0后返回。
[缺图]

对象的引用计数可通过retainCount实例方法取得:

[obj retainCount];

由对象寻址到对象内存头部,从而访问其中的retained变量。
[缺图]

alloc/retain/release/dealloc 具体总结如下:

  • 在Objective-C的对象中存有引用计数这一整数值。
  • 调用alloc或是retain方法后,引用计数值+1。
  • 调用release后,引用计数值-1。
  • 引用计数值为0,调用dealloc方法废弃对象。

1.2.4 苹果的实现

在看了GNUstep中的内存管理和引用计数的实现后,我们来看看苹果的实现。
retainCount、retain、release等函数,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。
GNUstep将引用计数保存在对象占用内存块头部的变量中,而苹果的实现,则是保存在引用计数表的记录中。
通过内存块头部管理引用计数的好处如下:

  • 少量代码即可完成
  • 能够统一管理引用计数用内催款与对象用内存块

通过引用计数表管理引用计数的好处如下:

  • 对象用内存块的分配无需考虑内存块头部
  • 引用计数表各记录中存有内存地址,可从各个记录追溯到各对象的内存块

这里特别要说的是,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被损坏,就能够确认各内存块的位置。
另外,在利用工具检测内存泄露是,引用计数表的各记录也有助于检测各对象的持有者是否存在。

1.2.5 autorelease

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

  • 生成并持有NSAutoreleasePool对象。
  • 调用已分配对象的autorelease实例方法。
  • 废弃NSAutoreleasePool对象。

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

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

上述源代码中最后一行的“[pool drain]”等同于“[obj release]”;
在Cocoa框架中,相当于程序煮循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。因此,应用程序开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。
尽管如此,但在大量产生autorelease的对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。

for (int i = 0; i<10000; i++) {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  //  大量产生autorelease的对象
  [pool drain]; // autorelease的对象被遗弃release
}

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

id array = [NSMutableArray arrayWithCapacity:1];

等同于

id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

1.2.6 autorelease 实现

autorelease 实例方法的本质是调用NSAutoreleasePool对象的addObejct类方法。
如果调用NSObject类的Autorelease实例方法,该对象将被追加到正在使用的NSAutoreleasePool对象中的数组里。
以下为通过drain实例方法废弃正在使用的NSAutoreleasePool对象的过程

- (void)drain {
    [self dealloc];
}

- (void) dealloc {
    [self emptyPool];
    [array release];
}

- (void) emptyPool {
    for (id obj in array) {
        [obj release];
    }
}

虽然调用了好几个方法,但可以确定对于数组中的所有对象都调用了release实例方法。

1.2.7 苹果的实现

如果autorelease NSAutoreleasePool对象会如何?

NSAutoreleasePool *pool = [[NSAutorelease alloc] init];
[pool autorelease];

会发生异常。
通过在使用OC,也就是foundation框架时,无论调用哪一个对象的autorelease实例方法,实际上是调用的都是NSObject类的autorelease实例方法。但是对于NSAutoreleasePool类,autorelease实例方法已被该类重载,因此运行时就会出错。

1.3 ARC规则

1.3.1 概要

上一节主要是讲了OC的内存管理,本节讲述ARC所引起的变化。
实际上“引用计数式内存管理”的本质部分在ARC中并没有改变。就像“自动引用计数”这个名称表示的那样,ARC只是自动的帮助我们处理“引用计数”的相关部分。

统一程序中按文件单位可以选择ARC有效或无效。
设置ARC有效:指定编译器属性为“-fobjc-arc”(默认)

1.3.2 内存管理的思考方式

引用计数式内存管理的思考方式就是思考ARC所引起的变化。

  • 自己生成的对象,自己持有;
  • 非自己生成的对象,自己也能持有;
  • 不再需要自己持有的对象时,释放;
  • 非自己持有的对象,无法释放;

这一思考方式在ARC有效时也是可行的。只是在源代码的记述方法上稍有不同。到底有什么不同?首先要理解ARC中追加的所有权声明。

1.3.3 所有权修饰符

OC为了处理对象,可将变量类型定义为id类型或各种对象类型。
所谓对象类型就是指向NSObject这样的OC类的指针,例如“NSObject *”。id类型用于隐藏对象类型的类名部分,相当于C语言中常用的“void *”。
ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符,所有权修饰符一共有4种:

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

__strong 修饰符

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

id obj = [[NSObject alloc] init];

id和对象类型在没有明确指定所有权修饰符时,默认为__strong修饰符。上面的源代码与以下相同。

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

ARC无效时:

{
id obj = [[NSObject alloc] init];
[obj release];
}

为了释放生成并持有的对象,增加了调用release方法的代码。该源代码进行的动作同先前ARC有效时的动作完全一样。
如此源代码所示,附有__strong修饰符的变量obj在超出其变量作用域时,既在该变量被废弃时,会释放其被赋予的对象。
如“strong”这个名称所示,__strong修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的实效,引用的对象会随之释放。

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

此外,对象所有者和对象的生存周期是明确的。那么,在取得非自己生成并持有的对象时,又会如何呢?
在NSMutableArray类的array类方法的源代码中取得非自己生成并持有的对象,具体如下:

{
id obj = [NSMutableArrray array]; //取得非自己生成但是自己持有的对象
}
因为变量obj超出其作用域,强引用失效,所以自动的释放自己持有的对象。对象的所有者不存在,因此废弃该对象。

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

// obj0持有对象A的强引用
id __strong obj0 = [[NSObject alloc] init]; // 对象A
// obj1持有对象B的强引用
id __strong obj1 = [[NSObject alloc] init]; // 对象B
// obj2不持有任何对象
id __strong obj2 = nil; 
// obj0持有obj1赋值的对象B的强引用,因为obj0被赋值,所以原先持有的对对象A的强引用失效。对象A的所有者不存在,因此废弃对象A。此时,持有对象B的强引用的变量为obj0和obj1。
obj0 = obj1;

// obj2持有obj0赋值的对象B的强引用,持有对象B的强引用。此时,持有对象B的强引用的变量为obj0、obj1和obj2。
obj2 = obj0;

// 因为nil赋给了obj1,所以对对象B的强引用失效。此时持有对象B的强引用的变量为obj0和obj2。
obj1 = nil;
// 因为nil赋给了obj0和obj2,所以对对象B的强引用失效。对象B的所有者不存在,因此废弃对象B。
obj0 = nil;
obj2 = nil;

通过上面这些不难发现,__strong修饰符的变量,不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。
当然,即便是OC类成员变量,也可以在方法参数上,使用附有__strong修饰符的变量。

__strong修饰符同后面要将的__weak修饰符和_autorelease修饰符一起,可以保证将附有这些修饰符的自动变量初始化为nil。

@interface Test : NSobject
{
    id __strong obj_;
}

- (void)setObject:(id __strong)obj;
@end

@implement
- (id)int {
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj {
    obj_ = obj;
}
@end

下面试着使用该类。

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

该例中生成并持有对象的状态记录如下:

{
    // test持有Test对象的强引用。
    id __strong test = [[Test alloc] init];
    //Test对象的obj_成员,持有NSObject对象的强引用。
    [test setObject:[[NSObject allokc] init]];
}
因为test变量超出其作用域,强引用失效,所以自动释放Test对象。test对象所有者不存在,因此废弃该对象。
废弃Test对象的同时,Test对象的obj_成本也被废弃,NSObject对象的强引用失效,自动释放NSObject对象。NSobject对象的所有者不存在,因此废弃该对象。

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

id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;

以下源代码与上相同。

id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;

正如苹果所言,通过__strong修饰符,不必再次键入retain或者release,完美的满足了“引用计数式内存管理的思考方式:

  • 自己生成的对象,自己持有;
  • 非自己生成的对象,自己也能持有;
  • 不再需要自己持有的对象时,释放;
  • 非自己持有的对象,无法释放;

前两项“自己生成的对象,自己持有”和“非自己生成的对象,自己也能持有”只需通过对带__strong修饰符的变量赋值便可达成。通过废弃带__strong修饰符的变量(变量作用域结束或是成员变量所属对象废弃)或者对变量赋值,都可以做到“不再需要自己持有的对象时释放”,最后一项“非自己持有的对象无法释放”,由于不必再次键入release,所以原本就不会执行。者些都满足于引用计数式内存管理的思考方式。
因为id类型和对象类型的所有权修饰符默认为__strong修饰符,所以不需要写上“__strong”。使ARC有效减淡的编程遵循了OC内存管理的思考方式。

__weak修饰符

看起来好像通过__strong修饰符编译器就能够完美的进行内存管理。但是遗憾的是,仅通过__strong修饰符是不能解决有些重大问题的。
这里提到的重大问题就是引用计数式内存管理中必然会发生的“循环引用”的问题。
以下为循环引用:

{
    id test0 = [[Test alloc] init]; //对象A。test0持有Test对象A的强引用
    id test1 = [[Test alloc] init]; //对象B。test1持有Test对象B的强引用
    // Test对象A的obj_成员变量持有Test对象B的强引用。此时,持有Test对象B的强引用的变量为Test对象A的obj_和test1.
    [test0 setObject:test1]; 

    // Test对象B的obj_成员变量持有Test对象A的强引用。此时,持有Test对象A的强引用的变量为Test对象B的obj_和test0.
    [test0 setObject:test1]; 
}
因为test0变量超出其作用域,强引用失效,所以自动释放Test对象A。
因为test1变量超出其作用域,强引用失效,所以自动释放Test对象B。

此时,持有Test对象A的强引用的变量为Test对象B的obj_。
此时,持有Test对象B的强引用的变量为Test对象A的obj_。

发生内存泄漏!

循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在,
此代码的本意是赋予变量test0的对象A和赋予变量test1的对象B在超出其变量作用域时被释放,既在对象不被任何变量持有的状态下予以废弃。但是,循环引用使得对象不能被再次废弃。
像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)。

id test = [[Test alloc] init];
[test setObject:test];

怎么样才能避免循环引用呢?看到__strong修饰符就会意识到了,既然有strong,就应该有与之对象的weak,也就是说,使用__weak修饰符可以避免循环引用。
__weak修饰符与__strong修饰符相反,提供若引用。若引用不能持有对象实例。

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

变量obj上附加了__weak修饰符。实际上如果编译以上代码,编译器会发出警告。
此源代码将自己生成并持有的对象赋值给附有__weak修饰符的变量obj。既变量obj持有对持有对象的若引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会立即被释放。编译器对此会给出警告。如果像下面这样,将对象赋值给附有_strong修饰符的变量之后再赋值给附有__weak修饰符的变量,就不会发生警告了。

{
    id __strong obj0 = [[NSObject alloc] init]; //自己生成并持有对象
    id __weak obj1 = obj0; //obj1变量持有生成对象的弱引用
}

因为obj0编程超出其作用域,强引用失效,所以自动释放自己持有的对象。因为对象的所有者不存在,所以废弃该对象。

因为带__weak修饰符的变量(既若引用)不持有对象,所以在超出其变量作用域时,对象既被释放。
__weak修饰符还有另一有点。在持有某对象的若引用时,若该对象被废弃,则此若引用将自动失效且处于nil被赋值的状态(空弱引用)。

__unsafe_unretained修饰符

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

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

附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象立即被释放。
在iOS4的程序中,必须使用__unsafe_unretained来替代__weak修饰符,赋值给附有__unsafe_unretained修饰符变量的对象子啊通过该变量使用时,如果没有确保其确实存在,那么应用程序就会崩溃。

__autoreleasing修饰符

ARC有效时autorelease会如何呢?不能使用autorelease方法,另外,也不能使用NSAutoreleasePool类。这样以来,虽然autorelease无法直接使用,但实际上,ARC有效时autorelease功能是起作用的。
ARC无效时会像下面这样来使用

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

ARC有效时,该源代码也能写成下面这样

@autorelasepool {
  id __autorelaseing obj =  [[NSObject alloc] init];
}

指定“@autoreleasepool块”来替代“NSAutoreleasePool类对象生成、持有即废弃”这一范围。
另外,ARC有效时,要通过将对象赋值给附加了__autoreleasing修饰符的变量来替代调用autorelease方法。对象赋值给附有__autoreleasing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,即对象被注册到autoreleasepool。
也就是可以理解为,在ARC无效时,用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法。
但是,显式的附加__autoreleasing修饰符同显式的附加__strong修饰符一样罕见。
我们通过实例来看看为什么显式的使用__autoreleasing修饰符也可以。
取得非自己生成并持有的对象时,如同以下源代码,虽然可以使用alloc/new/copy/mutableCopy以外的方法来取得对象,但该对象已被注册到了autoreleasepool。这同在ARC无效时取得已调用了autorelease方法的对象一样的。这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool。
另外根据后面要讲到的遵守内存管理方法命名规则(参考1.3.4),init方法返回值的对象不注册到autoreleasepool。
我们再来看看该源代码中对象的所有情况:

@autoreleasepoop {
    id __strong obj = [NSMutableArray array];//取得非自己生成并持有的对象。因为变量obj为强引用,所以自己持有对象。并且该对象由编译器判断其方法名后,自动注册到autoreleasepool。
}
因为变量obj超出其作用域,强引用失效,所以自动释放自己持有的对象。
同时,随着@autoreleasepoop块的结束,注册到autoreleasepool中的所有对象被自动释放。因为对象的所有者不存在,所以废弃对象。

像这样,不使用__autoreleasing修饰符也能使对象注册到auyoreleasepool,以下为取得非自己生成并持有对象时被调用方法的源代码示例。

+ (id)array {
    return [[NSMutableArray alloc] init];
}

该源代码也没有使用__autoreleasing修饰符,可写成以下形式。

+ (id)array {
    id obj =  [[NSMutableArray alloc] init];
    return obj;
}

因为没有显式指定所有权修饰符,所以id obj同附有__strong修饰符的id __strong obj是完全一样的。由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器会自动将其注册到autoreleasepool。

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

id __weak obj1 = obj0;

以下源代码与此相同。

id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;

为什么在访问附有__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢?这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在。因此,在使用附有__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象。
最后一个可非显式的使用__autoreleasing修饰符的例子,同前面讲述的id obj和id __strong obj完全一样。那么id *obj又如何呢?可以由id __strong obj的例子类推出 id __strong *obj吗?其实,推出来的是id __autoreleasing *obj。同样的,对象的指针NSObject **obj便称为了NSObject *__autoreleasing *obj。
像这样,id的指针或对象的指针在没有显式指定时会被附加上__autoreleasing修饰符。
比如,为了得到详细的错误信息,经常会在方法的参数中传递NSError对象的指针,而不是函数返回值。使用该方法的源代码如下所示。

NSEoor *error = nil;
BOOL result = [obj performOperationWithError:&error];

该方法的声明为:

- (BOOL)performOperationWithError:(NSError **)error;

同前面讲述的一样,id的指针或对象的指针会默认附加上__autoreleasing修饰符,所以等同于以下源代码。

- (BOOL)performOperationWithError:(NSError * __autoreleasing*)error;

NSRunLiio等实现不论ARC有效还是无效,均能够随时释放注册到autoreleasepool中的对象。
[待补充]

1.3.4 规则

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

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

下面详细解释各项

不能使用retain/release/retainCount/autorelease

内存管理是编译器的工作,因此没有必要使用内存管理的方法(retain/release/retainCount/autorelease)。总之,只能在ARC无效且手动进行内存管理时使用retain/release/retainCount/autorelease。

不能使用NSAllocateObject/NSDeallocateObject

alloc方法,实际上是通过直接调用NSAllocateObject函数来生成并持有对象的。在ARC有效时,禁止使用NSAllocateObject/NSDeallocateObject函数,同retain等方法一样,如果使用便会引起编译错误。

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

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

  • allock
  • new
  • copy
  • mutableCopy

以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC有效时也一样,返回的对象完全没有改变。只是在ARC有效时要追加一条命名规则。

  • init
    以init开始的方法的规则要比alloc/new/copy/mutableCopy更严格。该方法必须是实例方法,并且必须要返回对象。该返回对象并不注册到autoreleasepool上。基本上只是对alloc方法返回值的对象进行初始化处理,然后原封不动的返还给调用方。

不要显式调用dealloc

无论ARC是否有效,只要对象的所有者都不持有该对象,该对象就被废弃。对象被废弃时,不管ARC是否有效,都会调用对象的dealloc方法。
例如使用C语言库,在该库内部分配缓存时,dealloc方法需要通过free来释放留出的内存。

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

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

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

另外,在ARC无效时必须调用 [super dealloc];。

- (void)dealloc {
    [super dealloc];
}

ARC有效时会遵循无法显式调用dealloc这一规则,如果调用会引起编译错误。

使用@autoreleasepool块替代NSAutoreleasePool

ARC有效时,使用@autoreleasepool块替代NSAutoreleasePool

不能使用区域(NSZone)

不管ARC是否有效,区域在现在的运行时系统中已单纯的被忽略。

对象型变量不能作为C语言结构体(struct/union)的成员

C语言的规约上没有方法来管理结构体成员的生存周期。因为ARC把内存管理的工作分配给编译器,所以编译器必须能够知道并管理对象的生存周期。例如C语言的自动变量(局部变量)可使用该变量的作用域管理对象。但是对于C语言的结构体成员来说,这在标准上就是不可实现的。
要把对象型变量加入到结构体成员中时,可强制转换为void *(见下一条规则)或是附加前面所述的__unsafe_unretained修饰符。

struct Data {
    NSMutableArray __unsafe_unretained *array;
}

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

显式转换“id”和“void *”。

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”转换。
这些转换多数使用在OC对象与Core Foundation对象之间的相互变换中。

OC对象与Core Foundation对象
Core Foundation对象主要使用在用C语言编写的Core Foundation框架中,并使用引用计数的对象。在ARC无效时,Core Foundation框架中的retain/release分别是CFRetain/CFRelease。
CF对象与OC对象的区别很小,不同之处在于是由哪一个框架(Foundation框架还是CF框架)所生成的。无论是由哪种框架生成的对象,一旦生成之后,便能在不同的框架中使用。Foundation框架的API生成并持有的对象可以用CF框架的API释放。当然,反过来也是可以的。
因为CF对象与OC对象没有区别,所以在ARC无效时,只用简单的C语言的转换也能实现互换。另外这种转换不需要使用额外的CPU资源,因此也被称为“免费桥”(Toll-Free Bridge)。

[待补充]

1.3.5 属性

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

@property (nonatomic,strong) NSString *name;

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

属性声明的属性 所有权修饰符
assign __unsafe_unretained修饰符
copy __strong修饰符(但是赋值的是被复制的对象)
retain __strong修饰符
strong __strong修饰符
__unsafe_unretained __unsafe_unretained修饰符
weak __weak修饰符

以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。只有copy属性不是简单的赋值,它赋值的是通过NSCopying接口的copyWithZone:方法复制赋值源所生成的对象。
另外,在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误。比如下面这种情况:

@interface TestObject()
{
    id obj;
}
@property (nonatomic,weak) id obj;
@end

此时,成员变量的声明中需要附加__weak修饰符。

@interface TestObject()
{
    id __weak obj;
}
@property (nonatomic,weak) id obj;
@end

或者使用strong属性替代weak属性

@property (nonatomic,strong) id obj;

1.3.6 数组

[待补充]
以下是将附有__strong修饰符的变量作为静态数组使用的情况。

id objs[10];

__weak 修饰符,__autoreleasing修饰符以及__unsafe_unretained修饰符也与此相同。

id __weak objs[10];

__unsafe_unretained修饰符以外的__strong/__weak_autoreleasing修饰符保证其指定的变量初始化为nil。同样的,附有__strong/__weak_autoreleasing修饰符变量的数组也保证其初始化为nil。下面我们就来看看数组中使用附有__strong修饰符变量的例子。

{
    id objs[2];
    objs[0] = [[NSObject alloc] init];
    objs[1] = [NSMutableArray array];
}

数组超出其变量作用域时,数组中各个附有__strong修饰符的变量也随之失效,其强引用消失,所赋值的对象也随之释放。这与不使用数组的情形完全一样。
将附有__strong修饰符的变量作为动态数组来使用时又如何呢?在这种情况下,根据不同的目的选择使用NSMutableArray、NSMutableDictionary、NSMutableSet等Foundation框架的容器。这些容器会恰当的持有追加的对象并为我们管理这些对象。

1.4 ARC的实现

苹果的官方说明中称,ARC是“由编译器进行内存管理”的,但实际上只有编译器是无法完全胜任的,在此基础上还需要OC运行时库的协助。ARC由以下工具、库来实现。

  • clang(LLVM编译器)3.0以上;
  • obj4 OC运行时库493.9以上;

如果按照苹果的官方说明,假设仅由编译器进行ARC式的内存管理,那么__weak修饰符也完全可以使用在iOS4中。在iOS4中,无论怎样静态链接用于ARC的库,也不能在对象废弃时将__weak变量初始化为nil(空弱引用)。
下文基于“实现”来研究一下ARC。

1.4.1 __strong修饰符

[待补充]
赋值给附有__strong修饰符的变量在实际的程序中到底是怎样运行的呢?

{
    id __strong obj = [[NSObject alloc] init];
}

该源代码实际上可转换为调用以下的函数。

/*编译器的模拟代码 */
id obj = pbjc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);

如源代码所示,2次调用objc_msgSend方法(alloc方法和init方法),变量作用域结束时通过objc_release释放对象。虽然ARC有效时不能使用release方法,但由此可知编译器自动插入了release。下面我们来看看使用alloc/new/copy/mutableCopy以外的方法会是什么情况。

{
id __strong obj = [NSMutableArray array];
}

虽然调用了我们熟知的NSMutableArray类的array类方法,但得到的结果却与之前稍有不同。

/*编译器的模拟代码 */
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj);

虽然最开始的array方法的调用以及最后变量作用域结束时的release与之前相同,但中间的objc_retainAutoreleasedReturnValue函数是什么呢?
objc_retainAutoreleasedReturnValue 函数主要用于最优化程序运行。顾名思义,它是用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool中对象的方法,或是函数的返回值。像该源代码这样,在调用alloc/new/copy/mutableCopy以外的方法,即NSMutableArray类的array类方法等调用之后,由编译器插入该函数。
这种objc_retainAutoreleasedReturnvalue函数是成对的,与之相对的函数是objc_autoreleaseReturnValue。它用于alloc/new/copy/mutableCopy方法以外的NSMutableArray类的array类方法等返回对象的实现上。下面我们看看NSMutableArray类通过编译器会进行怎样的转换。

+ (id)array {
    return [[NSMutableArray alloc] init];
}

以下为该源代码的转换,转换后的源代码使用了objc_autoreleaseReturnValue函数。

/* 编译器的模拟代码*/
+ (id)array {
    id obj = objc_msgSend(NSMutableArray,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    return objc_autoreleaseReturnValue(obj);
}

像该源代码这样,返回注册到autoreleasepool中对象的方法使用了objc_autoreleaseReturnValue函数返回注册到autoreleasepool中的对象。但是objc_autoreleaseReturnValue函数同objc_autorelease函数不同,一般不仅限于注册对象到autoreleasepool中。
objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnvalue()函数,那么就不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的调用方。objc_retainAutoreleasedReturnValue函数与objc_retain函数不同,它即便不注册到autoreleasepool中而返回对象,也能够正确的获取对象。通过objc_autoreleaseReturnValue函数和objc_retainAutoreleasedReturnValue函数的协作,可以不将对象注册到autoreleasepool中而直接传递,这一过程达到了最优化。

iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)_第1张图片
省略autoreleasepool注册.png

1.4.2 __weak修饰符

[待补充]

__weak修饰符提供的功能:

  • 若附有__weak修饰符的变量所引用的对象被废弃,则将nil赋值给该变量。
  • 使用附有__weak修饰符的变量,即是使用注册到autoreleasepool中的对象。

weak表与引用计数表(参考1.2.4节)相同,作为散列表被实现。如果使用weak表,将废弃对象的地址作为键值进行检索,就能告诉的获取对应的附有__weak修饰符的变量的地址。另外,由于一个对象可同时赋值给多个附有__weak修饰符的变量中,所以对于一个键值,可注册多个变量的地址。

1.4.3 __autoreleasing修饰符

将对象赋值给附有__autoreleasing修饰符的变量等同于ARC无效时调用对象的autorelease方法。我们通过以下源代码来看一下。

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

该源代码主要将NSObject类对象注册到autoreleasepoop中,可作如下变换:

/*编译器的模拟代码*/
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

这与苹果的autorelease实现中的说明(参考1.2.7节)完全相同。虽然与ARC有效和无效时,其在源代码上的表现有所不同,但autorelease的功能完全一样。
在alloc/new/copy/mutableCopy方法群之外的方法中使用被注册到autoreleasepool中的对象会如何呢?下面我们来看看NSMutableArray类的array类方法。

@autoreleasepool {
    id __autoreleasing obj = [NSMutableArray array];
}

这与前面的代码源代码有何不同呢?

/*编译器的模拟代码*/
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleaseReturnvalue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

虽然持有对象的方法从alloc方法变为objc_retainAutoreleaseReturnvalue函数,但注册autoreleasepool的方法没有改变,仍是objc_autorelease函数。

1.4.4 引用计数

实际上,前面只讲了引用计数式内存管理的思维方式,特地没有介绍引用计数值本身,因此在这里提供了获取引用计数值的函数

uintptr_t _objc_rootRetainCount(id obj);
{
    id __strong obj = [[NSObject alloc] init];
    NSLog(@"retain count = %d", _objc_rootRetainCount(obj));
}

该源代码中,对象仅通过变量obj的强引用被持有,所以为1。

retain count = 1

下面使用__weak修饰符。

{
    id __strong obj = [[NSObject alloc] init];
    id __weak o = obj;
    NSLog(@"retain count = %d", _objc_rootRetainCount(obj));
}

由于弱引用并不持有对象,所以赋值给附有__weak修饰符的变量中也必定不会改变引用计数数值。

retain count = 1

结果同预想一样,那么通过__autoreleasing修饰符向autoreleasepool注册又会如何呢?

@autoreleasepool {
    id __strong obj = [[NSObject alloc] init];
    id __autoreleasing o = obj;
    NSLog(@"retain count = %d",__objc_rootRetainCount(obj));
}

结果如下

retain count = 2;

对象被赋予__strong修饰符变量的强引用所持有,且被注册到autoreleasepool中,所以为2。
以下确认@autoreleasepool块结束时释放已注册的对象。

id __strong obj = [[NSObject alloc] init];
@autoreleasepool {
    id __autoreleasing o = obj;
    NSLog(@"retain count = %d",__objc_rootRetainCount(obj));
}
NSLog(@"retain count = %d",__objc_rootRetainCount(obj));

在@ autoreleasepool块之后也显示引用计数数值。

retain count = 2; in @autoreleasepool
retain count = 1;

如我们预期的一样,对象被释放。
以下在通过附有__weak修饰符的变量使用对象时,基于显示autoreleasepool状态的_objc_autoreleasePoolPrint函数来观察注册到autoreleasepool中的引用对象。

@autoreleasepool {
    id __strong obj = [[NSObject alloc] init];
    _objc_autoreleasePoolPrint();
    id __weak o = obj;
    NSLog(@"before using __weak: retain count = %d",__objc_rootRetainCount(obj));
    NSLog(@"class = %@",[o class]);
    NSLog(@"after using __weak: retain count = %d",__objc_rootRetainCount(obj));
    _objc_autoreleasePoolPrint();
}

结果如下:

before using __weak: retain count = 1
class = NSObject
after using __weak: retain count = 2

通过以上过程我们可以看出,不使用__autoreleasing修饰符,仅使用附有__weak声明的变量也能将引用对象注册到了autoreleasepool中。
虽然以上这些例子均使用了_objc_rootRetainCount函数,但实际上并不能够完全信任该函数取得的数值。对于已释放的对象以及不正确的对象地址,有时也返回“1”。另外,在多线程中使用对象的引用计数数值,因为存有竞态条件的问题,所以取得的数值不一定完全可信。
虽然在调试中_objc_rootRetainCount函数很有用,但最好在来哦家其所具有的问题的基础上来使用。

你可能感兴趣的:(iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数))