什么是引用计数呢?不管是ARC模式(Automatic Reference Counting自动引用计数,可在xcode中设置是否使用)还是非ARC模式,在objective-c中关于对象的内存管理都采用引用计数的方式。每个创建的对象,它都有一个变量指示着当前这个对象被多少个变量引用。当这个对象被引用的计数为0时,编译器就会把这个对象从内存抹去(把它占用的空间全置为0)。
书中指出,在GNUstep(它是Cocoa的互换框架,因为Foundation框架中NSObject的源代码不公开,而GNUstep是公开的,而且设计思路和Cocoa非常相似,可从这个框架的源码来理解苹果Cocoa的实现)中,从NSObject的alloc方法可以看出:
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整数来保存引用计数,并将其写入内存头部。而且由此也可以看出,OC的对象的内存空间开辟时调用的是calloc函数,对象的内存是在堆中开辟的。可以用下图来战术有关
GNUstep中,alloc类方法返回的对象:
但是,Foundation的实现和GNUstep中有些出入,它用了一个散列表管理各个对象的引用计数:
不管是在内存头部插入结构体,还是使用散列表,都各有各的好处,总之记住一点,对于NSObject或者继承于NSObject的对象,只要没有自己重写各种生成对象的方法,在内存中生成对象的时候,都会有一个变量,专门记录当前对象被引用的个数。而内存管理的本质就是:这个变量一旦为0,编译器就会把这个对象所在的内存空间清0舍弃。
(从这里开始所介绍的只适用于非ARC模式下,即以前的开发模式。如果需要实验,需要把xcode设为非ARC模式,具体方法可以百度)
关于对象的操作方法,有以下几种(属于自己的总结,和书上有点区别)
对象操作 | objective-c方法 |
生成对象,并使引用计数置1 | alloc/new/copy/mutableCopy等方法 |
引用计数+1 | retain方法 |
引用计数-1 | release方法 |
废弃对象,清空对象所在的内存空间 | dealloc方法 |
注意retain方法和release必须成对出现,但调用的次数不限。
有了以上的概念,就可以向对象的生成进击了。NSObject中,生成对象的方法大致分为两类,以alloc/new/copy/mutableCopy名称开头;而另一类以对象名称开头,如NSArray中的array方法,也可以概括为类方法。这两类的方法区别在于:
假如当前有个person类:
Person.h
#import
@interface Person : NSObject{
NSString *_name;
NSString *_age;
}
-(void)setName:(NSString *)name;
-(void)setAge:(NSString *)age;
Person.m
#import "Person.h"
@implementation Person
-(void)setName:(NSString *)name
{
_name = name;
}
-(void)setAge:(NSString *)age
{
_age = age;
}
//重写dealloc方法,可以查看编译器什么时候舍弃对象
-(void)dealloc
{
[super dealloc];
NSLog(@"person dealloc");
}
@end
Person *p1 = [[Person alloc] init];
由于没有重写alloc和init方法,于是会调用NSObject的alloc和init方法。会在堆区开辟一块内存空间存放Person对象,并在栈区创建一个指针p1,它指向堆区的那个Person对象。并且Person对象的引用计数记为1,可以理解为p1由此持有了Person对象,就不需要再写[p1 retain]这样的语句,当使用结束后直接调用[p1 release]就可以为其引用计数减1,如果此时Person对象的引用计数为0时,编译器就会回收Person的内存空间。
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
Person *p1 = [[Person alloc] init];
NSLog(@"Person retaincount: %ld", (unsigned long)[p1 retainCount]);//结果是1
Person *p2 = p1;
NSLog(@"Person retaincount: %ld", (unsigned long)[p2 retainCount]);//结果还是1
//用p2指向了p1所指向的对象,但却不为它的引用计数+1 这是危险且不合规范的
//编译器虽然看到p2也指向了这个Person对象,但并不会自动为其引用计数+1,需要程序员手动为其引用计数+1
[p2 retain];
NSLog(@"Person retaincount: %ld", (unsigned long)[p2 retainCount]);//结果为2
//既然有了[p2 retain],那么p2用完之后也要有[p2 release],retain和release方法需要成对出现。
[p2 release];
//此时,仍然可以通过p2指针使用到Person对象,但这是不合规范且危险的
NSLog(@"Person p2:%@", p2);
[p1 release];
//到这里,可以看到重写的dealloc方法被调用了,因此Person对象已经被废弃了
//此时如果再访问Person对象就会报错
//NSLog(@"Person retaincount: %ld", (unsigned long)[p2 retainCount]);
//所以这时候p1和p2都成为野指针了,这个问题会在ARC中得到解决。也可以手动设置p1=nil,p2=nil解决
return 0;
}
由此,其实可以理解retain和release方法了,它们只是让引用计数+1 和 -1 ,并不会消除指针的指向效果。而且编译器在非ARC模式下并不会自动为引用计数+1和-1。因此在早期开发的时候,程序员必须时时刻刻关注引用计数的问题,并且需要的时候手动调用retain和release方法。只要使用就retain,只要用完就release,这样才是规范且安全的。一旦没有及时release,在超出指针的作用域后,就容易出问题。例如上面的return语句之后,p1和p2指针由于是栈区,超出作用域后就回收,当没有及时release的话,Person对象的引用计数不为0,编译器不回收它的内存,就造成内存泄漏了。
同时,从上面的例子可以反映出一个问题,当Person对象被废弃时,在return语句之前,p1指针和p2指针其实还在生命周期内,它们却指向了一块内容未知的内存区域。所以p1和p2成了野指针,容易引发不安全的问题。有经验的程序员此时都会给p1和p2赋值nil。
这类方法以对象名称开头,如NSString的string方法,NSArray的array方法……按照书上讲的话,意思是这样子生成的对象,自己不持有,如果需要持有的话需要再次调用retain方法。不过由于NSString和NSArray是常量,编译器不对它使用引用计数。而我在使用NSMutableString在xcdoe 8.3.2试验的时候,不管retain多少次,引用计数都为1。很多说法就是retaincount获取的数据不准确。加上苹果框架并不开源,不能很清楚的看到它的内存机制。因此,对于这类型的方法,只要记住两点,要么创建完后retain,使用完后要release(这样可以保证你想要的作用域),要么拿来就用,不需要手动retain或者release,这样的作用域就是nsrunloop。
NSMutableString* s1 = [NSMutableString stringWithFormat:@"abc%d",123];
[s1 retain];
//使用过程
[s1 release];
{
int a;
//在当前作用域里a都可以使用
}
/*
这里超出了a的作用于
自动变量a被废弃,不再可以访问
*/
autorelease和NSAutoreleasePool的使用可以总结为:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
//obj的使用过程
[pool drain];
对obj调用autorelease方法就是把obj指向的对象注册到自动释放池pool中。此时,程序员不再需要手动调用[obj release]方法,在调用[pool drain]时,pool会对它里面的所有对象统一调用它们的release方法。由此可以看出,创建pool一直到pool被drain这个范围内就是obj的作用域。
所以一句话理解就是,autorelease就是把指向的对象放进autoreleasepool,drain把autoreleasepool里的所有对象引用计数-1。这时候同样,哪个对象引用计数减为0了,废弃,还不为0的,继续保留。
那为什么要有autorelease呢?其实只要掌握了retain和release不是也能应付吗?其实autorelease和NSAutoreleasePool的使用场景在于,当我们有多个对象的时候,使用它们就可以免于管理多个对象的麻烦,同时避免频繁申请/释放内存也有利于执行效率的提高。
但在autolease使用的过程中还是有问题需要注意的。如果大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能为释放,因此有时候会产生内存不足的现象。典型例子就是读入大量图像同时改变尺寸。图像文件读入到NSData对象,并从中生成UIImage对象,改变该对象后生成新的UIImage对象。这种情况下,就会大量产生autorelease对象。
for(int i =0; i< 图像数; i++)
{
/*
读入图像
大量产生autorelease对象
由于没有废弃NSAutoreleasePool对象
最终导致内存不足
*/
}
在此情况下,有必要在适当的地方生成、持有或者废弃NSAutoreleasePool。
id array = [NSMutableArray arrayWithCapacity:1];
这个方法等同于下面的调用:
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
这天看了一下autorelease和函数返回值方面的内容,更加清楚了上面说的两种生成对象的方法的区别。也更加理解了autorelease的作用。这里记录一下。同样,本篇只记录关于MRC下这方面的内容,对于autorelease和函数返回值在ARC下的体现,留在下一篇介绍。
如果一个函数的返回值是指向一个对象的指针,那么这个对象肯定不能在函数返回之前进行 release(引用计数-1),这样调用者在调用这个函数时得到的就有可能是野指针了,在函数返回之后也不能立刻就release(引用计数-1),因为我们不知道调用者是不是retain(引用计数+1)了这个对象,如果我们直接 release 了,可能导致后面在使用这个对象时它已经成为 nil 了。
为了解决这个纠结的问题, Objective-C 中对对象指针的返回值进行了区分,一种叫做 retained return value ,另一种叫做 unretained return value 。前者表示调用者拥有这个返回值,后者表示调用者不拥有这个返回值,按照“谁拥有谁释放”的原则,对于前者调用者是要负责释放的,对于后者就不需要了。
按照苹果的命名 convention,以 alloc , copy , init , mutableCopy 和 new 这些方法打头的方法,返回的都是 retained return value,例如 [[NSString alloc] initWithFormat:] ,而其他的则是 unretained return value,例如 [NSString stringWithFormat:] 。我们在编写代码时也应该遵守这个 convention。
在 MRC 中我们需要关注这两种函数返回类型的区别,否则可能会导致内存泄露。
- (MyCustomClass *) initWithName:(NSString *) name
{
return [[MyCustomClass alloc] init]; // 不注册到autoreleasepool,使用者需要注意释放
}
这里补充一个说明,虽然我们使用unretained return value可以不考虑release,拿来就用。但还是不建议这样做,最好还是先retain一下,用完再老老实实release,因为这样,使用者才能完完全全掌握这个对象的生命周期。不然,作为放到autoreleasepool的对象,编译器可能在某个时候为它release了一下,而此时它引用计数为0了,被编译器废弃了。假如你之后还需要再用到它,就找不到这个对象