OC基础15:内存管理和自动引用计数

时间:2024-01-14 12:27:32

  "OC基础"这个分类的文章是我在自学Stephen G.Kochan的《Objective-C程序设计第6版》过程中的笔记。

1、什么是ARC?

(1)、ARC全名为Automatic Reference Counting,即是自动引用计数,会自动统计内存中对象的引用数,并在适当时候自动释放对象;

(2)、在工程中使用ARC非常简单:只需要像往常那样编写代码,只不过永远不用写retain、 release和autorelease三个关键字;

(3)、在使用ARC之前,需要手动管理内存计数,这种机制称为MRC,即是手动引用计数 (Manual Referecen Counting);

(4)、ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做的只不过是在代码编译时为你自动在合适的位置插入release或autorelease,就如同之前MRC时你所做的那样;

(5)、现在只需要用一个指针指向这个对象,只要指针没有被置空,对象就会一直保持在堆上。当将指针指向新值时,原来的对象会被release一次。这对实例变量、synthesize的变量或者局部变量都是适用的。比如:

   NSString *firstName = self.textField.text;

firstName现在指向NSString对象,这时这个对象(textField的内容字符串)将被hold住;

(6)、ARC的一个基本规则是:只要某个对象被任一strong指针指向,那么它将不会被销毁。如果对象没有被任何strong指针指向,那么就将被销毁。在默认情况下,所有的实例变量和局部变量都是strong类型的。可以说strong类型的指针在行为上和MRC时代retain的property是比较相似的,而ARC中默认的指针类型就是strong;

(7)、使用ARC以后再也不需要关心什么时候retain,什么时候release,但是这并不意味你可以不思考内存管理,你可能需要经常性地问自己这个问题:谁在持有着这个对象?

比如下面的代码,假设array是一个NSMutableArray数组并且里面至少有一个对象:

   id obj = [array objectAtIndex:0]; 

   [array removeObjectAtIndex:0]; 

   NSLog(@"%@", obj);

在MRC时代这几行代码应该就挂掉了,因为array中0号对象被remove以后就被立即销毁了,因此obj指向了一个dealloced的对象,因此在NSLog的时候将出现EXC_BAD_ACCESS。而在ARC中由于obj是strong的,因此它持有了array中的首个对象,array不再是该对象的唯一持有者。即使我们从array中将obj移除了,它也不会被销毁,因为它已经被别的指针持有了;

(8)、ARC也有一些缺点,对于初学者来说,可能仅只能将ARC用在objective-c对象上(也即继承自NSObject的对象),但是如果涉及到较为底层的东西,比如Core Foundation中的malloc()或者free()等,ARC就鞭长莫及了,这时候还是需要自己手动进行内存管理;

(9)、你必须时刻清醒谁持有了哪些对象,而这些持有者在什么时候应该变为指向nil。

2、关于MRC:

(1)、使用MRC时,为了保证某个对象存在,每当创建一个引用指向这个对象的时候,需要手工统计引用数,把引用数加1,使用以下语句:

[xxxClass retain];

(2)、当在某处不再需要这个对象时(但是可能别处还需要引用),要把引用数减1,使用以下语句:

 [xxxClass release];

(3)、当对象的引用数为0时,理论上一般就不再需要引用到这个对象了,那么就可以释放它的内存,通过使用NSObject类的dealloc()方法来操作;

(4)、在对象已经被销毁的情况下却还指向这个对象的引用,称为悬挂指针。给悬挂指针发送消息可能会引起系统崩溃;

(5)、当在定义的方法中创建了一个对象,并且最终要把这个对象用作方法的返回值,那么当方法的语句运行完的时候这个对象也还必须保留着以作返回,不能销毁。那么就不能使用release()方法粗暴地释放对象,可以使用以下语句:

[xxxClass autorelease];

       这个方法会把这个对象添加到自动释放池autoreleasepool中,在这个方法中做如下处理,假设在方法中创建的对象是xClass:

XClass *xClass = [[[XClass alloc] init] autorelease];

       或者是写在返回语句上:

       return [xClass autorelease];

(6)、在程序中使用来自Foundation、UIKit、APPKit框架的类时,需要首先创建一个自动释放池,语句为:

@autoreleasepool {

         ...

       }

       当执行到autoreleasepool的结尾时,系统会释放这个池,对于所有发送过autorelease消息来添加到池中的对象,系统会自动对这些对象发送release消息,当某个对象的引用计数已经减至0时(所以如果某个对象的引用计数大于0,它还是不会被释放),会发出dealloc消息,内存会被释放;

(7)、自动释放池里存放的不是实际的对象,仅仅是对象的引用;

(8)、任何继承自NSObject并以alloc、copy、mutableCopynew为前缀的方法创建的对象都不会被自动释放,需要手动来管理;

3、关于属性的特性assign、retain和copy:

(1)、假设在声明某个属性p的时候使用了以上某个特性,那么当为p赋值newValue的时候:

       self.p = newValue;

各个特性的效果各是这样的:

(2)、assign:这是默认特性,使用这个特性之后,赋值语句相当于:

p = newValue;

(3)、retain:赋值语句相当于:

if (p != newValue) {

[p release];

p = [newValue retain];

}

(4)、copy:赋值语句相当于:

if (p != newValue) {

[p release];

p = [newValue copy];

}

4、关于这个三个特性的进一步理解:

(1)、假设你分配了一块内存并存储了一个值在里面,然后把这块内存的地址赋值给了指针a,同时你希望指针b也共享这块内存,于是你又把a直接赋值给了b。这就是assign,简单的指针复制,此时a 和b指向同一块内存。

assign的问题在于:假设a不再需要这块内存时,它并不能直接释放内存,因为a并不知道b是否还在使用这块内存,如果a释放了b却还在使用这块内存,那么b就成了悬挂指针;
   (2)、而retain和assign差不多,也是指针复制,只不过retain在复制指针的过程中使用到了引用计数:当内存被分配并且赋值给a时,引用计数是1,当把a赋值给b时引用计数增加到 2。

所以这时如果a不再使用这块内存,它只需要把引用计数减1,表明自己不再拥有这块内存。b不再使用这块内存时也把引用计数减1即可。当引用计数变为0的时候,系统就知道该内存不再被任何指针所引用,可以把它直接释放掉;
   (3)、copy是在你不希望a和b共享一块内存时会使用到。使用了copy之后a和b各自会有自己的内存;

(4)、为什么在@implementation定义方法要使用到属性的时候要加self.?

因为使用self.会调用到setter方法。如果前面不加self.,相当于assign方式地调用属性,retainCount不会加1的。

5、一个例子可以进一步理解三个特性:

NSString *aHouse =
[[NSString alloc] initWithString:@”一套房子”];

上面一段代码会执行以下两个动作:
   (1)、在堆上分配一段内存用来存储@”一套房子” :假设内存地址为0X0011,内容为@”一套房子”

(2)、在栈上分配一段内存用来存储aHouse,假设地址为 0X00AA , 内容为 0X0011(即是@”一套房子”的地址);
   下面分别看三个特性的效果:  
   (1)、assign的情况: NSString  * bHouse  = aHouse;  
   此时bHouse和aHouse完全相同,地址都是 0X00AA,内容为 0X0011,即 bHouse 只是 aHouse的别名,对任何一个操作就等于对另一个操作。因此 retainCount 不会增加(一把钥匙,双方公用);  
   (2)、retain的情况: NSString  * bHouse  = [aHouse retain];   
   此时 bHouse 的地址仍然为 0X00AA ,存储的内容也仍然是 0X0011,但是aHouse的retainCount会加1。因此aHouse和bHouse都可以管理@”一套房子”所在的内存。(两把钥匙,各自一把);
   (3)、copy的情况: NSString  * bHouse  = [aHouse copy];

此时会在堆上重新开辟一段内存存放@”一套房子”,假设是0X0022,内容为@”一套房子”,同时会在栈上为bHouse分配空间,假设地址为0X00CC,内容便是0X0022。aHouse的retainCount不会变,bHouse有它自己的retainCount了(两套房子,各自一把钥匙,各开各的,互不相干)。

6、什么时候使用retain什么时候使用assign?以5的代码为例来解释:

(1)、什么时候用assign:

破房子、简单的房子就可以共享钥匙,比如:基础类型(简单类型,原子类型)NSInteger、CGPoint、CGFloat、C数据类型(int,float,double,char等);
   (2)、什么时候用copy:
   含有可深拷贝的mutable子类的类,如NSArray、NSSet、NSDictionary、NSData、NSCharacterSet、NSIndexSet、NSString等类。同时需要注意,到了衍生的Mutable类就不能用copy了,不然初始化会有问题。
   (3)、什么时候用retain:
   其他NSObject及其子类对象 (大多数)。

7、关于atomic和nonatomic特性:

(1)、atomic特性表示编译器生成的setter和getter方法是原子操作,nonatomic特性则表示setter和getter方法不会实现原子操作;

(2)、原子操作即是在多线程的情况下,某个资源会被访问它的线程独占,在这个线程使用完这个资源前,其他线程不可以介入;

(3)、所以在调用atomic特性的属性的setter和getter方法时,其他方法无法访问这个属性。属性的nonatomic特性则表示无法独占它的setter和getter方法;

(4)、属性默认是atomic特性;

(5)、所以在多线程的情况下要使用atomic特性,而这种机制是需要消耗内存的,所以如果在不需要多线程的环境下,应把属性声明为nonatomic,以节约内存;

(6)、所以会有atomic对应多线程,而nonatomic是禁止多线程这种说法(其实应该说使用nonatomic就要禁止多线程)。

8、如果定义了一个retain特性的数组属性:

@property (nonatomic, retain)
NSMuatableArray *data;

然后在程序中其他位置要使用到data,如果用以下的语句来初始化:

data = [NSMutableArray array];

array方法创建了一个自动释放的数组,然后把它赋值给了data,那么data数组在当前事件结束后就会被销毁,因为自动释放池会在事件结束后进行清理。如果要让这个数组在事件循环后还能够存活下来,可以使用以下3种方法:

(1)、data =
[[NSMutableArray array] retain];  //直接retain增加retainCount

(2)、data =
[[NSMutableArray alloc] init];  //alloc出来的对象不会被自动释放池销毁

(3)、self.data =
[NSMutableArray array];  //加上self.会使用到setter 方法,那么@property

//里的retain就会起作用了

这3中方法都会导致对象不会被自动释放池自动销毁,那么就需要覆盖dealloc方法:

-(void) dealloc {

[data release];

[super dealloc];

}

9、关于MRC的一些总结:

(1)、对一个对象发送retain消息可以保持这个对象不会被销毁,但是在使用完这个对象之后要发送release消息来释放;

(2)、对一个对象发送release消息并不意味着这个对象就会被销毁了,销毁与否取决于对象的引用计数,release消息只会让对象的引用计数减1,当引用计数减至0时对象才会销毁;

(3)、要对使用了alloc、new、copy、mutableCopy或retain方法的任何对象,或是具有retain和copy特性的属性进行释放,需要覆盖dealloc方法;

(4)、自动释放池清空的时候,系统会发送release消息给池中的每个对象,然后对于那些引用计数减至0的对象,系统会发送dealloc消息销毁这些对象;

(5)、如果某个对象需要作为方法的返回值使用,即是方法调用完之后方法中的某个对象仍必须继续存活下去,那么可以对这个对象使用autorelease方法。autorelease方法不会影响到引用计数,只会标记这个对象延迟释放;

(6)、当应用程序终止时,所有对象全部会被释放;

(7)、在程序运行的过程中,一个事件处理完后,系统会清空自动释放池,并等待下个事件发生。如果要让某个成员对象或者实例变量在自动释放池清空后还能够存在,可以对它发送retain消息,只要对象或变量的引用计数大于发送autorelease消息的数量,就能够在自动释放池的清理中生存下来。

10、关于强变量和弱变量:

(1)、强变量使用关键字strong,弱变量使用关键字weak;

(2)、通常所有对象的指针变量默认都是强变量,强变量默认会被初始化为零。但是属性的默认特性不是strong,默认特性是unsafe_unretaind(相当于assign);

(3)、为对象变量或者属性指定weak特性的方法如下:

__weak UIView *parentView;

@property (weak, nonatomic)
UIView *parentView;

(4)、strong关键字与retain相似,用了它引用计数会自动加1,即是它所指向的内容会为它而保留,不会无故被销毁。用以下代码来说明:

...  //首先有以下定义

@property (nonatomic, strong) NSString *string1;

@property (nonatomic, strong) NSString *string2;

...

@synthesize string1;

@synthesize string2;

...   //情况1:

self.string1 = @"xxxx";   //retainCount = 1

self.string2 = self.string1;  //retainCount = 2

self.string1 = nil;    
    //retainCount = 1

NSLog(@"String 2 = %@", self.string2);

//输出的结果是:String
2 = xxxx

...   //情况2,string2被声明为weak特性:

@property (nonatomic, weak) NSString *string2;

...

self.string1 = @"String 1";  //retainCount = 1

self.string2 = self.string1;    //retainCount = 1

self.string1 = nil;          //retainCount = 0

NSLog(@"String 2 = %@", self.string2);

//输出的结果是:String
2 = null

(5)、所以强变量会将指向对象的retainCount加1,能保证持有;而弱变量则无法影响到指向对象的retainCount;弱变量有一个好处:如上例所示,如果弱变量指向的对象被释放了,若变量会自动被设置为nil;

(6)、当两个对象互相持有对方的引用,并且双方都是强变量的时候,就会产生循环引用,会导致这两个对象一直存在,无法被销毁。这时候要把其中一个对象声明为弱变量来解决这个问题;