MRC下的内存管理
引用计数的思考
Objective-C中的内存管理,也就是引用计数。
有关内存管理的方法是包含在Cocoa框架中。Cocoa框架中Foundation框架类库的NSObject类担负内存管理的职责。
内存管理的思考方式
1.自己生成的对象,自己所持有。
使用alloc、new、copy、mutableCopy开头的方法名意味着自己生成的对象并且自己持有。(例:alloc、allocMyObject是,allocate不是)
2.非自己生成的对象,自己也能持有。
NSMutableArray类的array类方法。
//取得非自己生成并持有的对象
id obj = [NSMutableArray array];
[obj retain];
array的内部实现:
- (id)object{
id obj = [[NSObject alloc] init];
//自己持有对象
[obj autorelease];
//取得的对象存在,但自己不持有对象
return obj;
}
3.不再需要自己持有对象时释放。
自己持有的对象,一旦不再需要,有义务释放该对象。释放使用release方法。
4.无法释放非自己持有对象。
id obj = [NSMutableArray array];
//取得的对象存在,但自己不持有对象,所以会导致程序崩溃!
[obj release];
alloc、retain、release、dealloc实现
我们先从GNUstep中了解相关的实现:
//用来保存引用计数
struct obj_layout{
NSUInteger retained;
};
+ (id)alloc{
return [self allocWithZone:NSDefaultMallocZone()];
}
+ (id)allocWithZone:(struct _NSZone *)zone{
return NSAllocsteObject(self, 0 ,zone);
}
inline id NSAllocsteObject(Class aClass, NSUInteger extraBytes,NSZone *zone){
int size = sizeof(struct obj_layout) + 对象大小 + extraBytes;//计算容纳对象所需内存大小
//指向新空间的指针(此时是指向obj_layout结构体)
id new = NSZoneMalloc(zone, size);
//将该空间置为0
menset(new, 0 ,size);
//将指针指向对象
new = (id)&((struct obj_layout *) new)[1];
}
alloc类方法用struct obj_layout的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存全部置0后返回。
对象的引用计数可通过retainCount实例方法取得,本质也是从struct obj_layout的retained获取。
retain、release本质也是对retained值的修改,retain方法使retained变量加1,而release方法使retained变量减1,当retained变量等于0时调用dealloc实例方法,废弃对象(通过free内存空间)。
GNUstep将引用计数保存在对象占用内存块头部的变量,而苹果的实现是保存在引用计数表(散列表,键为内存块地址,值为引用计数)的记录中。优点如下:
- 在对象分配内存块时就无需考虑内存块头部。
- 通过引用计数表的记录追溯到各对象的内存块。
autorelease
autorelease就是自动释放,类似于C语言中自动变量(局部变量)的特性,超出其作用域,对象实例的release实例方法被调用。
autorelease的使用方法如下:
(1)生成并持有NSAutoreleasePool对象;
(2)调用已分配对象的autorelease实例方法;
(3)废弃NSAutoreleasePool对象。
NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。代码如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; //此处obj会调用release
苹果在主线程 RunLoop 里注册了两个 Observer:
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop):
- BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
- Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。优先级最低,保证其释放池子发生在其他所有回调之后。
当然,在ARC情况下我们使用@autoreleasepool{}替代NSAutoreleasePool。看了main方法的代码,你会发现整个应用都在autoreleasepool块中,意味着所有的autorelease对象最后都会被回收,不会导致内存泄露。
在一些特定的情况下,需要我们自己手动创建自动释放池:
-
创建很多临时对象的循环时
在循环中使用自动释放池可以为每个迭代释放内存。虽然迭代前后最终的内存使用相同,但你的应用的最大内存需求可以大大降低。
//场景:读入大量图片同时改变其尺寸
for (int i = 0; i < 图像数; i++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/*
*读入图像,大量产生autorelease对象
*如果没有废弃NSAutoreleasePool对象,会导致内存增加,最终内存不足
*/
[pool drain];//autorelease对象被一起release
}
-
创建一个子线程时
每个线程都将有它自己的自动释放池。不像主线程有统一生成的代码,对于任何自定义的线程,必须创建自己的自动释放池。
该例子我就用@autoreleasepool来表示:
//新线程的入口函数
- (void)mtThreadStart:(id)obj{
@autoreleasepool{
//新线程的代码
}
}
autorelease的实现
苹果的实现和GNUstep相同,我们就以GNUstep的源码来了解其原理。
当对象调用autorelease时,本质上调用了NSAutoreleasePool的adObject类方法,追加到NSAutoreleasePool对象中的数组。
- (id)autorelease{
[NSAutoreleasePool addObject:self];
}
NSAutoreleasePool类的实现:
+ (void)addObject:(id)anObj{
//嵌套情况下是最内侧的NSAutoreleasePool对象
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
if (pool != nil) {
[pool addObject:anObj];
}else{
NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
}
}
- (void)addObject:(id)anObj{
[array addObject:anObj];
}
当NSAutoreleasePool调用drain实例方法时,让数组中的对象release,并销毁自己。
- (void)drain{
[self dealloc];
}
- (void)dealloc{
[self emptyPool];
[array release];
}
- (void)emptyPool{
for (id obj in array) {
[obj release];
}
}
ARC下的内存管理
Objective-C中为了处理对象,可将变量类型定义为id类型或者各种对象类型。所谓对象类型就是指向NSObject这样的Objective-C类的指针,例如“NSObject *”。id 类型用于隐藏对象类型的类名部分,相当于C语言的“void *”。
ARC有效时,id 类型和对象类型通C语言其他类型不同,其类型必须附加所有权修饰符。(用于内存管理,不必再键入retain和release)
- __strong 修饰符
- __weak 修饰符
- __unsafe_unretained 修饰符
- __autoreleasing 修饰符
__strong 修饰符
__strong 修饰符是id 类型和对象类型默认的所有权修饰符。__strong表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
{
//因为obj为强引用,所以自己生成并持有对象
id __strong obj = [[NSObject alloc] init];
}
/*
*因为变量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不持有对象
*/
obj1 = obj0;
/*
* obj0持有obj1赋值的对象B的强引用,对持有的对象A的强引用失效。
* 对象A的所有者不存在,因此废弃对象A
*
* 此时对象B的强引用变量为obj0和obj
*/
obj2 = obj0;
/*
* obj2持有obj0赋值的对象B的强引用。
*
* 此时对象B的强引用变量为obj0和obj1,obj2
*/
obj1 = nil;
/*
* 因为nil赋值给obj1,所以对对象B的强引用无效。
*
* 此时对象B的强引用变量为obj0和obj2
*/
obj0 = nil;
/*
* 因为nil赋值给obj0,所以对对象B的强引用无效。
*
* 此时对象B的强引用变量为obj2
*/
obj2 = nil;
/*
* 因为nil赋值给obj2,所以对对象B的强引用无效。
*
* 对象B的所有者不存在,因此废弃对象B
*/
当然,也可以在方法参数上,使用附有__strong 修饰符的变量。
{
id __strong test = [[Test alloc] init];
/*
* test持有Test对象的强引用
*/
[test setObject:[NSObject alloc] init]];
/*
* test 对象的obj成员持有NSObject的强引用
*/
}
/*
* 因为test变量超出其作用域,强引用失效,所以自动释放Test对象
* Test对象的所有者不存在,因此废弃该对象
*
* 废弃Test对象的同时,成员obj也被废弃,对NSObject的强引用也失效
* NSObject对象的所有者不存在,因此废弃该对象
*/
__strong 修饰符的内部实现:
/*自己生成并持有对象*/
id __strong obj = [[NSObject alloc] init];
//以下为编译器模拟代码
id obj = objc_msgSend(NSObject ,@selector(alloc));
objc_msgSend(obj , @selector(init));
objc_release(obj);
/* 非自己生成但持有对象*/
id __strong obj = [NSMutableArray array];
//以下为编译器模拟代码
id obj = objc_msgSend(NSObject ,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
//其中array的实现:
+ (id)array{
return [[NSMutableArray alloc] init];
//编译器模拟代码
id obj = objc_msgSend(NSMutableArray ,@selector(alloc));
objc_msgSend(obj , @selector(init));
return objc_autoreleaseReturnValue(obj);
}
objc_autoreleaseReturnValue方法本来需要把对象注册到autoreleasepool中,这边苹果有个优化,objc_autoreleaseReturnValue紧接objc_retainAutoreleasedReturnValue会省略autoreleasepool注册。直接传递到方法或函数的调用方。
__weak 修饰符
当会发生循环引用时,__strong 修饰符就不适用了。循环引用容易发生内存泄露。所谓内存泄露就是应当废弃的对象在超出其生命周期后继续存在。以下有两种情况:
带__weak 修饰符的变量(弱引用)不持有对象,所以在超出其变量作用域时,对象即被释放,且处于nil被赋值的状态。
id __weak obj1 = nil;
{
id __strong obj0 = [[NSObject alloc] init];
/*
* obj0变量为强引用,所以自己持有对象
*/
obj1 = obj0;
/*
* obj1持有obj0的弱引用
*/
NSLog(@"%@",obj1);// 输出<NSObject:)x753e180>
}
/*
* 因为obj0变量超出其作用域,强引用失效,所以自动释放NSObject对象
* NSObject对象的无持有者,因此废弃该对象
*
* 废弃对象的同时,持有该对象弱引用的obj1变量的弱引用失效,nil赋值给obj1
*/
NSLog(@"%@",obj0);// 输出(null)
__weak 修饰符的内部实现:
我们来看看__weak内部怎么实现以下功能的:
① 若附有__weak修饰符的变量所引用的对象被废弃,则将赋值给该变量
{
id __weak obj1 = obj;
}
/* 编译器的模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
objc_destroyWeak(&obj1);
objc_initWeak函数将附有__weak修饰符的变量初始化为0后,会将赋值的对象作为参数调用objc_storeWeak函数。
/* 编译器的模拟代码 */
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(&obj1,0);
objc_storeWeak函数会把第二参数的赋值对象的地址作为key,将第一参数的附有__weak修饰符的变量的地址注册到runtime维护的weak表中。如果第二参数为0,则把变量的地址从weak表中删除。
weak表和引用计数表都是runtime维护的散列表。通过废弃对象的地址,可以高速获取到附有__weak修饰符的变量的地址(有可能多个),然后赋值为nil,从weak表中删除该记录,从引用技术表中删除废弃对象为key的记录。
② 使用附有__weak修饰符的变量,即是使用注册到autoreleasepool中的对象
{
id __weak obj1 = obj;//必须是有个强引用的来赋值给obj1,不然会马上被释放掉
NSLog(@"%@",obj1);
}
/* 编译器的模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
id temp = obj_loadWeakReteined(&obj1);//取出obj1对象retain
objc_autorelease(temp);//注册到autoreleasepool中
NSLog(@"%@",obj1);
objc_destroyWeak(&obj1);
因此在使用附有__weak修饰符的变量时,最好先暂时赋值给附有__strong修饰符的变量再使用,防止多次注册到autoreleasepool(一次使用就注册一次)。
__unsafe_unretained 修饰符
__unsafe_unretained 修饰符是不安全的所以权修饰符。其修饰的变量不属于编译器的内存管理对象,同附有__weak 修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。但是和__weak不同的是,__unsafe_unretained在弱引用失效时不会被置为nil,如果后面被访问到,程序会崩溃(悬垂指针)。
__autoreleasing 修饰符
ARC下会用@autoreleasepool块来替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量来替代autorelease方法。(在@autoreleasepool块中不加__autoreleasing也是可以释放的)
以下本质上就用到__autoreleasing修饰符:
1. 在访问__weak修饰符的变量时必须访问到autoreleasepool的对象。因为在访问__weak对象中,该对象有可能被废弃。所以要把访问的对象注册到autoreleasepool中,确保autoreleasepool结束前该对象存在。
id __weak obj0 = obj1;
//本质上编译器生成了以下这句代码,并输出[temp class]
//id __autoreleasing temp = obj0;
NSLog(@"Class = %@",[obj0 class]);
2. __autoreleasing在ARC中主要用在参数传递返回值和引用传递参数的情况下。
比如常用的NSError。如果你的error定义为了strong型,编译器会帮你隐式地做如下事情,保证最终传入函数的参数依然是个__autoreleasing类型的引用。
NSError *error;
NSError *__autoreleasing tempError = error; // 编译器添加
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError])
{
error = tempError; // 编译器添加
NSLog(@"Error: %@", error);
}
所以为了提高效率,避免这种情况,我们一般在定义error的时候将其声明为__autoreleasing类型的:
NSError *__autoreleasing error;
error指向的对象在创建出来后,被放入到了autoreleasing pool中,等待使用结束后的自动释放,函数外error的使用者并不需要关心*error指向对象的释放。
另外,所有这种指针的指针 (NSError **)的函数参数如果不加修饰符,编译器会默认将他们认定为__autoreleasing类型。
//除非显式得给value声明了__strong,否则value默认就是__autoreleasing的。
- (NSString *)doSomething:(NSNumber **)value
{
// do something
}
3.某些类的方法会隐式地使用自己的autorelease pool,在这种时候使用__autoreleasing类型要特别小心。
比如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:
- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){
@autoreleasepool // 被隐式创建
{
if (there is some error && error != nil)
{
*error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
}
}
}];
// *error 在这里已经被dict的做枚举遍历时创建的autorelease pool释放掉了 :(
}
__autoreleasing 修饰符的内部实现:
@autoreleasepool{
id __autoreleasing obj = [[NSObject alloc] init];
}
/* 编译器模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
@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);
所以,本质上是和autorelease的实现一样的。从代码中我们也可以看出,一个线程维护着一个autoreleasePool的栈。我们可以通过_objc_autoreleasePoolPrint函数来观察注册到autoreleasepool中的引用对象。
从MRC到ARC的转变
ARC有效时,要遵守以下规则:
- 不能使用retain/release/retainCount/autorelease
- 不能使用NSAllocteObject/NSdeallocObject
- 须遵守内存管理的方法命名规则
- 不能显式调用dealloc(只能用于已注册代理或观察对象)
- 使用@autoreleasepool块代替NSAutoreleasePool
- 不能使用区域(NSZone)
- 对象型变量不能作为C语言结构体的成员(可强制转换为void *,或者附加__unsafe_unretained 修饰符)
- 显式转换“id”和“void *”
非ARC下,这两个类型是可以直接赋值的
id obj = [NSObject alloc] init];
void *p = obj;
id o = p;
但是在ARC下就会引起编译错误,需要通过__bridege来转换。
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//显式转换
id o = (__bridge id)p;//显式转换
ARC有效时,Objective-C的属性声明的属性和所有权修饰符存在对应关系。