JAVA 使用GC 机制自动管理内存的,Objective-C支持手动管理内存,也支持 GC 机制,但是GC机制对于 iOS设备无效,也就是仅对 Mac OS X 电脑有效。这是合理的,因为iPhone、iPod、iPad等的内存、CPU肯定要比电脑低很多,你必须谨慎对待内存的使用,而不能肆无忌惮的等着GC 帮你去收拾烂摊子。
OC采用对象的内部通过一个retailCount计数器变量来记录对象引用次数,
每次调用对象的alloc、now 、copy 方法时,retailCount为1
调用release方法时retailCount减1
调用 retain方法时retailCount加1【调用retain方法会返回对象,与直接将对象赋值给一个变量的区别在于,retain会将retailCount加1】
当retailCount为0时,OC会自动调用对象的dealloc方法进行内存释放【当对象的retailCount为0时,内存已经被释放,而变量还指向之前对象的内存地址,此时如果再调用对象的方法时,会发生野指针错误】
如,
- int main(){
- Fraction *frac=[[Fraction alloc] initWithNumerator: 3 denominator: 5]; printf("%d ",[frac retainCount]);//1---alloc 分配内存并使引用计数器从 0 变为 1
- [frac retain];//2---引用计数器加 1 printf("%d ",[frac retainCount]);
- [frac retain];//3---引用计数器加 1 printf("%d ",[frac retainCount]);
- [frac release];//2---引用计数器减 1 printf("%d ",[frac retainCount]);
- [frac release];//1---引用计数器减 1 printf("%d ",[frac retainCount]);
- [frac release];//0---引用计数器减 1
- //此时 frac 的 dealloc 方法自动被调用,Objective-C 回收 frac 对象被回收。你可以在 Fraction 中覆盖-(void) dealloc 方法中加个输出语句观察一下。此时你再去调用 frac 的方 法都会导致程序崩溃,因为那块内存去被清理了。但是你可以像下面这样做。
- frac=nil;
- [frac print];//记得前面说过 nil 与 null 的区别
这段代码输出了frac的引用计数器的变化1---2---3---2---1---0,当然这段代码的retain操作毫无意义,仅仅是演示retain、release之后引用计数器的变化情况。
我们定义一个住址类型的类:
Address.h
- #import <Foundation/Foundation.h>
- @interface Address: NSObject{ NSString *city;
- NSString *street;
- }
- -(void) setCity: (NSString*) c;
- -(void) setStreet: (NSString*) s;
- -(void)setCity: (NSString*) c andStreet: (NSString*) s; -(NSString*) city;
- -(NSString*) street;
- @end
与前面的示例不同的是Address的成员变量city、street都是NSString的对象类型,不是基本数据类型。
Address.m
- #import "Address.h"
- @implementation Address -(void) setCity: (NSString*) c{
- [c retain]; [city release]; city=c;
- }
- -(void) setStreet: (NSString*) s{
- [s retain]; [street release]; street=s;
- }
- -(void)setCity: (NSString*) c andStreet: (NSString*) s{
- [self setCity: c];
- [self setStreet: s];
- }
- -(NSString*) city{
- return city;
- }
- -(NSString*) street{
- return street;
- }
- -(void) dealloc{
- [city release]; [street release]; [super dealloc];
- } @end
你可以先不理会这里的一堆retain、release操作,一会儿会做讲解。
main.m
- NSString *city=[[NSString alloc] initWithString: @"Beijing"];
- // initWithString 是 NSString 的一个使用 NSString 字面值初始化的方法。与直接使用下面的 C 语言字符序列初始化相比,@” ”支持 Unicode 字符集。
- NSString *street=[[NSString alloc] initWithCString: "Jinsongzhongjie"];
- // initWithCString 是 NSString 的一个使用 C 语言的字符序列初始化的方法。
- Address *address=[[Address alloc] init]; [address setCity: city andStreet: street];
- [city release]; [street release]; [address release];
我们在 main函数中创建了city、street两个NSString的实例,然后setter到address实例,由于city、street你使用了alloc分配内存,你势必就要release它们。首先要确定的是在main函数里release还是在address里release呢?因为你确实把他们两个传递给Address的实例address了。我们一般按照谁创建的就谁回收的原则(对象的拥有权),那么自然你要在main方法里release,我们恰当的选择了在调用完address的setter方法之后release,因为city、street在main函数中的任务(给address里的两个成员变量赋值)就此结束。此时,新的问题来了,我们在main函数中setter完之后release对象city、street,因此city、street的retainCount会由1变为0,这就会导致你传递到address里的city、street指针指向的对象被清理掉,而使address出错。很显然,你需要在address的setter方法里retain一下,增加city、street的引用计数为2,这样即便在main函数release之后,city、street指向的内存空间也不会被dealloc,因为2-1=1,还有一个引用计数。因此就有了Address中的setter的写法,我们拿setCity方法为例:
- -(void) setCity: (NSString*) c{
- [c retain];//---1
- [city release];//---2
- city=c;//---3
- }
在JAVA这种使用GC机制的语言中,我们只需要写第三条语句。其实Objective-C中你也可以只写第三条语句,我们管这种方式获得的city叫做弱引用,也就是city只是通过赋值操作,把自己指向了c指向的对象,但是对象本身的引用计数器没有任何变化,此时city就要承担[c release]之后所带来的风险,不过有些情况下,你可能确实需要这种弱引用。当然,大多数情况下,我们使用的都是强引用,也就是使用第一行代码首先retain一下,使引用计数器加1,再进行赋值操作,再进一步说就是先通过retain方法拿到对象的拥有权,再安全的使用对象。
第二行代码又是为什么呢?其实这是为了防止city可能已经指向了一个对象,如果不先对city进行一次release,而直接把city指向c指向的对象,那么city原来指向的对象可能会出现内存泄漏,因为city在改变指向的时候,没有将原来指向的对象的引用计数器减1,违反了你retain对象之后,要在恰当的时刻release对象的要求。第三行代码毋庸置疑的要在最后一行出现,但按照前面的阐述,貌似第一行、第二行的代码是没有先后顺序的,也就是可以先[city release],再[c retain],但真的是这样吗?有一种较为少见的情况,那就是把自己作为参数赋给自己(这听起来很怪),也就是说参数c和成员变city是一个指针变量,那么此时如果你先调用[city release],就会导致对象的retainCount归0,对象被dealloc了,那么[c retain]就会报错,因为对象没有了。综上所述,上面所示的三行代码是比较好的组织方式。但其实你可能也会看到有些人用其他的方式书写setter方法,这也没什么好奇怪的,只要保证对象正常的使用、回收就是没有问题的代码。
最后我们再看一下Address中的dealloc方法,因为你在setter中持有了city、street指向的对象的拥有权,那么你势必要和main函数一样,负责在恰当的时刻release你的拥有权,由于你在address实例中可能一直要用到city、street,因此release的最佳时刻就是address要被回收的时候。另外,我们知道对象被dealloc之后,原来指向它的指针们依然指向这块内存区域,Objective-C并不会把这些指着垃圾的指针们都指向nil,因此你如果还调用这些指针们的方法,就会报错。因此有些人喜欢在dealloc里把对象release之后紧接着指向nil,防止它会被意外调用而出现空指针异常。但我认为你这样可能屏蔽了错误,不是一个好办法。
Objective-C 的手动管理内存还提供了一种半自动的方式,就是第八节用到的NSAutoreleasePool类型。只要实例是alloc、new、copy出来的,你就需要选择完全手动的管理内存,也就是你要负责release。第八节的DenominatorNotZeroException异常我们是通过父类NSException的类方法exceptionWithName:reason:userInfo创建的,我们并没有使用到alloc、new、copy关键字,这种情况下,你只需要定义一个NSAutoreleasePool就可以了。为什么是这样呢?
我们先看下面的代码:
- NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
- Fraction *frac=[[Fraction alloc] initWithNumerator: 3 denominator: 5];
- printf("frac 的引用次数 %d ",[frac retainCount]);//1
- [frac retain];
- printf("frac 的引用次数 %d ",[frac retainCount]);//2
- [frac autorelease];
- printf("frac 的引用次数 %d ",[frac retainCount]);//2
- [frac autorelease];
- printf("frac 的引用次数 %d ",[frac retainCount]);//2 [
- pool release];
这里我们对frac用的是autorelease方法,不是release方法,autorelease方法并不对引用计数器减1,而是将对象压入离它最近的NSAutoreleasePool的栈顶(所谓离得最近就表示多个NSAutoreleasePool是可以嵌套存在的),等到NSAutoreleasePool的release方法被调用后,NSAutoreleasePool会将栈内存放的指针指向的对象的release方法。
ps:NSAutoreleasePoole是IOS 5以前的语法,IOS 5以后使用
- @autoreleasepool {
- .....
- }
其实exceptionWithName:reason:userInfo方法的内部实现就是把创建好的NSException的实例的指针返回之前,首先调用了指针的autorelease方法,把它放入了自动回收池。
其实在iOS开发中,苹果也是不建议你使用半自动的内存管理方式,但不像GC机制一样被禁用,原因是这种半自动的内存管理,容易在某些情况下导致内存溢出。我们看一下如下的代码:
- NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];
- for(int i=0;i<1000000;i++){ NSString *s=@"..."; if(i%1000==0){//执行代码}
- }
- [pool release];
这里我们在循环100万次的代码中每次都创建一个NSString实例,由于NSString没有使用alloc创建实例,因此我们使用了自动回收池。但这段代码的问题是这100万个NSString要在for循环完毕,最后的pool release方法调用后才会回收,这就会造成在for循环调用过程中,内存占用一直上涨。当然,你可以改进上面的代码如下所示:
- NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];
- for(int i=0;i<1000000;i++){
- NSString *s=@"..."; if(i%1000==0){
- [pool release];
- pool=[[NSAutoreleasePool alloc] init]; }
- }
上面的代码每循环1000次,就回收自动回收池,然后再紧接着重新创建一个自动回收池给下1000个NSString对象使用。其实这个问题很像Hibernate的一个问题:给你一张1000万行记录的Excel,让你导入到数据库,你该怎么做呢?直接的做法如下所示:
Session session=获取Hibernate的JDBC连接对象
for(int i=0;i<Excel 的行数;i++){
Object obj=每一行的Excel记录对应的JAVA对象;
session.save(obj);
}
Transaction.commit();
由于Hibernate的一级缓存是在你提交事务的时候才清空,并且刷新到数据库上的,因此在循环结束前,你的一级缓存里会积累大量的obj对象,使得内存占用越来越大。
解决办法与Objective-C的自动回收池差不多,就是如下的做法:Session session=获取Hibernate的JDBC连接对象
for(int i=0;i<Excel的行数;i++){
Object obj=每一行的Excel记录对应的JAVA对象;session.save(obj);
if(i%1000==0){
session.flush();
}
}
Transaction.commit();
我们看到每隔1000次就刷新一级缓存,它的作用就是清理一级缓存,把数据都发送到数据库上面去,但是我并没有提交事务,也就是数据都从JVM转移到数据库的缓冲区累积了,数据库依然会等待循环结束后的commit()操作才会提交事务。
总结:内存管理原则
1、谁创建,谁释放。如果你通过alloc、new或(mutable)copy来创建一个对象,那么你必须调用release或autorelease。换句话说,不是你创建的,就不用你去释放
2、一般来说,除了alloc、new或copy之外的方法创建的对象都被声明了autorelease
3、谁retain,谁release。只要你调用了retain,无论这个对象是如何生成的【包含autorelease】,你都要调用release