9、内存管理

概述

Objective-C为每个对象提供一个内部计数器,这个计数器 跟踪对象的引用次数。所有类都继承自 NSObject 的对象。
这里写图片描述

Cocoa中提供了一个机制来实现上面提到的这个逻辑模型,它被称为“引用计 数”(reference counting)或“保留计数”(retain counting) 。引用计数的 数值表示对象有几个“人”在使用它。
·每一个对象都拥有一个引用计数(retain count)
·当对象被创建的时候,引用计数的值是1
·当发送retain消息时,该对象的引用计数加1,该对象的引用计数为2
·当向这个对象发送release消息时,该对象的引用计数减1
·当一个对象的引用计数为0时,系统自动调用dealloc方法,销毁该对象
这里写图片描述

Person *person = [[Person alloc] init]; // 对象被创建时,引 计数为1 
NSLog(@"person 1: %ld", [person retainCount]); 
[person retain]; // 向对象发送retain消息,引 计数为2 
NSLog(@"person 2: %ld", [person retainCount]); 
[person retain]; // 向对象发送retain消息,引 计数为3 
NSLog(@"person 3: %ld", [person retainCount]); 
[person release]; // 向对象发送release消息,引 计数减1,此时引 计数为2 
NSLog(@"person release 1: %ld", [person retainCount]); 
[person release]; // 向对象发送release消息,引 计数再次减1,此时引 计数为1 
NSLog(@"person release 2: %ld", [person retainCount]);
[person release]; // 引 计数为0,系统将会 动调 dealloc 法,销毁对象,回收内存 

对象所有权的基本概念

当一个所有者(owner,其本身可以是任何一个Objective-C对象)做了以下某 个动作时,它就拥有了对一个对象的所有权(ownership)。
·如果创建或者复制某个对象时,则拥有了该对象的所有权
···alloc,allocWithZone:,copy,copyWithZone:,mutableCopy,mutableCopyWithZone:
·如果没有创建对象,而是将对象保留使用,同样拥有该对象的所有权
···retain
·如果你拥有了某个对象的所有权,在不需要某一个对象时,需要释放它们
···release, autorelease

该如何持有对象

我们就以便携电脑和CPU为例。假设有这么一个Laptop类、CPU类,我们在 Laptop类中添加一个设置CPU的方法。

CPU *powerPC = [[CPU alloc] init]; 
Laptop *apple = [[Laptop alloc] init]; // main函数中
[apple setCPU:powerPC]; 
- (void)setCPU:(CPU *)cpu {
    _cpu = cpu;
} // Laptop类.m 件中 

CPU的对象的所有权是powerPC它自己,而apple实例并不拥有powerPC对象的所有权,现在CPU的引用计数是1,即表示只有一个对象在使用它。

(1)假设在main函数主程序中,不小心向powerPC实例(对象)发送了release消息,即powerPC实例销毁了,但apple实例可能仍然在某个地方在使用powerPC实例,那么你的程序就会crash掉。

因此,我们需要给powerPC对象进行retain,使得apple的_cpu实例拥有 该对象的所有权,我们通常这么做:

CPU *powerPC = [[CPU alloc] init];
Laptop *apple = [[Laptop alloc]init];
[apple setCPU:powerPC];
[powerPC release];
- (void)setCPU:(CPU *)cpu
{
   _cpu = [cpu retain]; 
} // Laptop类.m 件中 

在main主函数中向CPU这个实例发送release消息,并没有使他真正的消亡,因为在setCPU:方法中向它发送了retain消息,powerPC实例的引用计数变为2,再向CPU发送release消息,使得CPU实例引用计数由2降至1(powerPC放弃了对象所有权)。powerPC实例的对象所有权给了_cpu这 个实例,因此由apple这个实例负责释放。

2)我们知道2005年后,苹果的CPU转向了intel的怀抱,因此,我们需要将CPU改为intel的CPU,如下所示:

CPU *powerPC = [[CPU alloc] init];
Laptop *apple = [[Laptop alloc]init];
[apple setCPU:powerPC];
[powerPC release];
CPU *intel = [[CPU alloc] init];
[apple setCPU:intel];
[intel release];
- (void)setCPU:(CPU *)cpu{
    _cpu = [cpu retain]; 
} // Laptop类.m 件中 

a、首先,我们初始化了三个实例对象,分别是(powerPC、intel和 apple)。apple首先首先用的IBM的CPU——powerPC(引用技术为2)

b、将powerPC这个实例release掉,引用计数为1(注意这里) c、然后,apple更换了自己的CPU。他持有了intel这个实例 d、在此之前的_cpu对象是powerPC!powerPC实例没有消失,但是没有 任何实例指向他了,于是内存泄露!(_cpu指向的是intel)

因此,我们需要给setCPU:方法加以修改,代码如下所示:

- (void)setCPU:(CPU *)cpu{
    [_cpu release];
    _cpu = [cpu retain]; 
} // Laptop类.m 件中 

a、apple在设置第一个CPU(powerPC)时,调用了setCPU:方法,执 行[_cpu release]方法时,此时apple没有CPU,_cpu对象为nil。我们知道 可以向nil发送消息,程序不会抛出异常,其结果是什么也不做。
b、apple持有了powerPC对象后,2005年改用了intel,于是调用 setCPU:方法。此时_cpu实例是powerPC,向_powerPC发送release消 息,即释放了之前的对象,然后保留新的对象,即intel。

(3)还有这样的一种问题,或者说我们需要优化、改进我们的代码

Laptop *apple = [[Laptop alloc]init];
CPU *intel = [[CPU alloc] init];
[apple setCPU:intel];
[intel release];
[apple setCPU:intel];

a、当旧对象和新的对象不同时,这里我们会释放掉旧对象,持有(保留) 新对象。当两者为同一个对象时,问题又出现了。

b、第二次执行[_cpu release]方法时,_cpu实例都是intel这个实例。因此,程序容易出现bug。

因此,我们需要给setCPU:方法再加以改进,代码如下所示:

- (void)setCPU:(CPU *)cpu {
    if (_cpu != cpu) {
        [_cpu release];
        _cpu = [cpu retain];
    }
} // Laptop类.m 件中 

a、当旧对象和新的对象(_cpu)不同时,会释放掉旧对象,保留新对象。

b、而当两个对象相同时,不会做任何事情(设置电脑的cpu)。

小结

初始化方法

直接向对象发送retain消 息。持有对象所有权。并在 dealloc方法释放改对象;

- (id)initWithEngine:(id)engine {
    self = [super init];
    if (self) {
        _engine = [engine retain];
    }
    return self;
}

设置方法

直接赋值,不保留对象;
直接保留对象,在dealloc方法中释放对象;
释放旧对象,保留新对象, 在dealloc方法中释放对象;

// 第一种形式
/*
- (void)setCpu:(Cpu *)cpu
{
    _cpu = cpu;
}
*/

// 第二种形式
/*
- (void)setCpu:(Cpu *)cpu
{
    _cpu = [cpu retain];
}
*/

// 第三种形式
/*
- (void)setCpu:(Cpu *)cpu
{
    [_cpu release];
    _cpu = [cpu retain];
}
*/

// 第四种形式
/*
- (void)setCpu:(Cpu *)cpu
{
    if (_cpu != cpu) { 

        [_cpu release];
        _cpu = [cpu retain];
    }
}
*/

// 第五种形式
/*
- (void)setCpu:(Cpu *)cpu
{
    [cpu retain];
    [_cpu release];
    _cpu = cpu;
}
*/

dealloc方法

在之前的学习中,我们已经接触了dealloc方法。它的作用是,当对象的引用计数为0,系统会自动调用dealloc方法,回收内存。它的一般写法:

- (void)dealloc
{
    [super dealloc];
}
- (void)dealloc
{
    [_cpu release];
    [super dealloc];
}

·什么需要调用父类的dealloc方法

子类的某些实例是继承自父类的。因此,我们需要调用父类的dealloc方法,来释放父类拥有的这些对象。

·调用的顺序

一般来说调用的顺序是,当子类的对象释放完时,然后再释放父类的所拥有的实例。这一点与调用初始化方法,正好相反。

点语法的内存管理

赋值

·assign:直接赋值,默认
·retain:保留对象
·copy:拷贝对象

读写性

·readwrite:生成getter、 setter方法,默认
·readonly:生成getter方法

原子性

·atomic:多线程环境下, 存在线程保护,默认
·nonatomic:多线程环境 下,不存在线程保护

assign、retain与copy的区别

·assign 直接赋值,只是一个别名而已。
·retain 保留的这个对象,两个对象指向了同一个位置。
·copy 开辟了一个新的内存空间,分别指向了不同的内存位置。
·与之前的对象完全脱离关系。这里我们尤其需要注意,某些时候copy的作用相当于retain,我们将在复制对象再一次讨论这个问题。

assign 参数

assign参数代表设置时候直接赋值,而不是复制或者保留它 。这种机制非常适合一些基本类型,比如NSInteger和CGFloat,或者就是不想直接拥有的类型,比如委托。assign相当于如下写法。

- (void)setTitle:(NSString *)newTitle {
       title = newTitle;
}

retain参数

retain参数会在赋值时把新值保留(发送retain消息)。此属性只能用于Objective-C对象类型,而不能用于基本数据类型或者Core Foundation。retain相当于如下写法:

(void)setTitle:(NSString *)newTitle {
    [newTitle retain];
    [title release];
    title = [[NSString alloc] initWithString: newTitle];
}

copy参数

copy在赋值时将新值拷贝一份,拷贝工作由copy方法执行, 此属性只对那些实行了NSCopying协议的对象类型有效。copy相当于如下写法:

- (void)setTitle:(NSString *)newTitle {
       [newTitle copy];
       [title release];
       title = [[NSString alloc] initWithString: newTitle];
}

自动释放池

内存释放池(Autorelease pool)提供了一个对象容器,每 次对象发送autorelease消息时,对象的引用计数并不真正变化,而是向内存释放池中添加一条记录,记下对象的这种 要求,直到当内存释放池发送drain或release消息时,当池被销毁前会通知池中的所有对象,全部发送release消息真正将引用计数减少。

这些语句必须要放在下面语句之间,直到池被释放,一个 对象要想纳入内存释放池对象,必须要发送autorelease。

/***********池对象 5.0之后的写法 *************/
@autoreleasepool {
    ....
} 

/***********池对象 5.0之前的写法 *************/
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    ....
[pool release]; 

实例

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[]) {

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSArray *weeksNames1 = [NSArray arrayWithObjects: 
                           @"星期一",@"星期二",@"星期三",@"星期四"
                           ,@"星期五",@"星期六",@"星期日",nil];

    NSArray *weeksNames2 = [[NSArray alloc] initWithObjects: 
                           @"星期一",@"星期二",@"星期三",@"星期四"
                           ,@"星期五",@"星期六",@"星期日",nil];

    //[weeksNames1 release];
    //[weeksNames1 autorelease];
    //[weeksNames2 release];
     //[weeksNames2 autorelease];

    NSLog(@" retain count: %i" , [weeksNames1 retainCount] );
    NSLog(@" retain count: %i" , [weeksNames2 retainCount] );

   [pool release];
    return 0;   
}

说明

NSArray类是Foundation框架提供的不可变数组类, Foundation框架中对象的创 建有两类方法:类方法(+)构造方法和实例方法(-)构 造方法。打开NSArray Class Reference文档,其中创建对象有关的方法如下所示。

//creating a object
+ (instancetype)array;
+ (instancetype)arrayWithObject:(ObjectType)anObject;
+ (instancetype)arrayWithObjects:(const ObjectType [])objects count:(NSUInteger)cnt;
+ (instancetype)arrayWithObjects:(ObjectType)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
+ (instancetype)arrayWithArray:(NSArray<ObjectType> *)array;
+ (nullable NSArray<ObjectType> *)arrayWithContentsOfFile:(NSString *)path;
+ (nullable NSArray<ObjectType> *)arrayWithContentsOfURL:(NSURL *)url;

//initing a object
- (instancetype)initWithObjects:(ObjectType)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
- (instancetype)initWithArray:(NSArray<ObjectType> *)array;
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
- (nullable NSArray<ObjectType> *)initWithContentsOfFile:(NSString *)path;
- (nullable NSArray<ObjectType> *)initWithContentsOfURL:(NSURL *)url;

从NSArray Class Reference文档可以看出,以+和类名开头(去掉NS,小写第1个字母,array),就是类级构造方法, 以-和initWith开头的就是实例构造方法。

实例构造方法,如果发出release消息就马上释放对象,如 果发出autorelease消息可以自动纳入内存释放池管理,不 会马上释放。

在iOS开发中由于内存相对少,因此基本上都采用实例构造方法实例化对象,采用发送release消息立刻释放对象内存。

自动释放池小结

·自动释放池的数据结构

自动释放池是以栈的形式实现,当你创建一个新的自动释放池时,它将会被添加到栈顶。接受autorelease消息的对象将被放入栈顶。

·再谈我们该如何持有对象

当我们使用alloc、copy、retain获得一个对象时,我们需要负责显式的安排对象的死亡,其他方法获得对象将交由自动释放池释放(单例除外)。

·release与drain的区别

当我们向自动释放池pool发送release消息,它会向池中临时对象发送一条release消息,并且自身也被销毁。向它发送drain消息时,只会执行前者。

·再谈自动释放池被销毁的时间

当我们使用AppKit创建工程时,程序会自动的创建自动创建和排空自动释放池的临时对象,通常是在一个事件循环中创建,结束时排空。

ARC和垃圾回收机制的基本概念

·ARC技术的基本概念
ARC自动引用技术(automatic reference counting),当你在编译程序
时提供自动管理内存的功能,它会为自动加入内存的控制代码,控制对象的 生命周期。如此一来,大大简化的内存管理步骤。注意版本的支持是在 iOS4(不支持弱引用)、iOS5上。
·垃圾回收机制的基本概念
与Java、.net语言相同objective c2.0以后,也提供了垃圾回收机制。但在iOS
移动终端设备中,并不支持垃圾回收机制(取决于终端设备的性能)。因此, iPhone并不能对内存进行自动垃圾回收处理(中间模式autorelease)。我们需要 注意垃圾回收机制并不是ARC,ARC也是需要管理内存的,只不过是隐式的管理 内存,编译器会在适当的地方自动插入retain、release和autorelease消息。

内存管理的常见错误

自定义类方法
// 错误的写法
+ (id)person
{
Person *person = [[self alloc]init];
[person release];
return person;
}
// 正确的写法
+ (id)person {
Person *person = [[self alloc]init];
return [person autorelease];
}

框架中的类方法
// 错误的写法
+ (void)description {
NSString *name = [NSString stringWithFormat:@"jack"];
[name release];
NSLog(@"name : %@", name);
}

// 正确的写法
+ (void)description
{
NSString *name = [NSString stringWithFormat:@"jack"];
NSLog(@"name : %@", name);
}

数组中的内存管理
使用类方法

// 错误的写法
Laptop *laptop = [[Laptop alloc] init]; laptop.name = [NSString stringWithFormat:@"apple"]; NSArray *array = [NSArray arrayWithObject:laptop]; [array release];

// 正确的写法
Laptop *laptop = [[Laptop alloc] init]; laptop.name = [NSString stringWithFormat:@"apple"]; NSArray *array = [NSArray arrayWithObject:laptop]; [laptop release];

laptop实例由alloc创建,我们有必要显式的将它释放,而数组的创建则相 反,它由自动释放池来管理它的生命周期。此外,当我们将laptop实例放入 数组时,实际上laptop实例的引用计数为2,因为数组也在使用它,对它进行 retain操作。

使用实例方法

// 错误的写法
Laptop *laptop = [[Laptop alloc] init];
laptop.name = [NSString stringWithFormat:@"apple"];
NSArray *array = [[NSArray alloc] initWithObjects:laptop, nil]; [laptop release];
// 正确的写法
Laptop *laptop = [[Laptop alloc] init];
laptop.name = [NSString stringWithFormat:@"apple"];
NSArray *array = [[NSArray alloc] initWithObjects:laptop, nil]; [laptop release];
[array release];

数组由alloc方法获得,因此,我们需要显示的将它释放,当数组释放时,它会向数组中的所有对象发送release消息。以此释放它们。

内存管理的经典问题(循环引用)

对象A retain对象B。同时对象B retain对象A。这时两个对象A和B都没有办法得到释放。这种情况我们称之为循环引用:

- (void)setA:(A *)a
{
    if (_a != a) {
        [_a release];
        _a = [a retain];
    }
}
- (void)dealloc
{
    [_a release];
    [super dealloc];
}
- (void)setB:(B *)b
{
    if (_b != b) {
        [_b release];
        _b = [b retain];
    }
}
- (void)dealloc
{
    [_b release];
    [super dealloc];
}

这时候这两个类随便那个这样改写就能解决

- (void)setB:(B *)b
{
   _b = [b retain];
}
- (void)dealloc
{
    [super dealloc];
}

内存管理总结

·内存管理总结

·当你使用new, alloc或copy方法创建一个对象时,该对象的引用计数器为1。当不再使用该对象时,你要负责向该对象发送一条release或者autorelease消息,这样,该对象将在其使用寿命结束时被销毁
·你通过任何其他方法获得一个对象时,则假设该对象的引用计数为1,而且已经被设置为自动释放,你不需要执行任何方法来释放该对象。如果你打算在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它。
·如果你保留了某个对象,你需要释放或自动释放该对象,必须保持retain方法和 release方法的使用次数相等。
·除了alloc、new或copy之外的方法创建的对象都被声明了autorelease。谁retain, 谁release。只要你调用了retain,无论这个对象是如何生成的,你都要调用 release。有时候你的代码中明明没有retain

大道至简

· 如果创建一个对象使用了alloc、copy[mutable]、retain,那么你就有义务向这个对象发送一条release或者autorelease消息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值