一、内存管理
1、为什么要进行内存管理
由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当APP所占用的内存较多时,系统就会发送内存警告,每个APP可用的内存是被限制的,如果一个APP使用的内存超过20M,则系统会向该APP发送Momory Warning消息,收到此消息后,需要回收一些不需要再继续使用的内存空间,比如回收一些不再使用的对象和变量等,否则程序会崩溃。
2、OC内存管理的范围
管理范围:
管理继承NSObject的对象,对其他的基本数据类型无效。
分析原因:
本质原因是因为对象 和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。
小结:
1>对象存储于 堆中
2>其他局部变量主要存放于 栈中
存储于栈中的局部变量的特性:当代码块结束时这个代码块中的所有局部变量会被 回收(指向对象的指针也被回收)
堆的存储特性:堆的管理是要 程序员来管理,系统自己不会管理
各个内存区如下:
2、内存管理的原理
1)对象的所有权及引用计数
1>对象所有权概念
如何对象 都可能拥有一个或多个所有者,如果一个对象至少还拥有一个所有者,它(对象)就会继续存在
2>Cocoa所有权策略
任何自己创建的 对象 都归自己所有,可以使用名字以“alloc”或“new”开头或名字中包含“copy”的方法创建对象,可以使用retain来获得一个对象的所有权
2)对象的引用计数器
每个OC对象都有自己的 引用计数器,是一个整数 表示对象被引用的次数,即现在有多少东西在使用这个 对象,对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。
在每个OC对象内部,都专门有 8个字节 的存储空间来存储引用计数器
3)引用计数器的作用
引用计数器是判断 对象 是否要回收的依据,就是当引用计数器从1变为0时,就回收(或引用计数器为0)。
注意:存在一种例外:对象为nil时,引用计数器为0,但不收回空间
4)对引用计数器的操作
给对象发送消息,进行相应的计数器操作
retain消息:使计数器+1,该方法返回对象本身
release消息:使计数器-1,(并不代表释放对象)
retainCount消息:获得对象当前的引用计数器值(占位符为:%ld 或 %lu)
5)对象的销毁
当一个对象的引用计数器为0时,那么它将被销毁,其占用的内存被系统收回。
当对象被销毁时,系统会自动向对象发送一条dealloc消息,一般会重写dealloc方法,在这里释放相关的资源,dealloc就像是对象的“临终遗言”
一旦重写了dealloc方法局必须调用[super dealloc],并且放在代码块的最后调用(不能直接调用dealloc方法)。
一旦对象被回收了,那么他所占据的存储空间就不再可用,坚持使用会导致程序崩溃(野指针错误)
注意:
1>如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经推出)
2>任何一个对象,刚生下来的时候,引用计数器都为1。(对象一旦创建号,默认引用计数器就是1)当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1
3、OC内存管理分类
Objective-C提供了三种内存管理方式:
MannulReference Counting(MRC 手动管理,在开发 iOS4.1 之前的版本的项目时我们要自己负责使用引用计数来管理内存,比如要手动 retain、release、autorelease等,而在其后的版本可以使用ARC,让系统自己管理内存)
automatic reference counting(ARC 自动引用计数,iOS4.1之后推出的)
garbage collection (垃圾回收)。iOS不支持垃圾回收
ARC作为苹果判断提供的技术,苹果推荐开发者使用ARC技术来管理内存:
开发中如何使用:需要理解MRC,但实际使用时尽量用ARC
二、手动内存管理
内存管理的关键如何判断对象被回收?
重写dealloc方法,代码规范
(1)一定要[super dealloc],而且要放到最后
意义是:先释放子类占用的空间,再释放父类占用的空间
(2)对self(当前)所拥有的其他对象做一次release操作
例如:
-(void) dealloc
{
[_car release];
[super dealloc];
}
注意:
永远不要直接通过对象调用dealloc方法(实际上调用并不会出错)
一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)为了防止调用出错,可以将“野指针”指向nil(0)。
下面我们通过实例程序的演示来体验 retain、release、dealloc
Person.h
Person.m
main.m
打印输出:
总结:
1>对象刚创建时,拥有者有一个就是自己,retainCount=1;
2>对象每调用一次retain,拥有者+1retainCount-1
3>对象每调用一次release,拥有者-1retainCount-1
4>dealloc方法要重写
5>当retainCount=0时,会自动调用 dealloc方法
2、内存管理的原则
1)原则:
只要还有人在使用某个对象,那么这个对象就不会被回收
只要你想使用这个对象,那么就应该让这个对象的引用计数器+1
当你不想使用这个对象时,应该让对象的引用计数器-1
2)谁创建对象,谁release(创建对象和release是成对出现)
(1)如果你通过alloc、new、copy来创建了一个对象,那么你就必须调用 release或者autorelease方法
(2)不是你创建的就不用你去负责释放
3)谁retain,谁release(retain和release是成对出现)
只要 你调用了retain,无论这个对象是如何生成的,你都要调用release
4)总结
有始有终,有加就应该有减。曾经让某个对象计算器加1,就应该让其在最后-1
三、手动内存管理之——单个对象内存管理
1、单个对象的野指针问题
思考一个问题:
对象在堆区的空间已经释放了,还能 再使用p1?
1)野指针错误:访问了一块坏的内存(已经被回收的,不可用的内存)
2)僵尸对象:所占内存已经被回收的对象,僵尸对象不能再被使用。(默认情况下Xcode为了提高编码效率,不会时时检查僵尸对象,打开僵尸对象检测)
注意:
1>空指针:没有指向任何东西的指针,给空指针发送消息不会报错
关于nil和Nil及NULL的区别
nil:A null pointer to an Objective-CObject.
nil 是一个对象值
Nil:A null pointer to an Objective-Cclass.
NULL:A null pointer to anything else.
NULL 是一个通用指针(泛型指针)
2>不能使用[释放对象 retain]让僵尸对象起死复生
3>野指针操作
下面通过实例程序来演示
Dog.h
Dog.m
main.m
打印输出:
小结:
通过上面的打印可以得到以下总结:
1>遵守在手动内存管理中的原则:
> 谁创建,谁release
> 谁retain,谁release
下面我们再通过实验来体验一下是否可以使用 僵尸对象
Dog.h和Dog.m和上面一样:
main.m
打印输出:
小结:
1》当程序运行到最后的一个[dog release]时,存储在堆区的对象dog([Dog new])的存储空间已经释放,但是存储在栈区的指针dog、jd 并没有释放,所以用dog、jd来方法run方法、retain方法时会报错,以为指针dog、jd指向的对象空间已经不在,这些操作是违法的是在使用僵尸对象
2》当在[dog release]以后,把指针dog、jd重新赋值来指向nil,在调用run方法和retain时并不会报错,这也是nil的一种用法,但是不推荐这样使用。
注意:3》如果dog、jd重新赋值指向nil是放在[dog release]以前,那就会造成内存的泄露!
2、避免使用僵尸对象的方法
因为如果程序较为复杂时,我们也不清楚执行哪一个release时,retainCount变为0,所以就会造成使用僵尸对象
下面我们通过实例程序演示看怎样使用nil来避免使用僵尸对象
Person.h
Person.m
main.m
打印输出:
小结:
从程序可以看出在[person release]后直接跟上一句person=nil后再继续调用run方法、retain、时不会报错。
3、对象的内存泄露
原因有:
1)没有按照内存规则来做,而造成retain和release 个数不配配,导致内存泄露
2)对象使用的过程中被赋值了nil,导致内存泄露
3)在函数或者方法中不当的使用retain或者release造成的问题
下面通过实例程序来体验上面的内存泄露情况:
1)没有按照内存规则来做,而造成retain和release 个数不配配,导致内存泄露
Person.h
Person.m
main.m
打印输出:
小结:
因为retainCount不等于0,所以不会打印dealloc中的内容
2)对象使用的过程中被赋值了nil,导致内存泄露
Person.h
Person.m
main.m
打印输出:
小结:
本来就是使用nil不当,造成内存泄露
3)在函数或者方法中不当的使用retain或者release造成的问题
Person.h
Person.m
main.m
打印输出:
小结:
从main.m来看,程序是符合程序内存管理原则的,谁创建,谁release;最后一句
NSLog(@"\n2——%lu",[personretainCount]);本应该会因为调用僵尸对象而报错,但是也没有报错。
四、手动内存管理之——多个对象内存管理
1、本节我们讨论的问题是:WR开着他的BMW汽车去兜风
分析这句话:有两个类:人:WR、汽车:BMW
实例程序演示:
Car.h
Car.m
Person.h
Person.m
main.m
打印输出:
小结:
1>上面的程序就是我们在不考虑内存泄露问题时,我们一般会这样写程序,因为在前面我们就是这样写的
2>我们从打印输出可以看出Person、Car的引用计数器是1,所以说明这样写程序造成了“内存泄露”
我们已经知道不能让程序的内存泄露,所以下面我们将讨论如何管理“多个对象的内存管理”
实例程序演示如下:
Car.h
Car.m
Person.h
Person.m
main.m
打印输出:
小结:
从打印输出可以看出:程序没有造成内存泄露
但是该程序并不完善,是有缺陷的,比如改一下程序就会造成“内存泄露”
Car、Person类不变
main.m改变如下:
打印输出:
小结:
上面的程序是脆弱的,当我们在[car release]之后增加调用driver方法后,这里程序就造成了内存泄露,因为WR没有释放,所以我们在WR释放前调用该对象方法是合理的,只能说该程序太“脆弱”
分析上面的问题:
上面程序的脆弱就是因为在调用driver方法的时候,Car已经销毁了,所以:程序最好是在Person没有销毁之前Car一直存在,也就是Car的引用计数器CarRetainCount=1
下面通过实例程序来演示
Car.h
Car.m
Person.h
Person.m
main.m
打印输出:
小结:
1>在我们的main.m中还是遵守内存管理的原则:
谁创建,谁release
2>这次只要Person没有销毁前可以任意的调用 driver方法,并且没有造成内存泄露
五、set方法内存管理
1、set方法存在的问题
1)set方法写法(一)
原对象无法释放造成的内存泄露
2)set方法写法(二)
原对象讷讷公狗释放,但是引起新的问题,set自己的时候,造成野指针
3)set方法写法(三)
判断新传入的对象是否是原来的对象,如果不是原来的对象则 释放,然后再retain
下面通过实例程序来演示
1)set方法写法(一)
原对象无法释放造成的内存泄露
对于第一个set方法写法的解决方法在上面程序中已经解决了
小结:
上面的程序也只适合给人传送一次车,但给人多次传送时,必定造成内存泄漏
2)set方法写法(二)
原对象能够释放,但是引起新的问题,set自己的时候,造成野指针
下面通过实例程序演示
Car.h
Car.m
Person.h
Person.m
main.m
打印输出:
小结:
1>从这个打印输出来看,发现BMW的引用计数器等于1,直到最后也没有变为0,所以造成了内存泄露
2>在main.m中依然遵守内存管理原则,谁创建,谁release
下面我们通过修改程序来改进
分析:当我们第二次调用 setCar方法时,让前一个传来的car销毁,然后让新传来的car retain就可以了
Car.h
Car.m
Person.h
Person.m
main.m
打印输出
其实这个程序还是有些“脆弱”,当程序第二次传入的car还是第一传入的car时,就会报错
下面通过实例演示来体验一下:
Car、Person类中的程序和上面的一样
main.m
打印输出
小结:
1>从这个打印输出来看,程序报错了,而此程序和上一个程序例子的不同仅仅是因为本程序第二次调用的setCar方法是传入的car和第一次传入的car是一样的。
2>分析报错的原因
总结:因为我们二次调用setCar方法所传入的car是相同的BMW
(1)当第二次调用setCar方法在[BMW release]前面,那么不会有任何问题
原因:当我们新建BMW时BMWCount=1,
当第一次调用setCar方法时
-(void)setCar:(Car *)car
{
[_carrelease]; 此时的_car=nil,所以[_car release]
等于[nil release]
_car=[carretain];此时_car=[BMW retain]
BMWCount=2
}
当第二次调用setCar方法时
-(void)setCar:(Car *)car
{
[_car release]; 此时的_car=BMW,所以[_car release]
等于[BMW release],BMWCount=1
_car=[car retain];此时_car=[BMW retain]
BMWCount=2
}
当执行[BMW release]后BMWCount=1
当执行[WR release]后BMWCount=0
所以不会造成内存泄露
(2)当第二次调用setCar方法在[BMW release]后面,就会发生报错
原因:当我们新建BMW时BMWCount=1,
当第一次调用setCar方法时
-(void)setCar:(Car *)car
{
[_car release]; 此时的_car=nil,所以[_car release]
等于[nil release]
_car=[car retain];此时_car=[BMW retain]
BMWCount=2
}
当执行[BMW release]后BMWCount=1
当第二次调用setCar方法时
-(void)setCar:(Car *)car
{
[_car release]; 此时的_car=BMW,所以[_car release]
等于[BMW release],BMWCount=0
_car=[car retain];此时_car=[BMW retain]就是使用
僵尸对象,所以会 报错
}
下面我们通过分析来改进程序:
报错原因就是因为我们二次传入的对象都是同一个对象而造成的,假如程序检查到是同一个对象时就不再去执行
[_carrelease];
_car=[carretain];
就不会造成使用僵尸对象的错误
下面我们通过实例程序来演示:
Car.h
Car.m
Person.h
Person.m
main.m
打印输出:
小结:
从打印输出来看Person、Car类都销毁了,没有造成内存泄露,
说明这样的set写法是没有问题的
3、set方法的内存管理
1)基本数据类型格式:直接赋值
int float doublelongstructenum
-(void)setAge:(int)age
{
_age=age;
}
2)OC对象类型格式
-(void)setCar:(Car *)car
{
//1.先判断是不是新传进来的对象
//判断新传来的car是否和旧值(原来的)_car不同
if(_car!=car)
{
//2.对旧对象做一次release
//让旧值(原来的)_car release释放掉
[_carrelease];//如没有旧对象,则没有影响
//3、对新对象做一次retain
//再让新传来来的car retain过后赋值给旧值_car
_car=[carretain];
}
}
1、如果在一个类中,有其他类的对象(关联关系)
2、set方法书写的时候,要遵守:
判断是否是同一个对象,release 旧值 , retain 新值