第九章、内存管理

1.对象生命周期

对象的生命周期包括:

  • 诞生:通过alloc或者new方法实现
  • 生存:接收消息并执行操作
  • 交友:通过复合以及向方法传递参数
  • 死去:被释放掉

1.引用计数

Cocoa采用了一种叫做引用计数的技术,有时也叫做保留计数

  • 每个对象都有一个与之相关联的整数,被称作它的引用计数器或保留计数器
  • 当某段代码需要访问一个对象时,该代码就将该对象的保留计数器加1,表示“我要访问该对象”,当这段代码结束对象访问时,将对象的保留计数器减1,表示它不再访问该对象
  • 当某个对象的保留计数器的值为0时,表示不再有代码访问该对象,它将会被销毁,其占用的内存被系统回收以便重用

各个部分代码

  • 当使用alloc ,new方法或者copy消息(接收到消息的对象会创建一个自身的副本)创建一个对象时,对象的保留技术器设置为1
  • 获得保留计数其器前的值,可以发送retainCount消息
  • 增加对象的保留技术器的值,可以给对象发送一个retain消息
  • 减少对象的保留技术器的值,可以给对象发送一个release消息
  • 当一个对象的因其计数保留器归0而即将被销毁时,OC会自动向对象发送一条dealloc消息,你也可以在自己的对象中重写delloc方法,这样就能释放掉已经分配的全部相关资源,一定不要直接调用dealloc方法,OC会在需要销毁对象时自动调用它
- (id) retain;
- (oneway void) release;
- (NSUInteger) retaintCount

RetainTracker示例:

#import 
@interface Retaintracker : NSObject
@end//Retaintracker
@implementation Retaintracker
-(id) init{
    if (self = [super init]) {
        NSLog(@"init: Retain count of %ld.",[self retainCount]);
    }
    return  self;
}//init
-(void) dealloc{
    NSLog(@"dealloc called .Bye Bye.");
    [super dealloc];
}//dealloc
@end//Retaintracker
int main(int argc, const char * argv[]) {
    Retaintracker *tracker = [Retaintracker new];//count = 1
    [tracker retain];//count = 2
    NSLog(@"%ld",[tracker retainCount]);
    [tracker retain];//count = 3
    NSLog(@"%ld",[tracker retainCount]);
    [tracker release];//count = 2
    NSLog(@"%ld",[tracker retainCount]);
    [tracker release];//count = 1
    NSLog(@"%ld",[tracker retainCount]);
    [tracker release];//count = 0 dealloc it
    return 0;
}//main
//运行结果
2018-10-14 10:10:26.406398-0700 RetainTracker[732:29322] init: Retain count of 1.
2018-10-14 10:10:26.406589-0700 RetainTracker[732:29322] 2
2018-10-14 10:10:26.406598-0700 RetainTracker[732:29322] 3
2018-10-14 10:10:26.406609-0700 RetainTracker[732:29322] 2
2018-10-14 10:10:26.406615-0700 RetainTracker[732:29322] 1
2018-10-14 10:10:26.406621-0700 RetainTracker[732:29322] dealloc called .Bye Bye.
Program ended with exit code: 0

2.对象所有权

  • 当我们说某个实体“拥有一个对象时”,就意味着该实体要负责确保对其拥有的对象进行清理
  • 如果一个对象内有指向其他对象的实例变量,则称该对象拥有这些对象,在RetainTracker程序中,main()函数拥有RetainTracker类的对象,因此main()函数要负责清理该类的对象

3.访问方法中的保留和释放

1.简单的调用

//Car类中变量engine的存取方法
- (void) setEngine:(Engine *) newEngine;
//main()中调用
Engine *engine = [Engine new];
[car setEngine:engine];
  • Car类保留engine对象,将engine对象的保留计数器的值增加到2,这是因为Car类和main()函数这两个实体都在使用engine对象,Car对象应该在setEngine:方法中保留engine对象,而main()函数负责释放engine对象
  • 然后,当Car对象完成其任务时再释放engine对象(dealloc方法),最后engine对象占用的资源被回收

2.复杂的的调用

//Car类中变量engine的存取方法
- (void) setEngine:(Engine *) newEngine{
      engine = [newEngine retain];
}
//main()中调用
Engine *engine1 = [Engine new];//count = 1
[car setEngine:engine1];//count = 2
[engine1 release];//count = 1

Engine *engine2 = [Engine new];//count = 1
[car setEngine:engine2];//count = 2

engine1对象出现问题了,main()函数已经释放了对engine1的引用,但是Car类一直没有释放engine1对象,它的保留计数器任然为1,此时,engine1对象发生了泄露

//Car类中变量engine的存取方法
- (void) setEngine:(Engine *) newEngine{
      [engine release];
      engine = [newEngine retain];
}
//main()中调用
Engine *engine1 = [Engine new];//count = 1
[car setEngine:engine1];//count = 2
[engine1 release];//count = 1

Engine *engine2 = [Engine new];//count = 1
[car setEngine:engine2];//count = 2

修改setEngine:方法后,解决了刚才出现的问题,但是当newEngine对象和原来的engine对象时同一个对象时,这段代码也会出问题

//Car类中变量engine的存取方法
- (void) setEngine:(Engine *) newEngine{
      [engine release];
      engine = [newEngine retain];
}
//main()中调用
Engine *engine = [Engine new];//count = 1
Car *car1 = [Car new];
Car *car2 = [Car new];

[car1 setEngine:engine];//count = 2
[engine release];//count = 1

[car2 setEngine:[car1 engine]];//oops!

[car1 engine]返回一个指向engine对象的指针,该对象的保留技术器的值为1,setEngine:方法的第一行就是将engine对象的保留技术器归0并释放,现在,newEngine和engine这两个实例变量都指向刚释放的内存区,引发错误

//Car类中变量engine的存取方法
- (void) setEngine:(Engine *) newEngine{
      [newEngine retain];
      [engine realease];
      engine = newEngine;
}
//main()中调用
Engine *engine = [Engine new];//count = 1
Car *car1 = [Car new];
Car *car2 = [Car new];

[car1 setEngine:engine];//count = 2
[engine release];//count = 1

[car2 setEngine:[car1 engine]];//oops!

如果首先保留新的engine对象,即使newEngine与engine是同一个对象,保留技术器的值也将先增加,然后立即减少,由于没有归0,engine对象没有被销毁,就不会引发错误,在访问方法中,如果先保留对象,然后再释放对象就不会错

4.自动释放的引入

观察这个description方法:

//description方法
-(NSString *)description{
       NSString *description;
       description = [[NSString alloc] initWithFormat:"I am %d years old.",4];
       return description;
}
//调用description方法
NSString *desc = [someObject description];
NSLog(@"%@",desc);
[desc release];

在这个例子中,我们用alloc方法创建一个新的字符串实例,然后返回该字符串实例,该字符串对象在调用过程中,使用完毕后就被释放,为了解决如此麻烦的问题,我们引入自动释放

5.自动释放池

Cocoa有一个自动释放池的概念,NSObject有一个叫做autorelease的方法

-(id)autorelease;
  • 该方法预先设定了一条会在未来某个时间发送的release消息,其返回值是接收消息的对象
  • 当给一个对象发送autorelease消息时,实际上是将该对象添加到自动释放池,当自动释放池被销毁时,会向该池中的所有对象发送release消息
//description方法
-(NSString *)description{
       NSString *description;
       description = [[NSString alloc] initWithFormat:"I am %d years old.",4];
       return description;
}
//调用description方法
NSLog(@"%@",[someObject description]);

由于description方法中的字符串对象是自动释放的,该对象暂时被放入了当前活动的自动释池中,等到调用NSLog()函数的代码运行结束后,自动释放池会被自动销毁

6.自动释放池的销毁时间

1.创建自动释放池:

  • 通过@autoreleasepool关键字
  • 通过NSAutoreleasePool对象

2.我们一直使用的Foundationkuto工具集中,创建和销毁自动释放池已经由@autorelease关键字完成
当你在使用@autorelease{}时,所有的花括号里面的代码都会被放入这个新池子里
注意:

  • 任何在花括号里面定义的代码在括号外就无法使用
  • 使用NSAutoreleasePool对象,创建和释放NSAutoreleasePool对象之间的代码就会使用这个新池子
NSAutoreleasePool *pool;
pool = [NSAutoreleasePool new];
···
[pool release];

3.创建一个自动释放池后,该池就会自动成为活动的池子,shifang该池后,保留技术器的值归0,然后被销毁,在销毁的过程中,该池将释放其包含的所有对象

  • 我们推荐使用关键字方法,它比对象方法更快
  • 使用AppKit时,Cocoa定期自动的为你创建和销毁释放池

7.自动释放池的工作流程

//手动管理内存下的程序
#import 
@interface Retaintracker : NSObject
@end//Retaintracker
@implementation Retaintracker
-(id) init{
    if (self = [super init]) {
        NSLog(@"init: Retain count of %ld.",[self retainCount]);
    }
    return  self;
}//init
-(void) dealloc{
    NSLog(@"dealloc called .Bye Bye.");
    [super dealloc];
}//dealloc
@end//Retaintracker

int main(int argc, const char * argv[]) {
    //创建一个自动释放池
    NSAutoreleasePool *pool;
    pool =[[NSAutoreleasePool alloc] init];
    //创建一个新的对象
    Retaintracker *tracker;
    tracker = [Retaintracker new];//count = 1
    //保留该对象,仅仅为了好玩和演示
    [tracker retain];//count = 2
    //每次向对象发送autorelease消息时,该对象都会被添加到这个自动释放池中
    //需要注意的是我们创建的自动释放池中现在有了一个引用指向该对象
    [tracker autorelease];//count = 2
     //当自动释放池被销毁时,将向tracker对象发送一条release消息
    [tracker release];//count = 1
    NSLog(@"releasing pool");
    //销毁自动释放池
    [pool release];

    //使用@autoreleasepool也能达到相同的目的,不过他不需要分配或者销毁自动释放池对象
    @autoreleasepool {
        Retaintracker *trackers;
        trackers = [Retaintracker new];//count = 1
        [trackers retain];//count = 2
        [trackers autorelease];//count = 2
        [trackers release];//count = 1
        NSLog(@"auto releasing pool");
    }
    
    return 0;
}
//运行结果
2018-10-18 12:00:51.894662-0700 newRetainCount[768:25241] init: Retain count of 1.
2018-10-18 12:00:51.894924-0700 newRetainCount[768:25241] releasing pool
2018-10-18 12:00:51.894940-0700 newRetainCount[768:25241] dealloc called .Bye Bye.
2018-10-18 12:00:51.894974-0700 newRetainCount[768:25241] init: Retain count of 1.
2018-10-18 12:00:51.895013-0700 newRetainCount[768:25241] auto releasing pool
2018-10-18 12:00:51.895025-0700 newRetainCount[768:25241] dealloc called .Bye Bye.
Program ended with exit code: 0

二、Cocoa的内存管理规则

规则:

  • 当你在使用new,alloc或copy方法创建一个对象时,该对象的保留计数器的值为1。当不再使用该对象时,你应该向该对象发送一条release或autorelease消息。这样,该对象就将在其使用寿命结束时被销毁
  • 当你通过其他方法获得一个对象时,假设该对象的保留计数器的值为1,而且已经被设置为自动释放,那么你不需要执行任何操作来确保该对象得到清理。如果你打算在一段时间内拥有该对象,则需要保留它并确保操作完成时释放它
  • 如果你保留了那个对象,就需要(最终)释放或自动释放该对象。必须保持retain方法和release方法的使用次数相等

无论什么时候有了一个对象,有两件事必须弄清楚:“怎样获得该对象的?打算拥有多长时间”

1.临时对象

你正在代码中使用某个对象,但是并未打算长期拥有该对象

  • 如果你是用alloc,new或copy方法获得的这个对象,就需要安排好该对象的内存释放
NSMutableArray *array;
array = [[NSMutableArray alloc] init];//count = 1
[array release];//count = 0
  • 如果你是用其他方法获得的这个对象,比如arrayWithCapacity:方法,则不需要关心如何销毁该对象
NSMutableArray *array;
array = [[NSMutableArray arrayWithCapacity:17];//count = 1,autorelease

2.拥有对象

通常,你可能希望在多段代码中一直拥有某个对象。典型的方法是,把它们加入到诸如NSArray或NSDictionary等集合中,作为其他对象的实例变量来使用,或作为全局变量使用(罕见)

如果你使用了alloc,new或copy方法获得了一个对象,则不需要执行任何操作,该对象的保留计数器的值为1,因此它将一直存在,你只需要确保在拥有该对象的dealloc方法中释放它既可

-(void)doStuff{
    flonkArray = [NSMutableArray new];//count = 1
}
-(void)dealloc{
    [flonkArray release];
    [super dealloc];
}

如果你使用alloc,new或copy以外的方法获得一个对象,想要保留该对象,有时候要考虑到事件循环,你需要保留自动释放的对象,以便这些对象在当前事件循环结束以后仍能继续存在

//当使用自动释放对象时,前面的方法可以按如下方式重写
-(void)doStuff{
    flonkArray = [NSMutableArray arrayWithCapacity:17];//count = 1,autoreleased
    [flonkArray retain];//count = 2,1 autorelease
}
-(void)dealloc{
    [flonkArray release];// count = 0
    [super dealloc];
}

在当前时间循环结束或自动释放池被销毁时,flonkArray对象会接受到一条release消息,因此其保留技术器的值从2减到1,因其保留计数器的值大于0,所以该对象继续存在。因此,我们需要在自己的dealloc方法中释放它,以便它被清理

自动清理池被清理的时间是完全确定的:要么是在代码中你自己手动销毁,要么是使用ApppKit时在事件循环结束时销毁

3.垃圾回收

  • OC2.0引入了自动内存管理机制,也称垃圾回收
    对于已经创建和使用的对象,当你忘记清理时,系统会自动识别那些对象仍在使用,那些对象可以回收
  • 启用垃圾回收功能:它是一个可选择是否开启的功能,只要转到项目信息窗口的Build Settings选项卡,并选择Required[-fobjc-gc-only]选项即可(-fobjc-gc选项是针对即支持垃圾回收又支持对象的保留和释放的代码)
  • 启用垃圾回收后,平常的内存管理命令全都变成了空操作指令,也就是不执行任何操作
  • 与自动释放池一样,垃圾回收器也是在事件循环结束时触发的
  • 垃圾回收功能只支持OS X应用开发

4.自动引用计数

  • 为了解决垃圾回收的缺陷,苹果公司产生了一个解决方法被称为自动引用计数(ARC)
    ARC会自动追踪你的对象并决定哪一个仍会使用而哪一个不会再用到,
  • ARC不是垃圾回收器,垃圾回收器在运行时工作,通过返回的代码来定期检查对象。与此相反,ARC是在编译时进行工作的,它帮你在代码中插入了retain和release语句
  • 要想在代码中使用ARC,必须满足以下三个条件:
    1.能够确定那些对象需要进行内存管理:对象的最上层集合知道如何去管理它的子对象
    2.能够表明如何去管理对象:你必须能够对某个对象的保留计数器的值进行加1或减1的操作,也就是说所有的NSSObjectlei的子类都能进行内存管理
    3.有可行的方法传递对象的所有权:在传递对象的时候,你的程序需要能够在调用者和接受者之间传递所有权

关于强引用与弱引用

当用指针指向某个对象时,你可以管理它的内存(通过retain和release),也可以不管理。如果你管理了,就拥有了这个对象的强引用。如果你没有,那么你拥有的则是弱引用





强引用也有自己的_strong关键字和strong特性。需要注意,内存管理的关键字和特性不能一起使用,两者互相排斥

ARC与垃圾回收






![






2.异常

1.异常就是意外事件,比如数组溢出,因为程序不知道怎么处理就会扰乱程序流程

2.当发生这种情况时,程序可以创建一个异常对象,让它在运行时系统中计算出接下来该怎么做

3.Cocoa中使用NSException类来表示异常,Cocoa要求所有的异常必须是NSException类型的异常,虽然你可以通过其他对象来抛出异常,但Cocoa并不会处理它们。此外,你也可以创建NSException的子类来作为你的异常

4.如果想要支持异常特性,请确保-fobj-exceptions项被打开,可以在Xcode中启用Enable Object-C Exceptions

5.在运行时系统中创建并处理异常的行为被称为抛出异常或者说是提出异常。需要注意的是,NSException拥有一些以raise开头的方法名

6.说明:如果一个异常被抛出但是没有被捕捉到,程序会在异常断点处停止运行并通知有这个异常

1.与异常有关的关键字

异常的所有关键字都是以@开头的,以下是它们各自的作用

  • @try:定义用来测试的代码块以决定是否要抛出异常
  • @catch():定义用来处理已抛出异常的代码块,接收一个参数,通常是NSException类型,但也可能是其他类型
  • @finally:定义无论是否有抛出异常都会执行代码块
  • @throw:抛出异常

2.捕捉不同类型的异常

你可以根据需要处理的异常类型使用多个@catch代码块。代码处理应该按照从具体到抽象的顺序排列,并在最后使用一个通用的处理代码

@try{
}@catch(MyCustomException *custom) {
}@catch(NSException *exception) {
}@catch(id value) {
}@finally {
}

说明:C语言程序中经常会在异常代码中使用setjmp和longjmp语句。但是我们不能使用它们来跳出@try语句,但可以使用goto和return语句退出异常处理代码

3.抛出异常

当程序检测到了异常就必须向处理它的代码块(有时叫做异常处理代码)报告这个异常,通知异常的过程被称为抛出异常

程序会创建一个NSException实例来抛出异常,并会使用以下两种技术之一

  • 使用@throw异常名:语句来抛出异常
  • 向某个NSException对象发送raise消息
    例子
//我们创建一个异常
NSException *theException = [NSException exceptionWithName:...];
//要抛出这个异常可以用这个语句
@throw theException;
//或者用
[theException raise];

4.异常也需要内存管理


5.异常和自动释放池


你可能感兴趣的:(第九章、内存管理)