iOS - 老生常谈内存管理(三):ARC 面世

2020-04-24 15:58:44 浏览数 (1)

前言

ARC全称Automatic Reference Counting,自动引用计数内存管理,是苹果在 iOS 5、OS X Lion 引入的新的内存管理技术。ARC是一种编译器功能,它通过LLVM编译器和Runtime协作来进行自动管理内存。LLVM编译器会在编译时在合适的地方为 OC 对象插入retainreleaseautorelease代码来自动管理对象的内存,省去了在MRC手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量,让开发者可以专注于应用程序的代码、对象图以及对象间的关系上。   本文通过讲解MRCARC的转变、ARC规则以及使用注意,来帮助大家掌握iOS的内存管理。   下图是苹果官方文档给出的从MRCARC的转变。

摘要

ARC的工作原理是在编译时添加相关代码,以确保对象能够在必要时存活,但不会一直存活。从概念上讲,它通过为你添加适当的内存管理方法调用来遵循与MRC相同的内存管理规则。

为了让编译器生成正确的代码,ARC限制了一些方法的使用以及你使用桥接(toll-free bridging)的方式,请参阅《Managing Toll-Free Bridging》章节。ARC还为对象引用和属性声明引入了新的生命周期修饰符。

ARCXcode 4.2 for OS X v10.6 and v10.7 (64-bit applications)以及iOS 4 and iOS 5应用程序中提供支持。但OS X v10.6 and iOS 4不支持weak弱引用。

Xcode 提供了一个迁移工具,可以自动将MRC代码转换为ARC代码(如删除retainrelease调用),而不用重新再创建一个项目(选择 Edit > Convert > To Objective-C ARC)。迁移工具会将项目中的所有文件转换为使用ARC的模式。如果对于某些文件使用MRC更方便的话,你可以选择仅在部分文件中使用ARC

ARC 概述

ARC会分析对象的生存期需求,并在编译时自动插入适当的内存管理方法调用的代码,而不需要你记住何时使用retainreleaseautorelease方法。编译器还会为你生成合适的dealloc方法。一般来说,如果你使用ARC,那么只有在需要与使用MRC的代码进行交互操作时,传统的 Cocoa 命名约定才显得重要。

Person 类的完整且正确的实现可能如下所示:

代码语言:javascript复制
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@property NSNumber *yearOfBirth;
@property Person *spouse;
@end
 
@implementation Person
@end

(默认情况下,对象属性是strong。关于strong请参阅《所有权修饰符》章节。)

使用ARC,你可以这样实现 contrived 方法,如下所示:

代码语言:javascript复制
- (void)contrived {
    Person *aPerson = [[Person alloc] init];
    [aPerson setFirstName:@"William"];
    [aPerson setLastName:@"Dudney"];
    [aPerson setYearOfBirth:[[NSNumber alloc] initWithInteger:2011]];
    NSLog(@"aPerson: %@", aPerson);
}

ARC会负责内存管理,因此 Person 和 NSNumber 对象都不会泄露。

你还可以这样安全地实现 Person 的 takeLastNameFrom: 方法,如下所示:

代码语言:javascript复制
- (void)takeLastNameFrom:(Person *)person {
    NSString *oldLastname = [self lastName];
    [self setLastName:[person lastName]];
    NSLog(@"Lastname changed from %@ to %@", oldLastname, [self lastName]);
}

ARC会确保在 NSLog 语句之前不释放 oldLastName 对象。

ARC 实施新规则

ARC引入了一些在使用其他编译器模式时不存在的新规则。这些规则旨在提供完全可靠的内存管理模型。有时候,它们直接地带来了最好的实践体验,也有时候它们简化了代码,甚至在你丝毫没有关注内存管理问题的时候帮你解决了问题。在ARC下必须遵守以下规则,如果违反这些规则,就会编译错误。

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

ARC下,禁止开发者手动调用这些方法,也禁止使用@selector(retain)@selector(release)等,否则编译不通过。但你仍然可以对 Core Foundation 对象使用CFRetainCFRelease等相关函数(请参阅《Managing Toll-Free Bridging》章节)。

不能使用 NSAllocateObject / NSDeallocateObject

ARC下,禁止开发者手动调用这些函数,否则编译不通过。 你可以使用alloc创建对象,而Runtime会负责dealloc对象。

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

MRC下,通过 alloc / new / copy / mutableCopy 方法创建对象会直接持有对象,我们定义一个 “创建并持有对象” 的方法也必须以 alloc / new / copy / mutableCopy 开头命名,并且必须返回给调用方所应当持有的对象。如果在ARC下需要与使用MRC的代码进行交互,则也应该遵守这些规则。

为了允许与MRC代码进行交互操作,ARC对方法命名施加了约束: 访问器方法的方法名不能以new开头。这意味着你不能声明一个名称以new开头的属性,除非你指定一个不同的getterName

代码语言:javascript复制
// Won't work:
@property NSString *newTitle;
 
// Works:
@property (getter = theNewTitle) NSString *newTitle;
不能显式调用 dealloc

无论在MRC还是ARC下,当对象引用计数为 0,系统就会自动调用dealloc方法。大多数情况下,我们会在dealloc方法中移除通知或观察者对象等。

MRC下,我们可以手动调用dealloc。但在ARC下,这是禁止的,否则编译不通过。

MRC下,我们实现dealloc,必须在实现末尾调用[super dealloc]

代码语言:javascript复制
// MRC
- (void)dealloc
{
    // 其他处理
    [super dealloc];
}

而在ARC下,ARC会自动对此处理,因此我们不必也禁止写[super dealloc],否则编译错误。

代码语言:javascript复制
// ARC
- (void)dealloc
{
    // 其他处理
    [super dealloc]; // 编译错误:ARC forbids explicit message send of 'dealloc'
}
使用 @autoreleasepool 块替代 NSAutoreleasePool

ARC下,自动释放池应使用@autoreleasepool,禁止使用NSAutoreleasePool,否则编译错误。

代码语言:javascript复制
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
代码语言:javascript复制
error:'NSAutoreleasePool' is unavailable: not available in automatic reference counting mode

关于@autoreleasepool的原理,可以参阅《iOS - 聊聊 autorelease 和 @autoreleasepool》。

不能使用区域(NSZone)

对于现在的运行时系统(编译器宏 __ OBJC2 __ 被设定的环境),不管是MRC还是ARC下,区域(NSZone)都已单纯地被忽略。

NSZone 《Objective-C 高级编程:iOS 与 OS X 多线程和内存管理》书中对 NSZone 做了如下介绍: NSZone 是为防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。 但是,现在的运行时系统已经忽略了区域的概念。运行时系统中的内存管理本身已极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。 下图是使用多重区域防止内存碎片化的例子:

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

C 语言的结构体(struct / union)成员中,如果存在 Objective-C 对象型变量,便会引起编译错误。

备注: 我使用的时候编译通过,难道编译器优化了吗?

代码语言:javascript复制
struct Data {
    NSMutableArray *mArray;
};
代码语言:javascript复制
error:ARC forbids Objective-C objs in struct or unions NSMutableArray *mArray;

虽然是 LLVM 编译器 3.0,但不论怎样,C 语言的规约上没有方法来管理结构体成员的生存周期。因为ARC把内存管理的工作分配给编译器,所以编译器必须能够知道并管理对象的生存周期。例如 C 语言的自动变量(局部变量)可使用该变量的作用域管理对象。但是对于 C 语言的结构体成员来说,这在标准上就是不可实现的。因此,必须要在结构体释放之前将结构体中的对象类型的成员释放掉,但是编译器并不能可靠地做到这一点,所以对象型变量不能作为 C 语言结构体的成员。

这个问题有以下三种解决方案:

  • ① 使用 Objective-C 对象替代结构体。这是最好的解决方案。

如果你还是坚持使用结构体,并把对象型变量加入到结构体成员中,可以使用以下两种方案:

  • ② 将 Objective-C 对象通过Toll-Free Bridging强制转换为void *类型,请参阅《Managing Toll-Free Bridging》章节。
  • ③ 对 Objective-C 对象附加__unsafe_unretained修饰符。
代码语言:javascript复制
struct Data {
    NSMutableArray __unsafe_unretained *mArray;
};

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

代码语言:javascript复制
struct x { NSString * __unsafe_unretained S; int X; }

__unsafe_unretained指针在对象被销毁后是不安全的,但它对诸如字符串常量之类的从一开始就确定永久存活的对象非常有用。

显式转换 “id” 和 “void *” —— 桥接

MRC下,我们可以直接在 idvoid * 变量之间进行强制转换。

代码语言:javascript复制
    id obj = [[NSObject alloc] init];
    void *p = obj;
    id o = p;
    [o release];

但在ARC下,这样会引起编译报错:在Objective-C指针类型idC指针类型void *之间进行转换需要使用Toll-Free Bridging,请参阅《Managing Toll-Free Bridging》章节。

代码语言:javascript复制
    id obj = [[NSObject alloc] init];
    void *p = obj; // error:Implicit conversion of Objective-C pointer type 'id' to C pointer type 'void *' requires a bridged cast
    id o = p;      // error:Implicit conversion of C pointer type 'void *' to Objective-C pointer type 'id' requires a bridged cast
    [o release];   // error:'release' is unavailable: not available in automatic reference counting mode

所有权修饰符

ARC为对象引入了几个新的生命周期修饰符(我们称为 “所有权修饰符”)以及弱引用功能。弱引用weak不会延长它指向的对象的生命周期,并且该对象没有强引用(即dealloc)时自动置为nil。 你应该利用这些修饰符来管理程序中的对象图。特别是,ARC不能防止强引用循环(以前称为Retain Cycles,请参阅《从 MRC 说起 —— 使用弱引用来避免 Retain Cycles》章节)。明智地使用弱引用weak将有助于确保你不会创建循环引用。

属性关键字

ARC中引入了新的属性关键字strongweak,如下所示:

代码语言:javascript复制
// 以下声明同:@property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;
 
// 以下声明类似于:@property(assign)MyClass *myObject;
// 不同的是,如果 MyClass 实例被释放,属性值赋值为 nil,而不像 assign 一样产生悬垂指针。
@property(weak) MyClass *myObject;

strongweak属性关键字分别对应__strong__weak所有权修饰符。在ARC下,strong是对象类型的属性的默认关键字。

ARC中,对象类型的变量都附有所有权修饰符,总共有以下 4 种。

代码语言:javascript复制
__strong
__weak
__unsafe_unretained
__autoreleasing
  • __strong是默认修饰符。只要有强指针指向对象,对象就会保持存活。
  • __weak指定一个不使引用对象保持存活的引用。当一个对象没有强引用时,弱引用weak会自动置为nil
  • __unsafe_unretained指定一个不使引用对象保持存活的引用,当一个对象没有强引用时,它不会置为nil。如果它引用的对象被销毁,就会产生悬垂指针。
  • __autoreleasing用于表示通过引用(id *)传入,并在返回时(autorelease)自动释放的参数。

在对象变量的声明中使用所有权修饰符时,正确的格式为:

代码语言:javascript复制
    ClassName * qualifier variableName;

例如:

代码语言:javascript复制
    MyClass * __weak myWeakReference;
    MyClass * __unsafe_unretained myUnsafeReference;

其它格式在技术上是不正确的,但编译器会 “原谅”。也就是说,以上才是标准写法。

__strong

__strong修饰符为强引用,会持有对象,使其引用计数 1。该修饰符是对象类型变量的默认修饰符。如果我们没有明确指定对象类型变量的所有权修饰符,其默认就为__strong修饰符。

代码语言:javascript复制
    id obj = [NSObject alloc] init];
    // -> id __strong obj = [NSObject alloc] init];
__weak

如果单单靠__strong完成内存管理,那必然会发生循环引用的情况造成内存泄漏,这时候__weak就出来解决问题了。 __weak修饰符为弱引用,不会持有对象,对象的引用计数不会增加。__weak可以用来防止循环引用。

以下单纯地使用__weak修饰符修饰变量,编译器会给出警告,因为NSObject的实例创建出来没有强引用,就会立即释放。

代码语言:javascript复制
    id __weak weakObj = [[NSObject alloc] init]; // ⚠️Assigning retained object to weak variable; object will be released after assignment
    NSLog(@"%@", obj);
    //  (null)

以下NSObject的实例已有强引用,再赋值给__weak修饰的变量就不会有警告了。

代码语言:javascript复制
    id __strong strongObj = [[NSObject alloc] init];
    id __weak weakObj = strongObj;

当对象被dealloc时,指向该对象的__weak变量会被赋值为nil。(具体的执行过程请参阅:《iOS - 老生常谈内存管理(四):源码分析内存管理方法》

备注:__weak仅在ARC中才能使用,在MRC中是使用__unsafe_unretained修饰符来代替。

__unsafe_unretained

__unsafe_unretained修饰符的特点正如其名所示,不安全且不会持有对象。

注意:尽管ARC内存管理是编译器的工作,但是附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。这一点在使用时要注意。

“不会持有对象” 这一特点使它和__weak的作用相似,可以防止循环引用。 “不安全“ 这一特点是它和__weak的区别,那么它不安全在哪呢?

我们来看代码:

代码语言:javascript复制
    id __weak weakObj = nil;
    id __unsafe_unretained uuObj = nil;
    {
        id __strong strongObj = [[NSObject alloc] init];
        weakObj = strongObj;
        unsafeUnretainedObj = strongObj;
        NSLog(@"strongObj:%@", strongObj);
        NSLog(@"weakObj:%@", weakObj);
        NSLog(@"unsafeUnretainedObj:%@", unsafeUnretainedObj);
    }
    NSLog(@"-----obj dealloc-----");
    NSLog(@"weakObj:%@", weakObj);
    NSLog(@"unsafeUnretainedObj:%@", unsafeUnretainedObj); // Crash:EXC_BAD_ACCESS

/*
strongObj:<NSObject: 0x6000038f4340>
weakObj:<NSObject: 0x6000038f4340>
unsafeUnretainedObj:<NSObject: 0x6000038f4340>
-----obj dealloc-----
weakObj:(null)
(lldb) 
*/

以上代码运行崩溃EXC_BAD_ACCESS。原因是__unsafe_unretained修饰的对象在被销毁之后,指针仍然指向原对象地址,我们称它为 “悬垂指针”。这时候如果继续通过指针访问原对象的话,就会导致Crash。而__weak修饰的对象在被释放之后,会将指向该对象的所有__weak指针变量全都置为nil。这就是__unsafe_unretained不安全的原因。所以,在使用__unsafe_unretained修饰符修饰的对象时,需要确保它未被销毁。

Q: 既然 __weak 更安全,那么为什么已经有了 __weak 还要保留 __unsafe_unretained ?

  • __weak仅在ARC中才能使用,而MRC只能使用__unsafe_unretained
  • __unsafe_unretained主要跟 C 代码交互;
  • __weak对性能会有一定的消耗,当一个对象dealloc时,需要遍历对象的weak表,把表里的所有weak指针变量值置为nil,指向对象的weak指针越多,性能消耗就越多。所以__unsafe_unretained__weak快。当明确知道对象的生命周期时,选择__unsafe_unretained会有一些性能提升。 A 持有 B 对象,当 A 销毁时 B 也销毁。这样当 B 存在,A 就一定会存在。而 B 又要调用 A 的接口时,B 就可以存储 A 的__unsafe_unretained指针。 比如,MyViewController 持有 MyView,MyView 需要调用 MyViewController 的接口。MyView 中就可以存储__unsafe_unretained MyViewController *_viewController。 虽然这种性能上的提升是很微小的。但当你很清楚这种情况下,__unsafe_unretained也是安全的,自然可以快一点就是一点。而当情况不确定的时候,应该优先选用__weak
__autoreleasing
自动释放池

首先讲一下自动释放池,在ARC下已经禁止使用NSAutoreleasePool类创建自动释放池,而用@autoreleasepool替代。

  • MRC下可以使用NSAutoreleasePool或者@autoreleasepool。建议使用@autoreleasepool,苹果说它比NSAutoreleasePool快大约六倍。
代码语言:javascript复制
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // Code benefitting from a local autorelease pool.
    [pool release]; // [pool drain]

Q: 释放NSAutoreleasePool对象,使用[pool release][pool drain]的区别? Objective-C 语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC 环境下[pool release][pool drain]效果一样,但在 GC 环境下drain会触发 GC 而release不做任何操作。使用[pool drain]更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release区别开。(注意:苹果在引入ARC时称,已在 OS X Mountain Lion v10.8 中弃用GC机制,而使用ARC替代)

  • ARC下只能使用@autoreleasepool
代码语言:javascript复制
    @autoreleasepool {
        // Code benefitting from a local autorelease pool.
    }

关于@autoreleasepool的底层原理,可以参阅《iOS - 聊聊 autorelease 和 @autoreleasepool》。

__autoreleasing 使用

MRC中我们可以给对象发送autorelease消息来将它注册到autoreleasepool中。而在ARCautorelease已禁止调用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到autoreleasepool中。

代码语言:javascript复制
    @autoreleasepool {
        id __autoreleasing obj = [[NSObject alloc] init];
    }

以上代码在MRC中等价于:

代码语言:javascript复制
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain];
    // 或者
    @autoreleasepool {
        id obj = [[NSObject alloc] init];
        [obj autorelease];
    }
__autoreleasing 是二级指针类型的默认修饰符

前面我们说过,对象指针的默认所有权修饰符是__strong。 而二级指针类型(ClassName **id *)的默认所有权修饰符是__autoreleasing。如果我们没有明确指定二级指针类型的所有权修饰符,其默认就会附加上__autoreleasing修饰符。

比如,我们经常会在开发中使用到NSError打印错误信息,我们通常会在方法的参数中传递NSError对象的指针。如NSStringstringWithContentsOfFile类方法,其参数NSError **使用的是__autoreleasing修饰符。

代码语言:javascript复制
    NSString *str = [NSString stringWithContentsOfFile:<#(nonnull NSString *)#>
                                              encoding:<#(NSStringEncoding)#> 
                                              error:<#(NSError *__autoreleasing  _Nullable * _Nullable)#>];

示例:我们声明一个参数为NSError **的方法,但不指定其所有权修饰符。

代码语言:javascript复制
- (BOOL)performOperationWithError:(NSError **)error;

接着我们尝试调用该方法,发现智能提示中的参数NSError **附有__autoreleasing修饰符。可见,如果我们没有明确指定二级指针类型的所有权修饰符,其默认就会附加上__autoreleasing修饰符。

代码语言:javascript复制
    NSError *error = nil;
    BOOL result = [self performOperationWithError:<#(NSError *__autoreleasing *)#>];
注意

需要注意的是,赋值给二级指针类型时,所有权修饰符必须一致,否则会编译错误。

代码语言:javascript复制
    NSError *error = nil;
    NSError **error1 = &error;                 // error:Pointer to non-const type 'NSError *' with no explicit ownersh
    NSError *__autoreleasing *error2 = &error; // error:Initializing 'NSError *__autoreleasing *' with an expression of type 'NSError *__strong *' changes retain/release properties of pointer
    NSError *__weak *error3 = &error;          // error:Initializing 'NSError *__weak *' with an expression of type 'NSError *__strong *' changes retain/release properties of pointer
    NSError *__strong *error4 = &error;        // 编译通过
代码语言:javascript复制
    NSError *__weak error = nil;
    NSError *__weak *error1 = &error;          // 编译通过
代码语言:javascript复制
    NSError *__autoreleasing error = nil;
    NSError *__autoreleasing *error1 = &error; // 编译通过

我们前面说过,二级指针类型的默认修饰符是__autoreleasing。那为什么我们调用方法传入__strong修饰的参数就可以编译通过呢?

代码语言:javascript复制
    NSError *__strong error = nil;
    BOOL result = [self performOperationWithError:<#(NSError *__autoreleasing *)#>];

其实,编译器自动将我们的代码转化成了以下形式:

代码语言:javascript复制
    NSError *__strong error = nil;
    NSError *__autoreleasing tmp = error;
    BOOL result = [self performOperationWithError:&tmp];
    error = tmp;

可见,当局部变量声明(__strong)和参数(__autoreleasing)之间不匹配时,会导致编译器创建临时变量。你可以将显示地指定局部变量所有权修饰符为__autoreleasing或者不显式指定(因为其默认就为__autoreleasing),或者显示地指定参数所有权修饰符为__strong,来避免编译器创建临时变量。

代码语言:javascript复制
- (BOOL)performOperationWithError:(NSError *__strong *)error;

但是在MRC引用计数内存管理规则中:使用alloc/new/copy/mutableCopy等方法创建的对象,创建并持有对象;其他情况创建对象但并不持有对象。为了在使用参数获得对象时,遵循此规则,我们应该指定二级指针类型参数修饰符为__autoreleasing

《从 MRC 说起 —— 你不持有通过引用返回的对象》章节中也说到,Cocoa 中的一些方法指定通过引用返回对象(即,它们采用ClassName **id *类型的参数),常见的就是使用NSError对象。当你调用这些方法时,你不会创建该NSError对象,因此你不持有该对象,也无需释放它。而__strong代表持有对象,因此应该使用__autoreleasing

另外,我们在显示指定__autoreleasing修饰符时,必须注意对象变量要为自动变量(包括局部变量、函数以及方法参数),否则编译不通过。

代码语言:javascript复制
    static NSError __autoreleasing *error = nil; // Global variables cannot have __autoreleasing ownership
使用所有权修饰符来避免循环引用

前面已经说过__weak__unsafe_unretained修饰符可以用来循环引用,这里再来啰嗦几句。

如果两个对象互相强引用,就产生了循环引用,导致两个对象都不能被销毁,内存泄漏。或者多个对象,每个对象都强引用下一个对象直到回到第一个,产生大环循环引用,这些对象也均不能被销毁。

ARC中,“循环引用” 是指两个对象都通过__strong持有对方。

解决 “循环引用” 问题就是采用 “断环” 的方式,让其中一方持有另一方的弱引用。同MRC,父对象对它的子对象持有强引用,而子对象对父对象持有弱引用。

ARC中,“弱引用” 是指__weak__unsafe_unretained

delegate 避免循环引用

delegate避免循环引用,就是在委托方声明delegate属性时,使用weak关键字。

代码语言:javascript复制
@property (nonatomic, weak) id<protocolName> delegate;
block 避免循环引用

Q: 为什么 block 会产生循环引用?

  • ① 相互循环引用: 如果当前block对当前对象的某一成员变量进行捕获的话,可能会对它产生强引用。根据block的变量捕获机制,如果block被拷贝到堆上,且捕获的是对象类型的auto变量,则会连同其所有权修饰符一起捕获,所以如果对象是__strong修饰,则block会对它产生强引用(如果block在栈上就不会强引用)。而当前block可能又由于当前对象对其有一个强引用,就产生了相互循环引用的问题;
  • ② 大环引用: 我们如果使用__block的话,在ARC下可能会产生循环引用(MRC则不会)。由于__block修饰符会将变量包装成一个对象,如果block被拷贝到堆上,则会直接对__block变量产生强引用,而__block如果修饰的是对象的话,会根据对象的所有权修饰符做出相应的操作,形成强引用或者弱引用。如果对象是__strong修饰(如__block id x),则__block变量对它产生强引用(在MRC下则不会),如果这时候该对象是对block持有强引用的话,就产生了大环引用的问题。在ARC下可以通过断环的方式去解除循环引用,可以在block中将指针置为nilMRC不会循环引用,则不用解决)。但是有一个弊端,如果该block一直得不到调用,循环引用就一直存在。

ARC 下的解决方式:

  • __weak或者__unsafe_unretained解决:
代码语言:javascript复制
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%p",weakSelf);
    };
代码语言:javascript复制
    __unsafe_unretained id uuSelf = self;
    self.block = ^{
        NSLog(@"%p",uuSelf);
    };

注意__unsafe_unretained会产生悬垂指针,建议使用weak

 对于 non-trivial cycles,我们需要这样做:

代码语言:javascript复制
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        if(!strongSelf) return;
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%p",weakSelf);
    };
  • __block解决(必须要调用block): 缺点:必须要调用block,而且block里要将指针置为nil。如果一直不调用block,对象就会一直保存在内存中,造成内存泄漏。
代码语言:javascript复制
    __block id blockSelf = self;
    self.block = ^{
        NSLog(@"%p",blockSelf);
        blockSelf = nil;
    };
    self.block();

MRC 下的解决方式:

  • __unsafe_unretained解决:同ARC
  • __block解决(在MRC下使用__block修饰对象类型,在block内部不会对该对象进行retain操作,所以在MRC环境下可以通过__block解决循环引用的问题)
代码语言:javascript复制
    __block id blockSelf = self;
    self.block = ^{
        NSLog(@"%p",blockSelf);
    };

更多关于block的内容,可以参阅《OC - Block 详解》。

属性

说到属性,不得不提一下@synthesize@dynamic这两个指令。

@synthesize 和 @dynamic
  • @property:帮我们自动生成属性的settergetter方法的声明。
  • @synthesize:帮我们自动生成settergetter方法的实现以及下划线成员变量。 以前我们需要手动对每个@property添加@synthesize,而在 iOS 6 之后 LLVM 编译器引入了 “property autosynthesis”,即属性自动合成。换句话说,就是编译器会自动为每个@property添加@synthesize

Q: @synthesize现在有什么作用呢? 如果我们同时重写了settergetter方法,则编译器就不会为这个@property添加@synthesize,这时候就不存在下划线成员变量,所以我们需要手动添加@synthesize。 @synthesize propertyName = _propertyName;

 有时候我们不希望编译器为我们@synthesize,我们希望在程序运行过程中再去决定该属性存取方法的实现,就可以使用@dynamic

  • @dynamic :告诉编译器不用自动进行@synthesize,等到运行时再添加方法实现,但是它不会影响@property生成的settergetter方法的声明。@dynamic是 OC 为动态运行时语言的体现。动态运行时语言与编译时语言的区别:动态运行时语言将函数决议推迟到运行时,编译时语言在编译器进行函数决议。
代码语言:javascript复制
@dynamic propertyName;
属性“内存管理”关键字与所有权修饰符的对应关系

属性“内存管理”关键字

所有权修饰符

assign

__unsafe_unretained

unsafe_unretained

__unsafe_unretained

weak

__weak

retain

__strong

strong

__strong

copy

__strong

更多关于属性关键字的内容,可以参阅《OC - 属性关键字和所有权修饰符》。

管理 Outlets 的模式在 iOS 和 OS X 平台下变得一致

ARC下,iOSOS X平台中声明outlets的模式变得一致。你应该采用的模式为:在nib或者storyboard中,除了来自文件所有者的top-level对象的outlets应该使用strong,其它情况下应该使用weak修饰outlets。(详情见 Nib Files in Resource Programming Guide)

栈变量初始化为 nil

使用ARCstrongweakautoreleasing的栈变量现在会默认初始化为nil。例如:

代码语言:javascript复制
- (void)myMethod {
    NSString *name;
    NSLog(@"name: %@", name);
}

打印name的值为null,而不是程序Crash

使用编译器标志启用和禁用 ARC

使用-fobjc-arc编译器标志启用ARC。如果对你来说,某些文件使用MRC更方便,那你可以仅对部分文件使用ARC。对于使用ARC作为默认方式的项目,可以使用-fno-objc-arc编译器标志为指定文件禁用ARC。如下图所示:

ARC支持 Xcode 4.2 及更高版本、OS X v10.6 及更高版本 (64-bit applications) 、iOS 4 及更高版本。但 OS X v10.6 和 iOS 4 不支持weak弱引用。Xcode 4.1 及更早版本中不支持ARC

Managing Toll-Free Bridging

Toll-Free Bridging

你在项目中可能会使用到Core Foundation样式的对象,它可能来自Core Foundation框架或者采用Core Foundation约定标准的其它框架如Core Graphics

编译器不会自动管理Core Foundation对象的生命周期,你必须根据Core Foundation内存管理规则调用CFRetainCFRelease。请参阅《Memory Management Programming Guide for Core Foundation》。

MRC下,我们可以直接在Objective-C指针类型idC指针类型void *之间进行强制转换,如Foundation对象和Core Foundation对象进行转换。由于都是手动管理内存,无须关心内存管理权的移交问题。

ARC下,进行Foundation对象和Core Foundation对象的类型转换,需要使用Toll-Free Bridging桥接)告诉编译器对象的所有权语义。你要选择使用__bridge__bridge_retained__bridge_transfer这三种桥接方案中的一种来确定对象的内存管理权移交问题,它们的作用分别如下:

桥接方案

用法

内存管理权

引用计数

__bridge

F <=> CF

不改变

不改变

__bridge_retained(或 CFBridgingRetain)

F => CF

ARC 管理 => 手动管理(你负责调用 CFRelease 或相关函数来放弃对象所有权)

1

__bridge_transfer(或 CFBridgingRelease)

CF => F

手动管理 => ARC 管理(ARC 负责放弃对象的所有权)

1 再 -1,不改变

  • __bridge(常用):不改变对象的内存管理权所有者。 本来由ARC管理的Foundation对象,转换成Core Foundation对象后继续由ARC管理; 本来由开发者手动管理的Core Foundation对象,转换成Foundation对象后继续由开发者手动管理。 下面以NSMutableArray对象和CFMutableArrayRef对象为例:
代码语言:javascript复制
    // 本来由 ARC 管理
    NSMutableArray *mArray = [[NSMutableArray alloc] init];
    // 转换后继续由 ARC 管理            
    CFMutableArrayRef cfMArray = (__bridge CFMutableArrayRef)(mArray); 

    // 本来由开发者手动管理
    CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); 
    // 转换后继续由开发者手动管理
    NSMutableArray *mArray = (__bridge NSMutableArray *)(cfMArray); 
    ...
    // 在不需要该对象时需要手动释放
    CFRelease(cfMArray); 

__bridge的安全性与赋值给__unsafe_unretained修饰符相近甚至更低,如果使用不当没有注意对象的释放,就会因悬垂指针而导致Crash

__bridge转换后不改变对象的引用计数,比如我们将id类型转换为void *类型,我们在使用void *之前该对象被销毁了,那么我们再使用void *访问该对象肯定会Crash。所以void *指针建议立即使用,如果我们要保存这个void *指针留着以后使用,那么建议使用__bridge_retain

而在使用__bridgevoid *类型转换为id类型时,一定要注意此时对象的内存管理还是由开发者手动管理,记得在不需要对象时进行释放,否则内存泄漏!

以下给出几个 “使用__bridgevoid *类型转换为id类型” 的示例代码,要注意转换后还是由开发者手动管理内存,所以即使离开作用域,该对象还保存在内存中。

代码语言:javascript复制
    // 使用 __strong
    CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    NSMutableArray *mArray = (__bridge NSMutableArray *)(cfMArray);
    
    NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 2, 因为 mArray 是 __strong, 所以增加引用计数
    NSLog(@"%ld", _objc_rootRetainCount(mArray));  // 1, 但是使用 _objc_rootRetainCount 打印出来是 1 ?
     
    // 在不需要该对象时进行释放
    CFRelease(cfMArray); 
    NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 1, 因为 __strong 作用域还没结束,还有强指针引用着
    NSLog(@"%ld", _objc_rootRetainCount(mArray));  // 1
    
   // 在 __strong 作用域结束前,还可以访问该对象
   // 等 __strong 作用域结束,该对象就会销毁,再访问就会崩溃
代码语言:javascript复制
    // 使用 __strong
    CFMutableArrayRef cfMArray;
    {
        cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
        NSMutableArray *mArray = (__bridge NSMutableArray *)(cfMArray);
        NSLog(@"%ld", CFGetRetainCount(cfMArray));  // 2, 因为 mArray 是 __strong, 所以增加引用计数
    }
    NSLog(@"%ld", CFGetRetainCount(cfMArray));      // 1, __strong 作用域结束
    CFRelease(cfMArray); // 释放对象,否则内存泄漏
    // 可以使用 CFShow 函数打印 CF 对象
    CFShow(cfMArray);    // 再次访问就会崩溃
代码语言:javascript复制
    // 使用 __weak
    CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    NSMutableArray __weak *mArray = (__bridge NSMutableArray *)(cfMArray);
    
    NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 1, 因为 mArray 是 __weak, 所以不增加引用计数
    NSLog(@"%ld", _objc_rootRetainCount(mArray));  // 1
    
    /*
     * 使用 mArray
     */
    
    // 在不需要该对象时进行释放
    CFRelease(cfMArray);
    
    NSLog(@"%@",mArray);  // nil, 这就是使用 __weak 的好处,在指向的对象被销毁的时候会自动置指针为 nil,再次访问也不会崩溃
代码语言:javascript复制
    // 使用 __weak
    NSMutableArray __weak *mArray;
    {
        CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
        mArray = (__bridge NSMutableArray *)(cfMArray);
        NSLog(@"%ld", CFGetRetainCount(cfMArray));  // 1, 因为 mArray 是 __weak, 所以不增加引用计数
    }
    CFMutableArrayRef cfMArray =  (__bridge CFMutableArrayRef)(mArray);
    NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 1, 可见即使出了作用域,对象也还没释放,因为内存管理权在我们
    
    CFRelease(cfMArray); // 释放对象,否则内存泄漏
  • __bridge_retained:用在Foundation对象转换成Core Foundation对象时,进行ARC内存管理权的剥夺。 本来由ARC管理的Foundation对象,转换成Core Foundation对象后,ARC不再继续管理该对象,需要由开发者自己手动释放该对象,否则会发生内存泄漏。
代码语言:javascript复制
    // 本来由 ARC 管理
    NSMutableArray *mArray = [[NSMutableArray alloc] init];       
    // 转换后由开发者手动管理              
    CFMutableArrayRef cfMArray = (__bridge_retained CFMutableArrayRef)(mArray); 
//    CFMutableArrayRef cfMArray = (CFMutableArrayRef)CFBridgingRetain(mArray); // 另一种等效写法
    ...
    CFRelease(cfMArray);  // 在不需要该对象的时候记得手动释放

__bridge_retained顾名思义会对对象retain,使转换赋值的变量也持有该对象,对象的引用计数 1。由于转换后由开发者进行手动管理,所以再不需要该对象的时候记得调用CFRelease释放对象,否则内存泄漏。

代码语言:javascript复制
    id obj = [[NSObject alloc] init];
    void *p = (__bridge_retained void *)(obj);

以上代码如果在MRC下相当于:

代码语言:javascript复制
    id obj = [[NSObject alloc] init];

    void *p = obj;
    [(id)p retain];

查看引用计数

ARC下,我们可以使用_objc_rootRetainCount函数查看对象的引用计数(该函数有时候不准确)。对于Core Foundation对象,我们可以使用CFGetRetainCount函数查看引用计数。

代码语言:javascript复制
uintptr_t _objc_rootRetainCount(id obj);

打印上面代码的obj对象的引用计数,发现其引用计数确实增加。

代码语言:javascript复制
    id obj = [[NSObject alloc] init];
    NSLog(@"%ld", _objc_rootRetainCount(obj)); // 1
    void *p = (__bridge_retained void *)(obj);
    NSLog(@"%ld", _objc_rootRetainCount(obj)); // 2
    NSLog(@"%ld", CFGetRetainCount(p));        // 2

以下给出几个示例代码:

代码语言:javascript复制
    CFMutableArrayRef cfMArray;
    {
        NSMutableArray *mArray = [[NSMutableArray alloc] init];
        cfMArray = (__bridge_retained CFMutableArrayRef)(mArray);
        NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 2, 因为 mArray 是 __strong, 而且使用了 __bridge_retained
    }
    NSLog(@"%ld", CFGetRetainCount(cfMArray));     // 1, __strong 作用域结束
    CFRelease(cfMArray); // 在不需要使用的时候释放,防止内存泄漏

如果将上面的代码由__bridge_retained改为使用__bridge会怎样?

代码语言:javascript复制
    CFMutableArrayRef cfMArray;
    {
        NSMutableArray *mArray = [[NSMutableArray alloc] init];
        cfMArray = (__bridge CFMutableArrayRef)(mArray);
        NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 1, 因为 mArray 是 __strong, 且使用 __bridge 还是由 ARC 管理,不增加引用计数
    }
    NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 程序崩溃,因为对象已销毁
  • __bridge_transfer:用在Core Foundation对象转换成Foundation对象时,进行内存管理权的移交。 本来由开发者手动管理的Core Foundation对象,转换成Foundation对象后,将内存管理权移交给ARC,开发者不用再关心对象的释放问题,不用担心内存泄漏。
代码语言:javascript复制
    // 本来由开发者手动管理
    CFMutableArrayRef cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    // 转换后由 ARC 管理
    NSMutableArray *mArray = (__bridge_transfer NSMutableArray *)(cfMArray);
//    NSMutableArray *mArray = CFBridgingRelease(cfMArray); // 另一种等效写法

__bridge_transfer作用如其名,移交内存管理权。它的实现跟__bridge_retained相反,会release被转换的变量持有的对象,但同时它在赋值给转换的变量时会对对象进行retain,所以引用计数不变。也就是说,对于Core Foundation引用计数语义而言,对象是释放的,但是ARC保留了对它的引用。

代码语言:javascript复制
    id obj = (__bridge_transfer void *)(p);

以上代码如果在MRC下相当于:

代码语言:javascript复制
    id obj = (id)p;
    [obj retain];
    [(id)p release];

下面也给出一个示例代码:

代码语言:javascript复制
    CFMutableArrayRef cfMArray;
    {
        cfMArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
        NSMutableArray *mArray = (__bridge_transfer NSMutableArray *)(cfMArray);
        NSLog(@"%ld", CFGetRetainCount(cfMArray));    // 1, 因为 cfMArray 指针指向的对象存在,所以仍然可以通过该指针访问
        NSLog(@"%ld", _objc_rootRetainCount(mArray)); // 1, mArray 为 __strong
    }
    // __strong 作用域结束,ARC 对该对象进行了释放
    NSLog(@"%ld", CFGetRetainCount(cfMArray)); // 再次访问就会崩溃

如果将上面的代码由__bridge_transfer改为使用__bridge会怎样? 其实在__bridge讲解中已经给出了示例代码,如果不释放就会造成内存泄漏。

以上提到了可以替代__bridge_retained__bridge_transfer的两个函数:CFBridgingRetainCFBridgingRelease,下面我们来看一下函数实现:

代码语言:javascript复制
/* Foundation - NSObject.h */
#if __has_feature(objc_arc)  // ARC

// After using a CFBridgingRetain on an NSObject, the caller must take responsibility for calling CFRelease at an appropriate time.
NS_INLINE CF_RETURNS_RETAINED CFTypeRef _Nullable CFBridgingRetain(id _Nullable X) {
    return (__bridge_retained CFTypeRef)X;
}

NS_INLINE id _Nullable CFBridgingRelease(CFTypeRef CF_CONSUMED _Nullable X) {
    return (__bridge_transfer id)X;
}

#else // MRC

// This function is intended for use while converting to ARC mode only.
NS_INLINE CF_RETURNS_RETAINED CFTypeRef _Nullable CFBridgingRetain(id _Nullable X) {
    return X ? CFRetain((CFTypeRef)X) : NULL;
}

// Casts a CoreFoundation object to an Objective-C object, transferring ownership to ARC (ie. no need to CFRelease to balance a prior  1 CFRetain count). NS_RETURNS_RETAINED is used to indicate that the Objective-C object returned has  1 retain count.  So the object is 'released' as far as CoreFoundation reference counting semantics are concerned, but retained (and in need of releasing) in the view of ARC. This function is intended for use while converting to ARC mode only.
NS_INLINE id _Nullable CFBridgingRelease(CFTypeRef CF_CONSUMED _Nullable X) NS_RETURNS_RETAINED {
    return [(id)CFMakeCollectable(X) autorelease];
}

#endif

可以看到在ARC下,这两个函数就是使用了__bridge_retained__bridge_transfer

小结:ARC下,必须恰当使用Toll-Free Bridging桥接)在Foundation对象和Core Foundation对象之间进行类型转换,否则可能会导致内存泄漏。 建议:

  • Foundation对象转为Core Foundation对象时,如果我们立即使用该Core Foundation对象,使用__bridge;如果我们想保存着以后使用,使用__bridge_retained,但是要记得在使用完调用CFRelease释放对象。
  • Core Foundation对象转为Foundation对象时,使用__bridge_transfer

编译器处理从 Cocoa 方法返回的 CF 对象

编译器知道返回Core Foundation对象的Objective-C方法遵循历史 Cocoa 命名约定。例如,编译器知道,在iOS中,UIColorCGColor方法返回的CGColor并不持有(因为方法名不是以alloc/new/copy/mutableCopy开头)。所以你仍然必须使用适当的类型转换,如以下示例所示:

代码语言:javascript复制
    NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
    [colors addObject:(id)[[UIColor lightGrayColor] CGColor]];

否则编译警告:

代码语言:javascript复制
    NSMutableArray *colors = [NSMutableArray arrayWithObject:[[UIColor darkGrayColor] CGColor]];
    // Incompatible pointer types sending 'CGColorRef _Nonnull' (aka 'struct CGColor *') to parameter of type 'id _Nonnull'
    // 不兼容的指针类型,将 CGColorRef(又称 struct CGColor *)作为 id 类型参数传入

使用桥接转换函数参数

当在函数调用中在Objective-CCore Foundation对象之间进行转换时,需要告诉编译器参数的所有权语义。Core Foundation对象的所有权规则请参阅《Memory Management Programming Guide for Core Foundation》。Objective-C的所有权规则请参阅《从 MRC 说起 —— 内存管理策略》章节。

如下实例所示,NSArray对象作为CGGradientCreateWithColors函数的参数传入,它的所有权不需要传递给该函数,因此需要使用__bridge进行强制转换。

代码语言:javascript复制
    NSArray *colors = <#An array of colors#>;
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);

以下实例中使用了Core Foundation对象以及Objective-CCore Foundation对象之间进行转换,同时需要注意Core Foundation对象的内存管理。

代码语言:javascript复制
- (void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
    CGFloat locations[2] = {0.0, 1.0};
    NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
    [colors addObject:(id)[[UIColor lightGrayColor] CGColor]];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
    CGColorSpaceRelease(colorSpace);  // Release owned Core Foundation object.
    CGPoint startPoint = CGPointMake(0.0, 0.0);
    CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds));
    CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint,
                                kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
    CGGradientRelease(gradient);  // Release owned Core Foundation object.
}

ARC 的实现

ARC仅仅依靠LLVM编译器是无法完成内存管理工作的,它还需要Runtime的支持。就比如__weak修饰符,如果没有Runtime,那么在对象dealloc时就不会将__weak变量置为nilARC由以下工具、库来实现:

  • clang(LLVM 编译器)3.0 以上
  • objc4 Objective-C 运行时库 493.9 以上

转换项目时的常见问题

除了以上说明的几点ARC新规则以外,ARC下还要注意以下几个问题,也是MRC转换到ARC项目的常见问题:

  • ARC要求你在init方法中将[super init]的结果分配给self
代码语言:javascript复制
    self = [super init];
    if (self) {
    ...
  • 你无法实现自定义retainrelease方法。 实现自定义retainrelease方法会破坏weak弱指针。你想要这么做的原因可能如下:
    • ① 性能 请不要再这样做了,NSObjectretainrelease方法的实现现在已经足够快了。如果你仍然发现有问题,请提交错误给苹果。
    • ② 实现自定义weak弱指针系统 请改用__weak
    • ③ 实现单例类 请改用shared instance模式。或者,使用类方法替代实例方法,这样可以避免创建对象。
  • “直接赋值” 的实例变量变成强引用了。

ARC之前,实例变量是弱引用(非持有引用) —— 直接将对象分配给实例变量并不延长对象的生命周期。为了使属性变strong,你通常会实现或使用@synthesize合成 “调用适当内存管理方法” 的访问器方法。相反,有时你为了维持一个弱引用,你可能会像以下实例这样实现访问器方法。

代码语言:javascript复制
@interface MyClass : Superclass {
    id thing; // Weak reference.
}
// ...
@end

@implementation MyClass
- (id)thing {
    return thing;
}
- (void)setThing:(id)newThing {
    thing = newThing;
}
// ...
@end

对于ARC,实例变量默认是strong强引用 —— 直接将对象分配给实例变量会延长对象的生命周期。迁移工具在将MRC代码转换为ARC代码时,无法确定它该使用strong还是weak,所以默认使用strong。 若要保持与MRC下一致,必须将实例变量使用__weak修饰,或使用weak关键字的属性。

代码语言:javascript复制
@interface MyClass : Superclass {
    id __weak thing;
}
// ...
@end

@implementation MyClass
- (id)thing {
    return thing;
}
- (void)setThing:(id)newThing {
    thing = newThing;
}
// ...
@end

或者:

代码语言:javascript复制
@interface MyClass : Superclass
@property (weak) id thing;
// ...
@end

@implementation MyClass
@synthesize thing;
// ...
@end

ARC 补充

__weak 黑科技

在所有权修饰符中我们简单介绍了__weak修饰符。实际上,除了在MRC下无法使用__weak修饰符以外,还有其他无法使用__weak修饰符的情况。

例如,有一些类是不支持__weak修饰符的,比如NSMachPort。这些类重写了retain / release并实现该类独自的引用计数机制。但是赋值以及使用附有__weak修饰符的变量都必须恰当地使用 objc4 运行时库中的函数,因此独自实现引用计数机制的类大多不支持__weak修饰符。

代码语言:javascript复制
    NSMachPort __weak *port = [NSMachPort new]; 
    // 编译错误:Class is incompatible with __weak references 类与弱引用不兼容

不支持__weak修饰符的类,其类的声明中添加了NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE宏,该宏的定义如下。

代码语言:javascript复制
// Marks classes which cannot participate in the ARC weak reference feature.
#if __has_attribute(objc_arc_weak_reference_unavailable)
#define NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE __attribute__((objc_arc_weak_reference_unavailable))
#else
#define NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE
#endif

如果将不支持__weak的类的对象赋值给__weak修饰符的变量,一旦编译器检测出来就会报告编译错误。但是在 Cocoa 框架中,不支持__weak修饰符的类极为罕见,因此没有必要太过担心。

__weak黑科技来了!!!!!

还有一种情况也不能使用__weak修饰符。就是当对象的allowsWeakReference/retainWeakReference实例方法返回NO时,这两个方法的声明如下:

代码语言:javascript复制
- (BOOL)allowsWeakReference;
- (BOOL)retainWeakReference;

这两个方法的默认实现是返回YES

如果我们在类中重写了allowsWeakReference方法并返回NO,那么如果我们将该类的实例对象赋值给__weak修饰符的变量,那么程序就会Crash

例如我们在HTPerson类中做了此操作,则以下代码运行就会Crash

代码语言:javascript复制
    HTPerson __weak *p = [[HTPerson alloc] init];
代码语言:javascript复制
// 无法对 HTPerson 类的实例持有弱引用。可能是此对象被过度释放,或者正在销毁。
objc[18094]: Cannot form weak reference to instance (0x600001d7c2a0) of class HTPerson. It is possible that this object was over-released, or is in the process of deallocation.
(lldb) 

所以,对于所有allowsWeakReference方法返回NO的类的实例都绝对不能使用__weak修饰符。

另外,如果实例对象的retainWeakReference方法返回NO,那么赋值该对象__weak修饰符的变量将为nil,代表无法通过__weak变量访问该对象。

比如以下示例代码:

代码语言:javascript复制
    HTPerson *p1 = [[HTPerson alloc] init];    
    HTPerson __weak *p2 = p1;
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);

/* 打印如下:
   <HTPerson: 0x600002e0dd20>
   <HTPerson: 0x600002e0dd20>
   <HTPerson: 0x600002e0dd20>
   <HTPerson: 0x600002e0dd20>
   <HTPerson: 0x600002e0dd20> */

由于p1__strong持有对象的强引用,所以在p1作用域结束前,该对象都存在,使用__weak修饰的p2访问该对象没问题。

下面在HTPerson类中重写retainWeakReference方法:

代码语言:javascript复制
@interface HTPerson ()
{
    NSUInteger count;
}

@implementation HTPerson
- (BOOL)retainWeakReference
{
    if (  count > 3) {
        return NO;
    }
    return [super retainWeakReference];    
}
@end

再次运行以上代码,发现从第 4 次开始,通过__weak变量就无法访问到对象,因为这时候retainWeakReference方法返回值为NO

代码语言:javascript复制
    HTPerson *p1 = [[HTPerson alloc] init];    
    HTPerson __weak *p2 = p1;
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);
    NSLog(@"%@", p2);

/* 打印如下:
   <HTPerson: 0x600003e23ba0>
   <HTPerson: 0x600003e23ba0>
   <HTPerson: 0x600003e23ba0>
   (null)
   (null) */

查看引用计数

ARC下,我们可以使用_objc_rootRetainCount函数查看对象的引用计数。

代码语言:javascript复制
uintptr_t _objc_rootRetainCount(id obj);

但实际上并不能完全信任该函数取得的数值。对于已释放的对象以及不正确的对象地址,有时也返回 “1”。另外,在多线程中使用对象的引用计数数值,因为存在竞争条件的问题,所以取得的数值不一定完全可信。 虽然在调试中_objc_rootRetainCount函数很有用,但最好在了解其所具有的问题的基础上来使用。

苹果对 ARC 一些问题的回答

Q: 我应该如何看待 ARC ?它将 retains/releases 调用的代码放在哪了?

尝试不要去思考ARCretains/releases调用的代码放在哪里,而是思考应用程序算法,思考对象的strongweak指针、所有权、以及可能产生的循环引用。

Q: 我还需要为我的对象编写 dealloc 方法吗?

有时候需要。 因为ARC不会自动处理malloc/freeCore Foundation对象的生命周期管理、文件描述符等等,所以你仍然可以通过编写dealloc方法来释放这些资源。 你不必(实际上不能)释放实例变量,但可能需要对系统类和其他未使用ARC编写的代码调用[self setDelegate:nil]ARC下的dealloc方法中不需要且不允许调用[super dealloc]Runtime会自动处理。

Q: ARC 中仍然可能存在循环引用吗?

是的,ARC自动retain/release,也继承了循环引用问题。幸运的是,迁移到ARC的代码很少开始泄漏,因为属性已经声明是否retain

Q: block 是如何在 ARC 中工作的?

ARC下,编译器会根据情况自动将栈上的block复制到堆上,比如block作为函数返回值时,这样你就不必再调用Block Copy

需要注意的一件事是,在ARC下,NSString * __block myString这样写的话,block会对NSString对象强引用,而不是造成悬垂指针问题。如果你要和MRC保持一致,请使用__block NSString * __unsafe_unretained myString或(更好的是)使用__block NSString * __weak myString

Q: 我可以在 ARC 下创建一个 retained 指针的 C 数组吗?

可以,如下示例所示:

代码语言:javascript复制
// Note calloc() to get zero-filled memory.
__strong SomeClass **dynamicArray = (__strong SomeClass **)calloc(entries, sizeof(SomeClass *));
for (int i = 0; i < entries; i  ) {
     dynamicArray[i] = [[SomeClass alloc] init];
}
 
// When you're done, set each entry to nil to tell ARC to release the object.
for (int i = 0; i < entries; i  ) {
     dynamicArray[i] = nil;
}
free(dynamicArray);

这里有一些注意点:

  • 在某些情况下,你需要编写__strong SomeClass **,因为默认是__autoreleasing SomeClass **
  • 分配的内存区域必须初始化为 0(zero-filled)。
  • free数组之前,必须将每个元素赋值为nilmemsetbzero将不起作用)。
  • 你应该避免使用memcpyrealloc

Q: ARC 速度上慢吗?

不。编译器有效地消除了许多无关的retain/release调用,并且已经投入了大量精力来加速 Objective-C 运行时。特别的是,当方法的调用者是ARC代码时,常见的 “return a retain/autoreleased object” 模式要快很多,并且实际上并不将对象放入自动释放池中。

备注:MRC下,通过alloc/new/copy/mutableCopy或以这些命名开头的方法创建的对象直接持有,而通过其它方法创建的对象会通过调用autorelease加入到自动释放池中。而ARC下不能调用autorelease方法,那么ARC怎么做到这一点呢? 《Objective-C 高级编程:iOS 与 OS X 多线程和内存管理》书中是说:在ARC下,编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到@autoreleasepool。 但经过测试,发现并不是如此。而且,以前在MRC下通过array类方法创建的NSMutableArray对象会被加入到@autoreleasepool,但是在ARC下并不会。 所以,根据方法名并不能判断ARC会不会将对象加入到@autoreleasepool。如果我们需要这么做,建议使用__autoreleasing修饰符。

需要注意的一个问题是,优化器不是在常见的调试配置中运行的,所以预计在-O0模式下将会比-Os模式下看到更多的retain/release调用。

Q: ARC 在 ObjC 模式下工作吗?

是。你甚至可以在类和容器中放置strong/weakid对象。ARC编译器会在复制构造函数和析构函数等中合成retain/release逻辑以使其运行。

Q: 哪些类不支持 weak 弱引用?

你当前无法创建对以下类的实例的weak弱引用:NSATSTypesetter、NSColorSpace、NSFont、NSMenuView、NSParagraphStyle、NSSimpleHorizontalTypesetter 和 NSTextView。

注意: 此外,在 OS X v10.7 中,你无法创建对 NSFontManager,NSFontPanel、NSImage、NSTableCellView、NSViewController、NSWindow 和 NSWindowController 实例的weak弱引用。此外,在 OS X v10.7 中,AV Foundation 框架中的任何类都不支持weak弱引用。

此外,你无法在ARC下创建 NSHashTable、NSMapTable 和 NSPointerArray 类的实例的weak弱引用。

Q: 当我继承一个使用了 NSCopyObject 的类,如 NSCell 时,我需要做些什么?

没什么特别的。ARC会关注以前必须显式添加额外retain的情况。使用ARC,所有的复制方法只需要复制实例变量就可以了。

Q: 我可以对指定文件选择退出ARC而使用MRC吗?

可以。当你迁移项目到ARC或创建一个ARC项目时,所以Objective-C源文件的默认编译器标志将设置为-fobjc-arc,你可以使用-fno-objc-arc编译器标志为指定的类禁用ARC。操作如下图所示:

Q: 在 Mac 上是否弃用了 GC (Garbage Collection) 机制?

OS X Mountain Lion v10.8 中不推荐使用GC机制,并且将在 OS X 的未来版本中删除GC机制。ARC是推荐的替代技术。为了帮助现有应用程序迁移,Xcode 4.3 及更高版本中的ARC迁移工具支持将使用GC的 OS X 应用程序迁移到ARC

注意: 对于面向 Mac App Store 的应用,Apple 强烈建议你尽快使用ARC替换GC,因为 Mac App Store Guidelines 禁止使用已弃用的技术,否则不会通过审核,详情请参阅 Mac App Store Review Guidelines。

0 人点赞