⼀、内存管理的⽅式
1、内存常见问题
(1)野指针异常:指针操作已经销毁的对象
指针指向某对象,该对象释放后,该指针即为野指针,对其操作造成野指针异常。
原因:过度释放。
(2)内存溢出:超出内存上限
iOS给每个应⽤程序提供了⼀定的内存,⽤于程序的运⾏。iPhone 3GS内存 30M左右,iPhone 5S 内存80M左右。⼀旦超出内存上限,程序就会Crash。
2、内存管理的方式
(1)垃圾回收(gc) | OC支持 — OS X开发 支持 | iOS 不支持
程序员只需要开辟内存空间,不需要⽤代码显式地释放,系统来判断哪些空间不再被使⽤,并回收这些内存空间,以便再 次分配。整个回收的过程不需要写任何代码,由系统⾃动完成垃圾回 收。Java开发中⼀直使⽤的就是垃圾回收技术。
(2)MRC (Manual Reference Count) | ⼈⼯引⽤计数 | iOS支持
手动操作引用计数,手动调用控制引用计数的方法
内存的开辟和释放都由程序代码进⾏控制。
(3)ARC (Auto Reference Count) | ⾃动引⽤计数 | iOS支持
自动操作引用计数,编译器调用引用计数的方法
iOS 5.0的编译器特性,它 允许⽤户只开辟空间,不⽤去释放空间。它不是垃圾回收!它的本质 还是MRC,只是编译器帮程序员默认加了释放的代码。
⼆、内存管理机制
C语⾔中:使⽤malloc和free,进⾏堆内存的创建和释放。堆内存只 有正在使⽤和销毁两种状态。 实际开发中,可能会遇到,两个以上的指针使⽤同⼀块内存。C语⾔ ⽆法记录内存使⽤者的个数。
OC采⽤引⽤计数机制管理内存,当⼀个新的引⽤指向对象时,引⽤ 计数器就递增,当去掉⼀个引⽤时,引⽤计数就递减。当引⽤计数到 零时,该对象就将释放占有的资源。
1、引⽤计数
(1)引用计数 标记程序运行期间,对象被引用的次数
(2)通过操作引用计数,控制对象是否被销毁。
(3)当引用计数应该减为0时,对象自动被销毁,存储空间被回收
2、操作引⽤计数的⽅法
alloc\ retain\ copy\ release\ autorelease
(1)造成引用计数增加
除了NSString的对象使用copy 其余的对象类型都声明成retain
+alloc 当前对象 计数 0 -> 1
Person * p = [[Person alloc] init];
NSLog(@"%lu", p.retainCount);
-retain 当前对象 计数 加1
指针的引用
新对象与原对象指向相同对象
[pretain];
NSLog(@"%lu", p.retainCount);
Person * p3 = [p2 retain];
NSLog(@"%lu,%lu,%lu", p.retainCount, p2.retainCount, p3.retainCount);
-copy 原来的对象计数 不变 新的对象 0 -> 1
拷贝
详情见四
(2)造成引用计数减少
release 当前对象 立即减1
// 调用release,实现引用计数-1
[p release];
NSLog(@"%lu", p.retainCount);
[p release];
NSLog(@"%lu", p.retainCount);
// 当引用计数应该减为0时,对象被销毁,存储空间被回收(系统完成)
// person对象被销毁(只是标记删除,并不清空数据),不建议通过指针操作
// 对象被销毁后,打印引用计数显示结果为1
[p release];
NSLog(@"%lu", p.retainCount); // 应该减为0
autorelease 当前对象 延迟减1 非立即
[pe1 autorelease]; // 未来的某个时刻引用计数-1(管理其的自动释放池release时)
autoreleasepool的使⽤:
第一种写法:将对象写在pool的创建和release之间
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Person * p = [[Person alloc] init]; // 1
[p retain]; // 2
[p retain]; // 3 NSLog(@"%lu", pool.retainCount);
[p autorelease]; // 3
[p autorelease]; // 3
NSLog(@"%lu", pool.retainCount);
// release销毁自动释放池(扔掉垃圾桶)
[pool release];
// drain清空自动释放池(只倒垃圾)
//[pool drain];
第二种写法:mrc支持两种,arc只支持第二种
Person * pe = [[Person alloc] init]; //1
[pe retain]; // 2
NSLog(@"--- %lu", pe.retainCount);
@autoreleasepool {
[pe autorelease]; // 2
NSLog(@"--- %lu", pe.retainCount);
}
NSLog(@"--- %lu", pe.retainCount); // 1
3、销毁对象
dealloc 引用计数将要减为0时,对象自动调用
(1)继承自NSObject,可以不实现,编译器默认实现
(2)如果实现dealloc方法
- (void)dealloc
{
代码
[super dealloc];
}
QA:
dealloc和release到底执行了什么操作?
三、内存管理的基本原则
引⽤计数的增加和减少相等,当引⽤计数降为0之后,不应该再使⽤这块内存空间。
正常情况
凡是使⽤了alloc、retain或者copy让内存的引⽤计数增加了,就需 要使⽤release或者autorelease让内存的引⽤计数减少。在⼀段代码 内,增加和减少的次数要相等。
异常情况:
假设当前pe指向的对象的引用计数为1
如果不写[pe release]; main函数运行结束后,局部变量pe被销毁,没有指针指向对象,造成内存泄漏
如果写了不止一句[pe release];造成过度释放。本质:野指针异常(pe 操作已释放的对象)。
3、语法糖
利用语法糖创建的对象和类方法创建的对象相同,均由自动释放池管理。
NSArray * arr = @[@"asfe",@"23",@"2"];
NSNumber *number = @123;
四、掌握copy的实现
自定义对象的拷贝要实现NSCopying协议和NSMutablecopying协议,但是在NSCopying协议中也能实现本质上的深拷贝所以只需实现一个方法就可以。
1、深浅拷贝
深拷贝 拷贝的是对象,即创建了新的对象
浅拷贝 拷贝的是指针,即操作的是原来的对象,没有创建新的对象
// 实现浅拷贝- (id)copyWithZone:(NSZone *)zone{ return [self retain];
}
// 深拷贝- (id)copyWithZone:(NSZone *)zone{ // Person * p = [[Person allocWithZone:zone] init]; // 以前的iOS版本需要这样写,如果需要向下兼容可以写这句代码 Person * p = [[Person alloc] init]; p.name = self.name; // self表示调用copy方法的对象。即被拷贝的对象 return p;
}
2、copy 和 mutableCopy
对象调用copy方法得到的是不可变的对象
对象调用mutableCopy得到的是可变的对象
不可变对象创建不可变对象(NSString)为浅copy其余全为深copy
NSZombieEnabled 环境变量
当设置NSZombieEnabled环境变量后,一个对象销毁时会被转化为_NSZombie,设置NSZombieEnabled后,当你向一个已经释放的对象发送消息,这个对象就不会向之前那样Crash或者产生一个难以理解的行为,而是放出一个错误消息,然后以一种可预测的可以产生debug断点的方式消失, 因此我们就可以找到具体或者大概是哪个对象被错误的释放了。
注意:NSZombieEnabled环境变量需要每次创建工程时设置,且上传之前必须关闭
*** -[__NSArray addObject:]:message sent to deallocated instance 0x6557370
Xcode打开僵尸对象:
个人总结:NSString 创建的字符串内容为ASCII,则在常量区创建常量字符串,否则,在堆区创建(待检验)
NSString * str1 = @"易荟云";请问str1的retainCount是多少?
@""在常量区,str1的retainCount为最大值。
drain与release区别
在MRC下,两者基本一样,在GC环境下,release 是一个no-op(无效操 作),所以无论是不是gc都使用drain
iOS有没有垃圾回收?autorelease 和垃圾回收制(gc)有什么关系?
没有。autorelease只是延迟释放,gc是每隔一段时间询问程序,看是否有无指针指向的对象,若有,就将它回收。他们两者没有什么关系。