前言:
本篇内容假设您已经对内存管理有了基础的理解。如retain、release、autorelease、autoreleasepool的使用、引用计数式内存管理概念等。本篇内容将围内存管理进行一些深入的探究。
一、内存管理的思考方式
1.1、内存管理的黄金法则
- 自己生成的对象自己持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
以上出现了“生成”、“持有”、“释放”三个词。而在OC内存管理中还要加上“废弃”一词。
对象操作与OC方法的对应
对象操作
|
OC 方法
|
生成并持有对象
|
使用alloc/new/copy/mutableCopy等名称开头的方法
|
持有对象
|
retain方法
|
释放对象
|
release方法
|
废弃对象
|
dealloc方法
|
1.2、命名规则
生成并持有对象的方法名命名规则是遵循驼峰设计法。下列名称意味着自己生成并持有对象。
- allocMyObject
- newThatObject
- copyThis
- mutableCopyYourObject
但是对于以下名称,并不在该规则范围内。
- allocate
- newer
- copyingThis
开发过程中必须严格遵守内存管理命名规则,编译器会根据方法名做出相应的处理。
1.3、关于生成并持有对象与生成不持有对象的实现
那么,如果要用某个方法生成对象,并将其返还给该方法的调用方,它的源代码又是怎样的呢?
-(
id
)allocObject{
//
自己成并持有对象
id obj = [[ NSObject alloc ] init ];
// 自己持有对象
return obj;
}
id obj = [[ NSObject alloc ] init ];
// 自己持有对象
return obj;
}
如上例所示,原封不动的返回用alloc方法生成的对象,就能让调用方也持有该对象。
那么,如果类似调用【NSMutableArray array】方法使取得的对象存在,但自己不持有对象。又是如何实现的呢?
-(
id
)object{
// 自己持有对象
id obj = [[ NSObject alloc ] init ];
// 将对象放入自动释放池
[obj autorelease ];
// 取得对象存在,但自己不持有对象
return obj;
}
// 自己持有对象
id obj = [[ NSObject alloc ] init ];
// 将对象放入自动释放池
[obj autorelease ];
// 取得对象存在,但自己不持有对象
return obj;
}
上例中(注意,方法名不能以alloc/new/copy/mutableCopy等名称开头),使用了autorelease方法。用该方法,可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,使对象在超出指定生存范围时能够自动并正确的释放。当然,也能够通过retain方法将调用autorelease方法取得的对象变为自己持有。
二、ARC规则
2.1、所有权修饰符
OC中为了处理对象,可将变量类型定义为id类型或各种对象类型。
所谓对象类型就是指向NSObject这样的OC类的指针,例如“NSObject *”。id类型用于隐藏对象类型的类名部分,相当于C语言中的“void *
ARC有效时,id类型和对象类型同C语言的其他类型不同,其类型上必须附件所有权修饰符。
- __strong 修饰符
- __weak 修饰符
- __unsafe_unretained 修饰符
- __autoreleasing 修饰符
__strong修饰符
__srong修饰符是id类型和对象类型默认的所有权修饰符。在ARC中,附有__strong修饰符的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。通过__strong修饰符,不必再次键入retain或者release即可满足完美地满足“引用计数式内存管理的思考方式”。另外,__strong修饰符同后面的__weak修饰符和__autoreleasing修饰符都可以保证将附有这些修饰符的自动变量初始化为nil。
__weak修饰符
众所周知,当两个OC对象互相强引用时会产生循环引用,而打破循环引用的利器就是__weak修饰符(弱引用)。__weak修饰符还有另一个有点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态,从而有效的避免野指针(已被释放内存的指针)的发生。
在底层实现上,编译器会维护一个weak表来实现,对象废弃将弱引用指针赋值为nil的操作。
weak表是以散列表的方式来实现的。其key为赋值对象的地址,value为被__weak修饰符修饰的变量。在赋值对象被废弃时最后会执行以下操作以确保__weak修饰符修饰的变量被置为nil。
- 从weak表中获取废弃对象的地址作为key的记录。
- 将包含在记录中的所有附有__weak修饰符变量的地址赋值为nil。
- 从weak表中删除该记录。
- 从引用计数表中删除废弃对象的地址为key的记录。
- 废弃对象
由此可知,如果大量使用附有__weak修饰符的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用__weak修饰符。
__unsafe_unretained 修饰符
__unsafe_unretained修饰符正如其名,是不安全的所有权修饰符。附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。__unsafe_unretained与__weak修饰符一样不会持有对象的强引用。其不同之处在于,若该对象被废弃,编译器不会对被__unsafe_unretained修饰的变量做任何处理从而产生野指针。
__autoreleasing 修饰符
关于__autoreleasing修饰符可以理解为,在ARC有效时,用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法。日常开发中我们虽然很少显式使用__autoreleasing修饰符,但是编译器的确通过隐式的使用__autoreleasing修饰符完成内存管理的工作。例如:
- 如之前提到的命名规则,编译器会检查方法名是否以alloc/new/copy/mutableCopy等名称开头,如果不是则自动将返回的对象以__autoreleasing修饰符修饰(注册到autoreleasepool)。
- 虽然__weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,即是使用注册到autoreleasepool中的对象。这是因为__weak修饰符只持有对象的弱引用,而在访问对象的弱引用的过程中,该对象有可能会被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象的存在。因此,在使用附有__weak修饰符的变量时,即是使用注册到autoreleasepool中的对象。但是,如果大量地使用附有__weak 修饰符的变量,注册到autoreleasepool的对象也会大量的增加,因此在使用附有__weak修饰符的变量时,最好先暂时的赋值给附有__strong修饰符的变量后使用。
- id的指针(id *)或对象的指针(NSObject * *)在没有显示指定时会被附加上__autoreleasing修饰符。比如,为了得到错误的详细信息,经常会在方法的参数中传递NSError对象的指针,而不是函数的返回值。Cocoa的框架中大多数方法也使用这种方式。
NSError
* error =
nil
;
BOOL result = [obj performOperationWithError :&error];
BOOL result = [obj performOperationWithError :&error];
该方法的声明为:
-(
BOOL
)performOperationWithError:(
NSError
**)error;
如同上面的描述一样,id的指针和对象的指针会默认附加上__autoreleasing修饰符,所以等同于一下源代码:
-(
BOOL
)performOperationWithError:(
NSError
*
__autoreleasing
*)error;
参数中持有NSError对象指针的方法,虽然为响应其执行结果,需要生成NSError类对象,但也必须符合内存管理的思考方式。以alloc/new/copy/mutableCopy等名称开头的方法返回值取得的对象是自己生成并持有的,其他情况下便是取得非自己生产并持有的对象。使用__autoreleasing修饰符的变量作为对象取得参数,与除alloc/new/copy/mutableCopy外其他方法返回的对象一样都会注册到autoreleasepool,并取得非自己生产并持有的对象。
比如performOperationWithError的实现应该是这样:
-(
BOOL
)performOperationWithError:(
NSError
*
__autoreleasing
*)error{
//生成的* error 将被注册到autoreleasepool
*error = [[NSError alloc]initWithDomain:(ErrorDomain) code:code userInfo: nil ];
return YES ;
}
*error = [[NSError alloc]initWithDomain:(ErrorDomain) code:code userInfo: nil ];
return YES ;
}
在使用参数取得对象时,贯彻内存管理的思考方式,我们需要将参数声明为附有__autoreleasing修饰符的对象指针类型。
2.2、显式转换id 和 void *
在ARC无效时id 型变量和 void * 变量互相赋值(强制转换)是没有任何问题的。但是在ARC有效时,id 型变量和 void * 变量互相赋值时需要进行特定的转换。转换的方式我们称之为“桥接”,桥接的方式有三种:
- __bridge 转换,如果只是单纯的赋值操作,可以使用“__bridge转换”
id
obj = [[
NSObject
alloc
]
init
];
void * p = ( __bridge void *)obj;
id o = ( __bridge id )p;
void * p = ( __bridge void *)obj;
id o = ( __bridge id )p;
但是,“__bridge 转换”的安全性与赋值给__unsafe_unretained修饰符相近,甚至会更低。如果管理时不注意赋值对象的所有者,就会因为野指针而导致程序崩溃。
- __bridge_retain 转换,可以使要转换赋值的变量也持有所赋值的对象。另外,__bridge_retain 只提供了将 id 类型变量转换为 void * 变量的能力(即 oc -> c)。
void
* p =
0
;
{
//obj 指向对象 引用计数 1
id obj = [[ NSObject alloc ] init ];
//p 持有 obj 指向对象 引用计数二
p = ( __bridge_retained void *)obj;
}
//obj 释放 引用计数减一
// 访问 p 指向对象
NSLog ( @"class = %@" ,[( __bridge id )p class ]);
{
//obj 指向对象 引用计数 1
id obj = [[ NSObject alloc ] init ];
//p 持有 obj 指向对象 引用计数二
p = ( __bridge_retained void *)obj;
}
//obj 释放 引用计数减一
// 访问 p 指向对象
NSLog ( @"class = %@" ,[( __bridge id )p class ]);
变量作用域结束时,虽然obj失效,对象引用计数减一。但由于__bridge_retain 转换使变量p仍然处于持有该对象的状态,因此该对象不会被废弃。
- __bridge_transfer 转换,提供了与__bridge_retain相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随即释放对对象的强引用。另外 __bridge_transfer 只提供了将 void * 变量转换为 id 类型变量的能力
// p
指向创建的对象
引用计数为
1
void * p = ( __bridge_retained void *)[[ NSObject alloc ] init ];
// obj 指向对象的同时 p 释放对对象的强引用 引用计数依然为 1
id obj = ( __bridge_transfer id )p;
void * p = ( __bridge_retained void *)[[ NSObject alloc ] init ];
// obj 指向对象的同时 p 释放对对象的强引用 引用计数依然为 1
id obj = ( __bridge_transfer id )p;
同__bridge_retain转换与retain类似,__bridge_transfer转换与release类似。
2.3、Objective-C对象 与 Core Foundation对象
在2.2 提到的“桥接转换”多数使用在OC对象与Core Foundation对象之间的相互变换中。OC对象与Core Foundation对象的区别很小,不同之处只在于是由哪一个框架所生成的。无论是由哪一种框架所生成的对象,一旦生成之后,便能在不同的框架之中使用。Foundation框架的API生成的对象可以用Core Foundation框架的API释放。当然反过来也是可以的。
因为Core Foundation 和 OC 对象没有区别,所以在ARC无效时,OC变量和Core Foundation变量之间可以相互赋值。而ARC是基于NSObject的 无法管理Core Foundation的内存,所以就需要一些特定的转换。除了 “桥接转换之外” Core Foundation 提供了两个类似的函数进行OC 对象 与Core Foundation对象之间的转换。
- CFBridgingRetain(<id _Nullable X>)其功能与 __bridge_retain 一致
CFMutableArrayRef
cfObj =
NULL
;
{
// 变量 obj 持有 对生成对象的强引用 引用计数 1
NSMutableArray * obj = [[ NSMutableArray alloc ] init ];
// 通过 CFBridgingRetain 将 对象 CFRetain 并赋值给变量 cfObj 引用计数为 2
cfObj = ( CFMutableArrayRef ) CFBridgingRetain (obj);
}
//obj 超出作用域 其强引用失效 引用计数 1
// 将该对象 CFRelease 引用计数 0 释放对象
CFRelease (cfObj);
{
// 变量 obj 持有 对生成对象的强引用 引用计数 1
NSMutableArray * obj = [[ NSMutableArray alloc ] init ];
// 通过 CFBridgingRetain 将 对象 CFRetain 并赋值给变量 cfObj 引用计数为 2
cfObj = ( CFMutableArrayRef ) CFBridgingRetain (obj);
}
//obj 超出作用域 其强引用失效 引用计数 1
// 将该对象 CFRelease 引用计数 0 释放对象
CFRelease (cfObj);
- CFBridgingRelease(<CFTypeRef _Nullable X>)其功能与 __bridge_transfer 一致
{
//Core Foundation 框架生成对象 对象引用计数 1
CFMutableArrayRef cfObj = CFArrayCreateMutable ( kCFAllocatorDefault , 0 , NULL );
// 通过 CFBridgingRelease 赋值,变量 obj 持有对象的强引用的同时, cfObj 通过 CFRelease 释放对象的强引用 对象引用计数 1
NSMutableArray * obj = CFBridgingRelease (cfObj);
}
//obj 超出作用域 引用计数 0 对象释放
//Core Foundation 框架生成对象 对象引用计数 1
CFMutableArrayRef cfObj = CFArrayCreateMutable ( kCFAllocatorDefault , 0 , NULL );
// 通过 CFBridgingRelease 赋值,变量 obj 持有对象的强引用的同时, cfObj 通过 CFRelease 释放对象的强引用 对象引用计数 1
NSMutableArray * obj = CFBridgingRelease (cfObj);
}
//obj 超出作用域 引用计数 0 对象释放
在OC对象与Core Foundation 对象 (id 对象 和 void * 对象)相互转换时 必须恰当的使用CFBridgingRetain 和 CFBridgingRelease (__bridge_retain 和__bridge_transfer),否则会产生内存泄漏或者野指针。因此在实现代码时一定要高度审视。
2.3最优化程序运行
如之前提到的命名规则,编译器会检查方法名是否以alloc/new/copy/mutableCopy等名称开头,如果不是则自动将返回的对象以__autoreleasing修饰符修饰。而在ARC中,编译器通常会返回objc_autoreleaseReturnValue(obj)函数返回的的对象,而不是objc_autorelease(obj)。objc_autorelease(obj)的作用仅限于将对象注册到autoreleasepool中。而objc_autoreleaseReturnValue(obj)的作用不仅限于注册对象到autoreleasepool中。
objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用在调用了方法或函数后紧接着调用了objc_retainAutoreleasedReturnValue函数,那么久不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的调用方。通过objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue函数的协作,可以不将对象注册到autoreleasepool中而是直接传递,使这一过程达到最优。体现在代码层类似这样(以下为伪代码):
+(
id
)array{
return [[ NSMutableArray alloc ] init ];
}
return [[ NSMutableArray alloc ] init ];
}
以上代码在ARC中会转换为以下伪代码:
+(
id
)array{
id obj = objc_msgSend(NSMutableArray, @selector (array));
objc_msgSend(obj, @selector (init));
return objc_autoreleaseReturnValue(obj);
}
id obj = objc_msgSend(NSMutableArray, @selector (array));
objc_msgSend(obj, @selector (init));
return objc_autoreleaseReturnValue(obj);
}
而当[NSMutableArrayarray]方法的返回值赋给被__strong修饰符修饰的变量时,编译器会调用objc_retainAutoreleasedReturnValue函数以避免将对象注册到autoreleasepool中
{
id
__strong
obj = [
NSMutableArray
array
];
}
以上代码在ARC中会转换为以下伪代码:
id
obj = objc_msgSend(NSMutableArray,
@selector
(array));
objc_retainAutoreleasedReturnValue(obj);
objc_retainAutoreleasedReturnValue(obj);
//超出obj 作用域 释放对象
objc_release(obj);
objc_release(obj);
2.4属性修饰符与所有权修饰符
赋值给属性修饰符修饰的属性就相当于赋值给各属性修饰符对应的所有权修饰符修饰的变量中。只有copy属性不是简单的赋值,它赋值的是通过NSCopying接口的copyWithZone:方法复制赋值源所生成的对象。以下为属性修饰符与所有权修饰符的对应关系
属性修饰符 | 所有权修饰符 |
assign | __unsafe_unretained修饰符 |
copy | __strong修饰符(但是赋值的是被复制的对象) |
retain | __strong修饰符 |
strong | __strong修饰符 |
unsafe_unretained | __unsafe_unretained修饰符 |
weak | __weak修饰符 |
本篇对于OC内存管理进行了简要的剖析,如有什么疑问或不对的地方,欢迎评价指正,共同讨论共同进步。