2、OC内存管理

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\n",[frac retainCount]);//1---alloc 分配内存并使引用计数器从 0 变为 1
[frac retain];//2---引用计数器加 1 printf("%d\n",[frac retainCount]);
[frac retain];//3---引用计数器加 1 printf("%d\n",[frac retainCount]);
[frac release];//2---引用计数器减 1 printf("%d\n",[frac retainCount]);
[frac release];//1---引用计数器减 1 printf("%d\n",[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的成员变量citystreet都是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

你可以先不理会这里的一堆retainrelease操作,一会儿会做讲解。 

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函数中创建了citystreet两个NSString的实例,然后setteraddress实例,由于citystreet你使用了alloc分配内存,你势必就要release它们。首先要确定的是在main函数里release还是在addressrelease呢?因为你确实把他们两个传递给Address的实例address了。我们一般按照谁创建的就谁回收的原则(对象的拥有权),那么自然你要在main方法里release,我们恰当的选择了在调用完addresssetter方法之后release,因为citystreetmain函数中的任务(给address里的两个成员变量赋值)就此结束。此时,新的问题来了,我们在main函数中setter完之后release对象citystreet,因此citystreetretainCount会由1变为0,这就会导致你传递到address里的citystreet指针指向的对象被清理掉,而使address出错。很显然,你需要在addresssetter方法里retain一下,增加citystreet的引用计数为2,这样即便在main函数release之后,citystreet指向的内存空间也不会被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],就会导致对象的retainCount0,对象被dealloc了,那么[c retain]就会报错,因为对象没有了。综上所述,上面所示的三行代码是比较好的组织方式。但其实你可能也会看到有些人用其他的方式书写setter方法,这也没什么好奇怪的,只要保证对象正常的使用、回收就是没有问题的代码。

最后我们再看一下Address中的dealloc方法,因为你在setter中持有了citystreet指向的对象的拥有权,那么你势必要和main函数一样,负责在恰当的时刻release你的拥有权,由于你在address实例中可能一直要用到citystreet,因此release的最佳时刻就是address要被回收的时候。另外,我们知道对象被dealloc之后,原来指向它的指针们依然指向这块内存区域,Objective-C并不会把这些指着垃圾的指针们都指向nil,因此你如果还调用这些指针们的方法,就会报错。因此有些人喜欢在dealloc里把对象release之后紧接着指向nil,防止它会被意外调用而出现空指针异常。但我认为你这样可能屏蔽了错误,不是一个好办法。

Objective-C 的手动管理内存还提供了一种半自动的方式,就是第八节用到的NSAutoreleasePool类型。只要实例是allocnewcopy出来的,你就需要选择完全手动的管理内存,也就是你要负责release。第八节的DenominatorNotZeroException异常我们是通过父类NSException的类方法exceptionWithName:reason:userInfo创建的,我们并没有使用到allocnewcopy关键字,这种情况下,你只需要定义一个NSAutoreleasePool就可以了。为什么是这样呢?

我们先看下面的代码: 

NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
Fraction *frac=[[Fraction alloc] initWithNumerator: 3 denominator: 5];
printf("frac 的引用次数 %d\n",[frac retainCount]);//1
[frac retain];
printf("frac 的引用次数 %d\n",[frac retainCount]);//2
[frac autorelease];
printf("frac 的引用次数 %d\n",[frac retainCount]);//2
[frac autorelease];
printf("frac 的引用次数 %d\n",[frac retainCount]);//2 [
pool release];

这里我们对frac用的是autorelease方法,不是release方法,autorelease方法并不对引用计数器减1,而是将对象压入离它最近的NSAutoreleasePool的栈顶(所谓离得最近就表示多个NSAutoreleasePool是可以嵌套存在的),等到NSAutoreleasePoolrelease方法被调用后,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次,就回收自动回收池,然后再紧接着重新创建一个自动回收池给下1000NSString对象使用。其实这个问题很像Hibernate的一个问题:给你一张1000万行记录的Excel,让你导入到数据库,你该怎么做呢?直接的做法如下所示:
Session session=获取HibernateJDBC连接对象

for(int i=0;i<Excel 的行数;i++){
Object obj=
每一行的Excel记录对应的JAVA对象;

session.save(obj);

}
Transaction.commit();
由于Hibernate的一级缓存是在你提交事务的时候才清空,并且刷新到数据库上的,因此在循环结束前,你的一级缓存里会积累大量的obj对象,使得内存占用越来越大。

解决办法与Objective-C的自动回收池差不多,就是如下的做法:Session session=获取HibernateJDBC连接对象
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、谁创建,谁释放。如果你通过allocnew或(mutable)copy来创建一个对象,那么你必须调用releaseautorelease。换句话说,不是你创建的,就不用你去释放

2、一般来说,除了allocnewcopy之外的方法创建的对象都被声明了autorelease

3、谁retain,谁release。只要你调用了retain,无论这个对象是如何生成的【包含autorelease】,你都要调用release




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值