在高级语言中,例如C#是通过垃圾回收机制(GC)来解决这个问题,但是在OC并没有类似的垃圾回收机制,因此必须由程序员手动去维护。今天就讲讲OC中的内存管理:
一、内存管理原理
在Xcode4.2之后的版本,由于引入了ARC(Automatic Reference Counting)机制,程序编译Xcode会自动给你的代码添加内存释放代码,如果编写手动释放代码Xcode会报错,因此在今天的内容,如果使用Xcode4.2以上的版本,必须手动关闭ARC,这样有助于理解OC的内存机制。关闭的步骤如下:
项目设置->Building-setting->搜索garbage找到Objective-C Automatic Reference Counting,设置成No即可。
在C#中有GC给我们自动管理内存,当我们实例化一个对象之后,通常会有一个变量引用这个对象,变量是存储对象的内存地址,当不再需要引用这个对象时,GC就会自动回收这个对象,简单的说,当一个对象没有任何对象引用的时候,就会被回收。
举个简单的例子:
using System;
namespace GC
{
class Program
{
private static void Test()
{
object o=new object();
} static void Main(string[] args)
{
Test();
}
}
}
上述的代码,在Test方法中,通过new Object()创建了一个对象,o是一个对象的引用,它是一个局部变量,作用域为Test方法体内。当在Main方法执行完test,O就会被释放掉,因为没有变量在引用new Object()这个对象,因此GC会自动回收这个对象所占用的空间。
二、引用计数器
在OC中没有GC,那么它的内存是怎么管理呢?其实在OC中内存管理是依赖对象引用计数器来进行的,何为计数器?在OC中每个对象内部都有一个与之对应的整数(retainCount),叫“引用计数器”,一个对象在创建之后它的引用计数器为1,当调用这个对象的时候(alloc,retain,new,copy)的时候,引用计数器自动在原来的基础上加1,当调用这个对象的release方法之后,它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。
下面我们简单做个例子:
Person.h
Person.m
main.m
执行结果如下:
在这个例子中,我们通过重写了dealloc方法,可以知道一个对象是否已经被回收,如果没有被回收则有可能造成内存泄漏。如果一个对象被释放之后,那么最后引用它的变量我们需要手动式何止为nil,否则有可能造成野指针错误,因为访问了不属于这个程序的内存地址。另外要注意一点,在OC中像空指针发送消息是不会引起任何错误的。
产生野指针报错一般如下:
三、内存释放的原则
手动管理内存有时候并不容易,因为对象的引用错综复杂,可能对象之间又相互引用,这时候就要遵循一个原则:谁创建,谁引用。
假设现在有一个Person类,每个Person都可能买一辆汽车,通常买汽车这个行为我们会封装成一个方法中,同时买车的过程中,我们可能会多看几辆车来确定最终理想的车,代码如下:
Car.h
Car.m
Person.h
Person.m
Main.m
执行结果如下:
需要注意的有:
car1和car2都被回收了,然而我们执行p.car run却不会报错,这就达到了我们的目的。首先我们要先解释下,为什么我们的setCar不是写成这种形式?
-(void)setCar:(Car *)car{
_car=car;
}
因为我们之前讲的属性赋值都是采用这种方式的啊。根据前面所说的内存释放原则,getCar方法完全符合,在方法中定义的两个对象car1、car2也都是在这个方法中释放的,main函数的P也是在main函数中定义个释放的。
然而我们是写成这个样子:
-(void)setCar:(Car *)car{
if (_car!=car) { //首先判断要赋值的变量和当前成员变量是不是同一个变量
[_car release]; //释放之前的对象
_car=[car retain];//赋值时重新retain
}
}
在这个方法中我们通过调用car retain可以保证每次属性赋值的时候对象的引用计数器+1,这样以来调用过getCar方法可以保证人员的Car属性不会被释放,同时呢,也可以保证上一次的赋值对象Car1能够正常的释放,因为我们在赋新值的时候,进行了release操作。最后在Person的dealloc方法中对_car进行了一次realse操作(因为在setCar时候做了一次的retain操作),就可以保证_car正常回收。
四、Propety(属性参数)
如上,setCar方法的情况是比较多的,那么如何使用@propety自动实现呢?答案是使用属性参数,例如上面的setter方法,可以通过@property定义如下:
@property (nonatomic,retain) Car *car;
这样子我们不需要手动去实现car的setter方法,setter方法也不会导致内存泄漏,其实大家可以看到前面Person的name属性也加上了(nonatomic,copy)属性,这些参数是什么意思呢?
如下所示,property的参数分为三类,也就是说参数最多只能有三个,中间用逗号隔开,每类的参数可以从上表中的参数中任选一个,如果不进行设置,会取以上三类的默认参数,默认参数为(atomic,readwrite,assign)
一般情况下,如果在多线程开发中一个属性可能会被两个或者两个以上的线程同时访问,这个时候就要考虑atomic,相当于加锁,否则的话,建议使用nonatomic,不用加锁,效率够高。readwrite方法会生成getter、setter两个方法,如果只是readonly只会生成getter方法,关于set方法处理需要特别说明一下,假如我们定义一个属性a,这里列出以上三种生成方案:
1、assign,用于基本类型
-(void)setA:(int)a{
_a=a;
}
2、retain,常用于非字符串对象
-(void)setA:(Car *)a{
if(_a!=a){
[_a release];
_a=[a retain];
}
}
3、copy,常用于字符串对象,block,NSArray,NSDictonary
-(void)setA:(NSString *)a{
if(_a!=a){
[_a release];
_a=[a copy];
}
}
备注:
这是基于MRC进行介绍,ARC的情况不一样,例如ARC的基本数据类型默认的属性参数为(atomic,readwrite,assign),对象类型的默认属性参数为(atomic,readwrite,strong)
五、自动释放池
在OC中,也有一种内存自动释放的机制,叫做:“自动引用计数”或者“自动释放池”,然而,与C#的GC不一样的是,它是一个半自动的,有些操作需要我们手动去设置。自动内存释放我们使用@autoreleasepool关键字来声明一个代码块,如果一个对象在初始化的时候调用了autorelease方法,那么当代码块的代码执行完毕之后,在块中调用过autorelease方法对象都会自动调用一次release方法,这样一来就起到了自动释放的作用,同时对象的销毁过程也得到了延迟,因为统一调用了release方法。
Person.h
Person.m
Main.m
让我们看下执行结果:
如上结果所示,三个对象都得到了释放,但是person4并没有释放,原因很简单,因为我们手动retain了一下,计数器加1,当自动释放池统一release对象,但是Peron4没有释放,这就造成了内存泄漏。再说Person1,autorelease方法只是将对象的内存释放延迟到了自动释放池销毁的时候,因此person1,调用完autorelease之后,它还存在的,因此给它赋值不会有任何问题。在OC中通常一个静态方法返回一个对象本身的话,在静态方法我们需要调用autorelease方法,因为按照内存释放原则,在外部使用不应该对操作这个静态对象的release方法或者autorelease方法,因此这个调用需要在静态方法内部完成。
总结下内存管理:
1、autorealease方法不会改变对象的引用计数器,只是将这个对象放在自动释放池中。
2、自动释放池的本质是当自动释放池销毁后调用对象的release方法,不一定就能销毁对象,只是-1的操作,如果计数器>1,该对象则无法销毁。
3、由于自动释放池最后统一销毁对象,因为如果一个操作比较占用内存,对象比较多,或者对象占用资源比较多,最好不要放在自动释放池中,或者考虑放在多个自动释放池中。
4、OC中类库的静态方法一般都不需要手动释放,因为内部已经调用了autorelease方法。