内存管理的必要性
由于从早期开始, 手机由于工业和大小的限制, 在手机上的内存是有限制的. 从早期的512k开始, 到1GB, 2GB乃至4GB
但是手机上的应用数不胜数. 所以这需要手机的操作系统给一个应用分配一个空间, 如iPhone 5S给一个程序分配的内存是80M. 而像图片 音频 视频等资源是极其耗内存(如一张高清图片4M左右)的. 如果不对手机内存加以管理, 可能导致80M分分钟被占满, 然后本内存被操作系统强制回收, 然后应用闪退.
内存管理的方法
垃圾回收(gc) 轮询延迟
MRC (人工计数)
ARC (自动计数)
垃圾回收
程序员只需要开辟内存空间,不需要⽤代码显⽰地释 放,系统来判断哪些空间不再被使⽤,并回收这些内存空间,以便再 次分配。整个回收的过程不需要写任何代码,由系统⾃动完成垃圾回 收。Java开发中⼀直使⽤的就是垃圾回收技术。
⼈⼯引⽤计数
内存的开辟和释放都由 程序代码进⾏控制。相对垃圾回收来说,对内存的控制更加灵活,可 以在需要释放的时候及时释放,对程序员的要求较⾼,程序员要熟悉 内存管理的机制。
iOS 5.0的编译器特性,它 允许⽤户只开辟空间,不⽤去释放空间。它不是垃圾回收!它的本质 还是MRC,只是编译器帮程序员默认加了释放的代码
在mac os上, 内存管理是用垃圾回收机制的. 而ios开发上是用引用计数管理.
iOS⽀持两种内存管理⽅式:ARC和MRC。 MRC的内存管理机制是:引⽤计数。 ARC是基于MRC的。
什么是引用计数
引用计数是OC中一种计算对象被引用的数量的机制.
OC中, 每个对象在创建的时候(也就是在堆中开辟了空间)的时候, 在对象所在的空间里会自动开辟一块4个字节的空间, 值为1, 保存的是一个整数. 当一个指针(对象名)指向本块空间的时候, 计数 +1, 指针不再指向这个空间的时候, 计数 -1. 当计数 = 0的时候, 系统就会调用dealloc方法对本块空间进行回收.
在MRC中, 计数的增 和 减操作都需要程序员用代码实现的. 这要求程序员对内存管理操作十分熟悉. 对程序员的管理内存水准要求比较高.
影响引用计数的方法
+alloc // 具有空间清零功能, 将引用计数置为1
-retain // 计数 + 1
-copy // 计数 不定
-release // 计数 - 1
-autorelease // 计数 - 1, 但是当前不执行, 而是在调用本方法的对象出了自动释放池作用域的时候执行
程序员在使用这些方法的时候, 注意alloc, retain, copy都要各自对应一个release
但是release不能过度使用, 当引用计数已经被减至0的时候, 对象就会被系统自动调用dealloc方法进行回收了. 此时再调用指向被回收对象的指针调用release, 会导致崩溃.
查看当前的引用计数的可以通过 retainCount 来查看.
先引入几个概念
- 野指针, 当对象的空间计数变成0的时候, 仍然指向被回收的空间时就称为野指针.
- 空指针, 当指针指向nil的时候, 就称该指针为空指针.
- 僵尸对象, 当对象的空间计数变成0的时候, 该空间被系统回收, 但是里面的值并没有被删除, 这样的对象叫僵尸对象.
- 内存泄漏, 一个对象没有任何指针指向它, 但是它的空间计数依然大于0, 而没有被回收. 这就叫内存泄漏.
- 内存溢出, 对象太多, 没有及时释放, 导致最后内存空间被占满, 最后内存被强制回收, 就叫做内存溢出.
野指针的危害
其中野指针的危害性是非常大的, 因为它指向的正是僵尸空间, 如果没有及时的转化为空指针, 可能在某个对象创建的时候, 正好覆盖了这块空间. 那么此时野指针就会引爆各种错误. 或程序崩溃, 或数据异常.
由于OC中是没有空指针错误的, 使用空指针调用方法, 结果就是什么都不执行, 相当于没有此语句.
僵尸对象, 是空间被回收的对象, 它的空间会被标记为可以覆盖. 未来的某个时候, 它的空间会被其他对象覆盖.
p.s 野指针调用retainCount的到的结果也是1, 所以程序员需要在写程序的时候特别注意, 否则就会分辨不清当前的指针是否是野指针. 空指针的调用retainCount的结果为0.
autorelease
autorelease需要依托自动释放池完成计数-1的操作. 只有出了自动释放池才会完成它的作用. 虽然最后的结果和release相同都是对象都能被回收, 但是不建议大量的使用autorelese, 因为很有可能在未出自动释放池的时候就已经把内存占满而崩溃了.
注意, 如果在出自动释放池之前, 已经用relese语句把对象释放了, 在出自动释放池的时候会因为又执行了autorelease语句的计数器-1功能, 导致野指针错误.
- 便利构造器中autorelease的使用
- (instancetype)personWithName:(NSString *)name
age:(NSInteger)age {
Person *p = [[Person alloc] initWithName:name age:age];
return [p autorelease]; // 本着一个alloc对应一个release原则. 还需要使用对象p, 所以用autorelease
}
注: 为了逻辑上的完整性, 除了便利构造器 和 get方法, 其他的方法不建议这么写. 其他的方法比如copyWithZone 自定义初始化方法, 这些在main函数中可以看到alloc copy的方法, 通常在main函数中用对应次数的release来进行一对一的释放. 这样形成一个规范, 除了便利构造器 和get方法 其他能在其他函数中见到alloc copy retain 的方法都在外部释放.
- 自动释放池
写法1:
1.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2.[pool release];
语句1和语句2相当于一对花括号, 写在两句语句中间的语句, 默认是写在自动释放池pool中. 不需要通过其他的语句进行添加.写法2:
@autoreleasepool { ...; }
是对写法一的优化, 在花括号中间写的语句, 就是写在自动释放池中.
dealloc方法
在本类中重写dealloc方法, 必须在dealloc方法里加上一句[super dealloc];
并且这句写在最后一句
因为[super dealloc]把对象释放掉了, 在它后面的再调用对象方法, 会出现野指针问题. 为了避免给自己找不痛快, 建议把[super dealloc]; 写在最后.
切记, 程序员对内存空间的管理操作, 都是对对象空间中的计数器进行操作, 空间回收(调用dealloc方法)是交由系统来完成的.如果在main函数中自己调用dealloc方法会导致崩溃.
附, 建议的书写规范是将dealloc方法写在最上方.
copy的应用
copy有三种拷贝方式
伪拷贝
浅拷贝
深拷贝
copy方法 对引用计数的影响, 要看程序员需求的 换句话说是由程序员决定本方法对引用计数的影响.
伪拷贝 浅拷贝 深拷贝三种方式 对引用计数的影响都不同
使用copy的原则是该对象的类必须遵守NSCopying协议.
也就是必须重写了
- (id)copyWithZone:(NSZone *)zone;
方法
对这个方法进行不同的实现, 也就形成了不同的拷贝方式.
伪拷贝
- (id)copyWithZone:(NSZone *)zone { // 此时两个对象指针指向同一个空间也就是同一个对象, 值也是指向同一个空间. return self; }
此时使用了copy方法, 计数器不加1, 拷贝的对象结果是自己本身. 此时
Person *p2 = [p1 copy];
// 相当于
Person *p2 = p1;
浅拷贝
两者不占用同一空间, 但是两者的值指向同样的地址.- (id)copyWithZone:(NSZone *)zone { // 新建一个空间(对象), 将值传到新空间中. 此时两个对象虽然不同, 但是值是指向同一个地址.(常量区) Person *p = [[Person alloc] initWithName:_name age:_age]; return p; // release在外部的copy语句后写. }
此时两个对象指针指向两个不同的对象, 被拷贝的对象中的计数不变, 拷贝出来的新对象的计数从0到1.
深拷贝
两者不占用同一空间, 且值也不再同一个空间. 两者相对完全独立.
对字符串进行拷贝 拷贝的结果 要看字符串这个类如何实现的拷贝方法
对不可变字符串的拷贝, 其实相当于直接retain了一次.
可变字符串拷贝的时候, 就是真拷贝了一个新的深拷贝的结果也是两个对象指针指向两个不同的空间, 被拷贝的对象中的计数不变, 拷贝出来的新对象的计数从0到1. 和浅拷贝的结果是一样的.
小知识: 字符串类NSString是OC中一个非常复杂的类簇, 调用不同的初始化方法, 可能值是存在于不同的内存空间中. 有兴趣的同学可以试试, 对一个用语法糖生成的NSString对象或者初始化的值是英文字符串进行无数次的release一样不会报错.