前文:引用计数
请先看我的这篇文章:
引用计数
1. 内存管理
1.1 alloc/retain/release/dealloc的实现
在这里,我们使用开源软件GNUstep(GNUstep的源代码虽不能说与苹果的Cocoa完全相同,胆识从使用者角度来看,两者的行为和实现方法是一样的。)来说明。
alloc实现
在Objective-C中,对象的创建和初始化通常是由类的工厂方法和初始化方法来完成的。这些方法的实现通常包含了为对象分配内存和初始化对象的代码。
以NSObject类的alloc方法为例,其源代码如下:
+ (instancetype)alloc {
return [self allocWithZone:NULL];
}
这个方法是一个类方法,返回一个新分配的对象。它实际上是调用了类的allocWithZone:方法并传入了一个NULL的参数。
而allocWithZone:方法的源代码如下:
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return NSAllocateObject(self, 0, zone);
}
这个方法也是一个类方法,返回一个新分配的对象。它实际上是调用了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函数通过调用NSZoneMalloc函数来分配存放对象所需要的内存空间,之后将空间置0,最好返回作为对象使用的指针。
以下是去掉NSZone后简化了的源代码:
struct obj_layout {
NSUInteger retained;
};
+ (id) alloc {
int size = sizeof(struct obj_layout) + 对象大小;
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
return (id)(p+1);
}
alloc类方法用struct obj_layout 中的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0后返回。以下用图示来展示有关GNUstep的实现。
retain
对象的引用计数可用retainCount实例方法获得。
下面是GNUstep源代码。
- (NSUInteger) retainCount
{
return NSExtraRefCount (self) + 1;
}
inline NSUInteger
NSExtraRefCount (id anOnject)
{
return ((struct obj_layout *) anObject)[-1].retained;
}
由地址寻找到对象内存头部,从而访问其中的retained变量。
因为分配时全部置0,所以retained 为0。由NSExtraRefCount(self)+1得出,retainCount为1。可以推测出retain方法使retained变量加1,而release方法使retained变量减1。
[obj retain];
下面看一下像上面那样调用出的retain实例方法。
- (id) retain
{
NSIncermentExtraRefCount(self);
return self;
}
inline void
NSIncrementExtraRefCount(id anObject)
{
if (((struct obj_layout *)anObject[-1].retained == UINT_MAX - 1)
[NSException raise:NSInternalInconsistencyException format:@"NSIncrementExtraRefCount () asked to increment too far"]);
((struct obj_layout *) anObject)[-1].retained++;
}
这段代码实现了 Objective-C 对象的引用计数机制中 retain 方法的具体实现。当我们调用一个对象的 retain 方法时,其引用计数值会加1,表示有一个新的对象持有了该对象的引用,从而防止该对象在被其它对象持有的引用都被释放后被系统回收。
具体实现中,retain 方法内部会调用 NSIncrementExtraRefCount 函数来实现引用计数的增加。NSIncrementExtraRefCount 函数会将传入的对象的 retained 域加1,该域存储了当前对象被引用的次数。如果 retained 域已经达到最大值(即 UINT_MAX - 1),则会抛出一个内部不一致的异常。
在这段代码中,通过将传入的对象指针 anObject 转换为结构体指针 ((struct obj_layout *) anObject),然后访问其前一个元素 [-1],即访问对象所在内存块的前一个元素,这个前一个元素就是对象的布局结构体,通过访问这个布局结构体的 retained 域,可以得到当前对象的引用计数值,进而实现对其进行增加的操作。
release
以下为release方法的实现。
- (id) release
{
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
if (((struct obj_laout *) anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_laout *) anObject)[-1].retained--;
return NO;
}
}
这段代码实现了 Objective-C 对象的引用计数机制中 release 方法的具体实现。当retained变量大于0时减1,等于0时调用dealloc实例方法,废弃对象。
具体引用计数的访问与retain方法相同。
dealloc
以下为delloc实例方法的实现。
- (void)dealloc
{
NSDealloocateObject(self);
}
inline void
NSDeallocateObject(id anObject)
{
struct obj_layout *o = &((struct obj_layout *)anObject0[-1];
free(o);
}
上述代码仅废弃由alloc分配的代码块。
1.2 苹果的实现
因为NSObject类的源代码没有公开,此处利用Xcode的调试器和iOS大概追溯出其实现过程。
在NSObject类的alloc类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数。
+alloc
+allocWithZone:
class_createInstance
calloc
其中:
class_createInstance 是一个 Objective-C Runtime API,用于创建一个指定类的实例对象。它的声明如下:
id class_createInstance(Class cls, size_t extraBytes);
其中,cls 是要创建实例对象的类,extraBytes 是要为这个对象分配的额外字节数,通常情况下这个参数为 0。函数返回一个指向新创建实例对象的指针。
其中调用了calloc函数分配内存块。
同样的方法,看一下retainCout/retain/release实例方法实现。
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue 返回0时,-release调用dealloc)
其中:
__CFDoExternRefOperation 是 Core Foundation 框架中的一个 C 函数,用于执行外部引用计数操作。在 Core Foundation 中,有一类对象叫做外部引用对象(External References),这些对象的引用计数不由 Core Foundation 管理,而是由外部程序来管理。当 Core Foundation 操作这些对象时,需要调用外部程序提供的函数来对对象的引用计数进行操作,这就是 __CFDoExternRefOperation 函数的作用。
__CFDoExternRefOperation 函数的声明如下:
void __CFDoExternRefOperation(CFTypeRef cf, void (*op)(CFTypeRef cf1, void *context), void *context, Boolean isDeallocating);
该函数接收四个参数:
cf:外部引用对象的指针。
op:指向外部程序提供的引用计数操作函数的指针。这个函数接收两个参数,第一个参数是外部引用对象的指针,第二个参数是上下文信息指针。
context:指向上下文信息的指针,传递给 op 函数。
isDeallocating:表示是否正在释放对象的标志。
在 __CFDoExternRefOperation 函数中,我们首先对传入的参数进行了非空判断,然后根据 isDeallocating 参数的值选择调用外部程序提供的引用计数操作函数 op 的不同版本。如果 isDeallocating 参数为 true,就调用 op 函数的释放版本,否则就调用 op 函数的保留版本。在调用 op 函数之前,我们先对对象进行了锁定操作,防止在操作期间发生对象被释放的情况。
需要注意的是,__CFDoExternRefOperation 函数只是一个内部函数,外部程序一般不会直接调用它。外部程序通常会使用 Core Foundation 提供的一些宏或函数来管理外部引用对象的引用计数,例如 CFMakeExternalRefCounted()、CFRetain() 和 CFRelease() 等函数。
下面是简化了的__CFDoExternRefOperation函数的源代码。
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对像对应的散列表(obj);
int count;
switch(op) {
case OPERATTON_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case OPERATTON_retain:
CFBasicHashAddValue(table, obj);
return obj;
case OPERATTON_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}
__CFDoExternRefOperation函数按retainCount/retain/release操作进行分发,调用不同的函数。NSObject类的retainCount/retain/release实例方法也许如下面代码所示。
- (NSUInteger) retainCount
{
return (NSInteger) __CFDoExternRefOperation(OPERATTON_retainCount, self);
}
- (id) retain
{
return (id) __CFDoExternRefOperation(OPERATTON_retain, self);
}
- (void) release
{
return __CFDoExternRefOperation(OPERATTON_release, self);
}
可以从__CFDoExternRefOperation函数以及由此函数调用的各个函数名来看,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。如图:
这样实现与GNUstep相比的好处:
通过内存块头部管理引用计数的好处如下:
- 少量代码即可完成
- 能够统一管理引用计数内存块与对象的内存块
通过引用计数表管理引用计数的好处如下:
- 对象用内存块的分配无需考虑内存块头部。
- 引用计数表各记录中存有内存地址,可从各个记录追溯到各对象的内存块。
另外,在利用工具检测内存泄露时,引用计数表的各记录也有助于检测各对象的持有者是否存在。
1.3 autorelease
autorelease方法可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,是对象在超出指定的生存范围时能够自动并正确地释放(release方法)。
另外,编程人员可以设定变量的作用域。
autrelease的具体使用方法如下:
- 生成并持有NSAutoreleasePool对象;
- 调用已分配对象的autorrelease实例方法;
- 废弃NSAutoreleasePoll对象。
在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。
1.4 autorelease实现
autorelease的实现。GNUstep的源代码。
[obj autorelease];
此源代码调用NSObject类的autorelease实例方法。
- (id) autorelease
{
[NSAutoreleasePool addObject:self];
}
autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法。
下面看NSAutoreleasePool类的实现。由于NSAutoreleasePool类的源代码比较复杂,所以我们假象一个简化的源代码进行说明。
+ (void) addObject:(id)anObj
{
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
if (pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
}
}
addObject类方法调用正在使用的NSAutoreleasePool对象的addobject实例方法。以下源代码中,被赋予pool变量的即为正在使用的NSAutoreleasePool对象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
如果嵌套生成或持有的NSAutoreleasePool对象,理所当然会使用最内侧的对象。下例中,pool2为正在使用的NSAutoreleasePool对象。
NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool2 drain];
[pool1 drain];
[pool0 drain];
下面看一下addObject实例方法的实现。
- (void) add object:(id) anObj
{
[array addObject:anObj];
}
实际的GNUstep实现使用的是连接列表,这同在NSMutableArray对象中追加对象参数是一样的。
如果调用NSObject类的autorelease实例方法,该对象将被追加到正在使用的NSAutoreleasePool对象的数组里。
以下为通过drain实例方法废弃正在使用的NSAutoreleasePool对象的过程。
[pool drain];
- (void) drain
{
[self dealloc];
}
- (void) dealloc
{
[self emptyPool];
[array release];
}
- (void) emtyPool
{
for (id obj in array) {
[obj release];
}
}
对于我数组中的每个对象都调用了release实例方法。
1.5 苹果的实现
可通过objc4库的runtime/objc-arr.mm来确认autorelease的实现。
- objc4/runtime/objc-arr.mm class AutoreleasePoolPage
class AutoreleasePoolPage
{
static inline void *push()
{
相当于生成或持有NSAutoreleasePool类对象
}
static inline void *pop(void *token)
{
相当于废弃NSAutoreleasePool类对象
releaseAll();
}
static inline autoleasePool(id obj)
{
相当于NSAutoreleasePool类的addObject类方法
AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的AutoreleasePoolPage实例;
autoreleasePoolPage->add(obj);
}
id *add(id obj)
{
将对象追加到内部数组中
}
void releaseAll()
{
调用内部数组中对象的release实例方法
}
};
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
id *objc_autorelease(id obj)
{
return AutoreleasePollPage::autorelease(obj);
}
我们使用调试器来观察一下NSAutoreleasePool类方法和autorelease方法的运行过程。如下所示。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于objc_autoreleasePoolPush()
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于objc_autorelease(obj)
[pool drain];
// 等同于objc_autoreleasePoolPop(pool)
我们可以通过NSAutoreleasePool类中的调试未公开类方法showPools或运行时系统中的非公开函数_objc_autoreleasePoolPrint()来确认已被autorelease的对象的状况。
[NSAutoreleasePool showPools];
extern void _objc_autoreleasePoolPrint(void); // 引入函数
_objc_autoreleasePoolPrint(); // 使用函数
该函数在检查对象是否被自动release时非常有用
2. ARC介绍
2.1 概要
实际上,“引用计数式内存管理”的本质部分在ARC中并没有改变。就像“自动引用计数”这个名称表示的那样,ARC只是自动的帮助我们处理“引用计数”的相关部分。
设置ARC有效的编译方式如下:
- 使用clang(LLVM编译器)3.0或以上版本
- 指定编译器属性为“-fobjc-arc”
Xcode 4.2及以后的版本默认设定对所有的文件ARC有效。
2.2 所有权修饰
Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。
id类型用于隐藏对象类型的类名部分,相当于C语言中常用的“void *”
ARC有效时,id类型和对象类型必须附加所有权修饰符。
所有权修饰符一共有四种。
- __strong
- __weak
- __unsafe_unretained
- __autoreleasing
2.2.1 __strong修饰符
id和对象类型在没有明确指定所有权修饰符时,默认为__strong修饰符。
__strong修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
- 自己生成并持有对象的源代码;
{
// 自己生成对象
id __ strong obj = [[NSObject alloc] init];
// 因为变量obj为强引用,所以自己持有对象
}
// 因为变量obj超出作用域,强引用失效
// 自动释放自己持有的对象
// 对象的持有者不存在,因此废弃该对象
此时,对象的持有者和生存周期是明确的。
- 取得非自己生成并持有的对象;
{
// 取得非自己生成并持有的对象
id __strong obj = [NSMutableArray array];
// 因为变量obj为强引用,所以自己持有对象
}
// 因为变量obj超出其作用域,强引用失效
// 所以自动地释放自己持有的对象
在这里对象的所有者和对象的生存周期也是明确的。
- 附有__strong修饰符的变量之间可以相互赋值;
id __strong obj0 = [[NSObject alloc] init]; // 对象A
// obj0 持有对象A的强引用
id __strong obj1 = [[NSObject alloc] init]; // 对象B
// obj1 持有对象B的强引用
id __strong obj2 = nil;
//obj2 不持有任何对象
obj0 = obj1;
// obj0持有obj1赋值的对象B的强引用
// 因为obj0被赋值,所以原先持有的对对象A的强引用失效
// 对象A的所有者不存在,因此废弃对象A
// 此时持有对象B的变量为obj0和obj1
obj2 = obj0;
// obj2持有由obj0赋值的对象B的强引用
// 此时,持有对象B的强引用的变量为obj0、obj1和obj2
obj1 = nil;
// obj1对对象B的强引用失效
obj2 = nil;
// obj2对对象B的强引用失效
obj0 = nil;
// obj0对对象B的强引用失效
// 对象B的持有者不存在,因此废弃对象B
__strong修饰符的变量,不仅只在变量作用域中,在赋值上也能正确地其管理对象的持有者。
- 用于Objective-C的成员变量和方法参数上;
Test.h
@interface Test : NSObject
{
id __strong obj_;
}
-(void)setObject:(id __strong)obj;
@end
Test.m
@implementation Test
- (id) init
{
self = [super init];
return self;
}
- (void) setObject:(id __strong)obj
{
obj_ = obj;
}
@end
mian.m
{
id __strong test = [[Test alloc] init];
//test持有Test对象的强引用
[test setObject:[[NSObject alloc] init]];
//Test对象的obj成员,持有NSObject对象的强引用
}
//因为test变量超出其作用域,强引用失效,所以自动释放Test对象
//Test对象的所有者不存在,因此废弃该对象
//废弃Test对象的同时,Test对象的obj_成员也被废弃,NSobject对象的强引用失效(说明成员变量的周期与对象是同步的)
//自动释放NSObject对象,NSObject对象的所有者不存在,因此废弃该对象
- 可以与__weak修饰符和__autoreleasing修饰符一起,保证自动变量初始化为nil;
// 下列代码效果相同
id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;
id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;
}
- __strong可能引起的四个变化的思考。
通过__strong修饰符,不必再次键入retain或者release,完美地满足了“引用计数式内存管理的思考方式”。
- 自己生成的对象,自己所持有 即第一点,对带__strong 修饰符的变量赋值便可达成
- 非自己生成的对象,自己也能持有 即第二点
- 自己持有的对象不再需要时释放 通过废弃带__strong修饰符的变量(结束变量作用域)或者对变量赋值
- 非自己持有的对象无法释放 不必键入release,所以原本就不会执行。
2.2.2 __weak修饰符
用来解决__strong修饰符不能解决地一些问题,比如循环引用问题。
- 带有__strong修饰符的成员变量在持有对象时,很容易发生循环引用,并出现内存泄漏。
{
id test0 = [[Test alloc] init]; // 对象A
// test0持有Test对象A的强引用
id test1 = [[Test alloc] init]; // 对象B
// test1持有Test对象B的强引用
[test0 setObject:test1];
// Test对象A的obj_成员变量持有对象B的强引用
[test1 setObject:test0];
// TestB的成员变量obj_持有Test对象A的强引用
}
// 因为test0变量超出其作用域,强引用失效,自动释放Test对象A
// 因为test1变量超出其作用域,强引用失效,自动释放Test对象B
// 此时,持有Test对象A的强引用的变量为Test对象B对象的obj_
// 此时,持有Test对象B的强引用的变量为Test对象A对象的obj_
// 发生内存泄漏
内存泄漏:应当废弃的对象在超出其生命周期后继续存在。内存泄漏在iOS开发中轻则影响性能,重则导致crash。
当对象持有其自身时,也会发生循环引用(自引用)。
id test = [[Test alloc] init];
[test setObject:test];
这时使用__weak修饰符就可以避免循环引用
__为例修饰提供弱引用,弱引用不能持有对象实例。我们来看看下面的代码。
id __weak obj = [[NSObject alloc] init];
如果编译以上代码,编译器就会发出警告。
变量obj被__weak修饰符修饰,持有对象的弱引用。弱引用不能持有对象实例,为了不以自己持有的状态来保存自己生成的对象,该对象会被立即释放。
将对象赋值给__strong修饰符的变量之后在赋值给__weak修饰符修饰的对象就不会发生警告了。
将先前发生可能发生循环引用的类成员变量改成附有__weak修饰符的成员变量的话,该现象便可避免。
@interface Test : NSObject
{
id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end
- __weak修饰符的空弱应用。
在持有某对象的弱引用时,若该对象被放弃,则此弱引用自动失效且处于nil被赋值的状态。
id __weak obj1 = nil;
{
id __strong obj0 = [[NSObject alloc] init];
obj1 = obj0;
NSLog(@"A:%@", obj1);
}
//因为obj0变量超出其作用域,强引用失效,所以自动释放自己持有的对象。
//因为对象无持有者,所以废弃该对象
//废弃对象的同时,弱引用的obj1变量的弱引用失效,nil赋值给obj1.
//输出赋值给obj1变量中的nil
NSLog(@"B:%@", obj1);
此源代码运行结果:
像这样,使用__weak修饰符可避免循环引用。通过检查__weak修饰符的变量是否为nil,可以判断被赋值的对象是否已废弃。
2.2.3 __unsafe_unretained修饰符
__unsafe__unretained 是不安全的所有权修饰符,附有__unsafe__unretained 修饰符的变量不属于编译器的内存管理类对象。
附有__unsafe_unretained修饰符的变量同__weak修饰符的变量一样,因为自己生成的对象不能继续为自己所有,所以生成的变量会立即被释放。下面我们用代码看看差异:
id __unsafe_unretained obj1 = nil;
{
id __strong obj0 = [[NSObject alloc] init];
obj1 = obj0;
NSLog(@"A:%@", obj1);
}
//因为obj0变量超出其作用域,强引用失效,所以自动释放自己持有的对象。
//因为对象无持有者,所以废弃该对象
//变量obj1表示的对象已被废弃(悬垂指针),错误访问
NSLog(@"B:%@", obj1);
在使用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在。
2.2.4 __autoreleasing修饰符
ARC有效时不能使用autorelease方法,也不能使用NSAutoreleasePool类。但是ARC有效时autorelease功能是起作用的。
- 在ARC有效时,可以使用@autoreleasepool来代替NSAutreleasePool类,用附有__autoreleasing修饰符的变量来代替autorelease方法。
- 隐式附加__autoreleasing修饰符
- 获取非自己生成并持有的对象
编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值对象注册到autoleasepool(这同在ARC无效时取得调用了autorelease方法的对象是一样的)也有猜想说当不可变对象以alloc/new/copy/mutableCopy开始时,进行浅拷贝,并没有调用autorelease方法,而是调用了copy方法。
@autoreleasepool {
//取得非自己生成并持有的对象
id __strong obj = [NSMutableArray array];
//因为变量obj为强引用,所以自己持有对象
//且该对象由编译器判断其方法名后,自动注册到autoreleasepool
}
//变量obj超出其作用域,强引用失效,所以自动释放自己持有的对象
//随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象呗自动释放
//因为对象的持有者不存在,所以废弃对象
像这样,不使用__autoreleasing修饰符也能使对象注册到autoreleaspool。以下是取得非自己生成并持有的对象时被调用方法的源代码示例。
+ (id) array
{
return [[NSMutableArry alloc] inut];
}
因为没有显式制定所有权修饰符,当return使对象变量超出其作用域,对应的自己持有的对象会被自己自动释放,但该对象作为函数的返回值,编译器会自动将其注册到autoreleasepool
- 访问__weak修饰符修饰的变量时,实际上必定要访问注册到autoreleasepool的对象
我对这一部分抱有疑问,具体请看我的另一篇博客:【iOS】ARC实现
- id的指针或对象的指针在没有显式指定时会被附加上__autoreleasing修饰符
比如,为了得到详细的错误信息,经常会在方法的参数中传递NSError对象的指针,而不是函数返回值。源代码如下:
NSError *error = nil;
Bool result = [obj performOperationWithError:&error];
// 方法声明
- (BOOL) performOperationWithError:(NSError **)error;
同前面讲述的一样,id的指针或对象的指针或默认加上__autoreleasing修饰符,所以等同于以下源代码。
- (BOOL) performOperationWithError:(NSError * __autoreleaseing *)error;
使用附有__autoreleasing修饰符的变量作为对象取得参数,与除alloc/new/copy/mutableCopy外的其他方法取得对象完全一样,都会注册到autoreleasepool,并取得非自己生成并持有的对象。
- (BOOL) performOperationWithError:(NSError * __autoreleasing *)error
{
// 错误发生
*error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
return NO;
}
NSError * __autoreleasing* 类型的error作为*error被赋值,所以能够返回注册到autoreleasepool中的对象。
- 赋值给对象指针时,所有权修饰符必须一致
NSError *error = nil;
NSError * __strong *pError = &error;
//其他所有权修饰符也是一样
NSError __weak *error = nil;
NSError * __weak *pError = &error;
NSError __unsafe__unretained *error = nil;
NSError * __unsafe__unretained *pError = &error;
//NSError *__autoreleasing error;
//在这里,加上__autoreleasing之后,相当于在MRC中对返回值error做了如下事情:
//*error = [[[NSError alloc] init] autorelease];
//error指向的对象在创建出来后,被放入到了autoreleasing pool中,等待使用结束后的自动释放,函数外error的使用者并不需要关心error指向对象的释放。
在前面的方法参数中,编译器自动的将源代码转换成了下面形式。
NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
Bool result = [obj performOperationWithError:&tmp];
error = tmp;
当然也可以显示的指定方法参数中的对象指针类型的所有权修饰符,这样也能传递对象。但是为了贯彻内存管理的思考方式(除alloc/new/copy/mutableCopy外的其他方法取得非自己生成并持有的对象。),我们有必要将参数声明为附有__autoreleasing修饰符的对象指针类型。
- 显式地指定__autoreleasing修饰符时,必须注意对象变量要为自动变量(包括局部变量,函数以及方法函数)
2.3 属性
当ARC有效时,Objective-C类的属性也会发生变化。
@property (nonatomic, strong) NSString *string;
当ARC有效时,以下可作为这种属性声明中使用的关键字来用。
属性声明中的关键字 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained |
copy | __strong(但是赋值的是被复制的对象不可变副本) |
retain | __strong |
strong | __strong |
unsafe_unretained | __unsafe_unretained |
weak | __weak |
以上各种关键字赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。
另外,在声明成员变量时,如果同属性不一致则会引起编译错误。例:
// 声明属性
@property (nonatomic, weak) id obj;
// 声明成员变量
id _obj;
编译会出现如下错误:
2.4 数组
2.4.1 静态数组
例:
id __strong objs[10];
除__unsafe_unretained修饰符以外的__strong/__weak/_autoreleasing修饰符保证其指定的变量初始化为nil,同样的,附有它们的数组也保证其初始化为nil。
下面看看使用例子:
{
id objs[2];
objs[0] = [[NSObject alloc] init];
objs[1] = [NSMutableArray array];
}
数组超出其作用域时,数组各个附有__strong变量也随之消失,所赋值的对象也随之释放。
2.4.2 动态数组
在C语言的动态数组中也可以使用附有__strong修饰符的变量,只是必须要遵守一些事项。
- 声明动态数组;
id __strong *array = nil;
需要显示指定__strong修饰符(id指针类型变量默认为__autoreleasing修饰符),并保证附有__strong修饰符的id指针类型变量被初始化为nil。(__strong修饰符不保证附有它的id指针类型变量被初始化为nil)
- 使用用calloc分配确保向分配的附有__strong修饰符变量的容器占有的内存块;
array = (id __strong *)calloc(entries, sizeof(id));
由于使用附有__strong修饰符的变量前必须先将其初始化为nil,所以这里使用时分配区域初始化为0的calloc函数来分配内存。也可以在用malloc分配内存后使用memset等函数将内存填充为0。
由于malloc函数分配的内存区域没有初始化为0,因此nil会被赋值给附有__strong修饰符的并被赋值了的随机地址的变量中,从而释放一个不存在的对象。
array = (id __strong *)malloc(entries * sizeof(id));
for (int I = 0; I < entries; I++) {
array[I] = nil;
}
- 使用free函数废弃内存块前,将nil赋值给数组各元素保证对象被释放;
for (int I = 0; I < entries; I++) {
array[I] = nil;
}
free(array);
这里使用memset等函数将内存填充为0不会释放所赋值的对象,这非常危险。
另外,使用memcpy函数拷贝数组以及realloc函数重新分配内存也会有危险。因为数组元素所赋值的对象可能被保留在内存中或是重复被废弃。