【Objective-C 篇】 ☞ 5. MRC、ARC
MRC — 手动管理内存
1.1 内存引用平衡原则
1) 如果使用alloc,new开头,或者是copy(复制一个对象)来创建一个对象,意味着你拥有这个对象的所有权。这个对象的引用计数器初始值为1(也有可能>1)。
2) 如果你拥有这个对象的所有权,在不使用此对象时,就有责任向对象发送release消息。(谁创建了对象,谁就有责任release这个对象)
3) 如果并不拥有一个对象的所有权,而想要使用这个对象,为了防止你在使用此对象期间,对象被别人释放掉,需要向对象发送retain消息,以保持对象。此时可以认为,你也拥有了这个对象所有权。
4) 当你使用完retain过的对象后,有责任release一下这个对象。
(谁retain了一个对象,谁就有责任release这个对象)
配对出现:(+1、-1 ==>平衡)
我们创建的对象不用了,就release;我们retain的对象不用了,就release。
内存管理的原则就是有加就有减。也就是说, 一次alloc(new)对应一次release, 一次retain对应一次release。
1.2 自动释放池(autoreleasepool)
通过自动释放池来管理对象,只需要一个自动释放池,可以管理很多对象,当自动释放池结束的时候,会自动向池中的每个对象都发送release消息。
1) 如果一个对象创建后,不能马上释放它,但又不得不尽到释放对象的责任,此时可以将对象放入自动释放池,延迟对象的释放时机。比如绝大部分工厂方法都是如此。工厂方法中的对象是方法中创建的,按理来说应该由工厂方法内部释放,但工厂方法的功能决定了这个对象不能马上释放,此时应该将对象放入自动释放池。
2) 当自动释放池结束时,会向池中的所有对象发送release消息,如果此时,池中的对象的引用计数器是1,那么,对象会被释放掉。
3) 如何开始和结束一个自动释放池呢?
//自动释放池,用于回收对象的存储空间。 @autoreleasepool{ //开始(创建一个自动释放池) …… …… } //结束(自动释放池销毁了, 给自动释放池中所有的对象发送一条release消息) |
还有一种性能低下,被淘汰的自动释放池创建方式(了解)
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];//开始一个自动释放池 …… …… [pool drain];//结束一个自动释放池 |
4) 无论一个对象是否在自动释放池,只要这个对象是由别人创建的,你要使用此对象,就得retain,用完此对象,就得release,即使对象在自动释放池,也依然如此。
5) 实际开发中,合理使用自动释放池来避免内存使用出现峰值。
如果App出现的内存使用的峰值,此时才考虑是否是由于大量使用工厂方法造成的,是否需要使用自动释放池解决问题。要合理使用自动释放池,大量使用会消耗系统资源。
6) autorelease方法:在自动释放池中使用,目的是将对象添加到自动释放池中。自动释放池销毁时会对池中对象作release操作。(延迟release)
@autoreleasepool { Person *p = [[[Person alloc] init] autorelease];
} |
- autorelease方法与release方法的区别:这两个方法都能把对象的引用计数器减1。
- release是一个精确的减1,对对象的操作只能在release之前进行,如果是在之后,就会出现野指针错误;
- autorelease是一个不精确的引用计数器减1,当给对象发送autorelease消息时,对象就会被放到自动释放池中,自动释放池销毁时会给池中的所有对象发送release消息,使得所有对象的计数器减1,所以本质上autorelease还是会调用release。
- 常见错误:
// 销毁自动释放池的时候 要对person再执行release操作的话 会报野指针错误 @autoreleasepool { Person *person = [[[Person alloc] init] autorelease]; [person release]; } |
// 对象执行两次autorelease意味着自动释放池销毁的时候 对象会执行两次release操作 会报野指针错误 @autoreleasepool { Person *person = [[[[Person alloc] init] autorelease] autorelease]; } |
1.3 setter方法的内存管理(应用场景:两个类是聚合关系)
当一个方法传入一个对象后,如果需要将这个对象用实例变量等手段保存起来持续使用时,需要做以下事:
1) 先将此对象的引用计数器加1(retain)
2) 再将原来实例变量指向的对象的引用计数器减1(release)
3) 最后将传入的对象地址保存到实例变量中
4) 被retain的对象通常需要在dealloc方法中release.
只要一个对象想使用房间,就需要对这个房间的引用计数器+1
只要一个对象不想使用房间,就需要对这个房间的引用技术器-1
经常会被问到的问题:
1) 前面3个顺序可否颠倒?
能不能先release原来的对象,再赋值,最后retain新对象? 一般可以,但不建议
2) dealloc方法中,可不可以使用self.属性 = nil;的方式释放属性所指向的对象? ok的
可以的,self.属性 相当于调用上面的setter方法(该方法中有向对象发release)
Demo:setter方法的内存管理
1.4 property修饰符
1) nonatomic 非原子性操作 (安全性低,但效率高。iOS实际开发中99%用这个,以确保性能)
atomic 原子性操作 (安全性高,但很耗费资源)
注意,默认是原子性的(atomic),所以在定义属性时一定要写上nonatomic。
2) assign, retain, copy
assign 是默认值,仅做赋值。不会解决内存问题(即不会retain,也不会release)。在MRC中可用于对象类型和非对象类型。
retain 只能用于对象类型的属性,会解决内存问题(生成的setter方法会自动加入retain,release等内存操作代码)
copy 一些特殊对象类型,如果不希望和别人共享一个对象,用copy会自动创建一个新的对象。
有些属性在传入后,需要拷贝一份,以防止传入的对象内容被修改后,影响到我的对象。
(并不是所有的对象都能拷贝,只有遵守<NSCopying>协议,实现了协议中CopyWithZone方法的这种对象才拥有拷贝的功能)
3) readonly/readwrite
readwrite 是默认的,编译器生成getter和setter方法
readonly 只读,编译器只生成getter方法
属性访问器的类型:
注:声明属性默认情况下,并没有解决内存问题
当使用 @property(retain)引用数据类型…,帮我们解决了setter使用中的内存问题,但dealloc中的release操作,我们自己来做。
1.5 MRC中的循环引用(两个对象互相引用(即互相包含)时,要一强一弱!)
如果A对象要拥有B对象, 而B对象又要拥有A对象, 此时会形成循环retain
如何解决这个问题:让其中一方不要做retain操作即可!
Demo:
ARC — 自动管理内存
1.1 概念
Automatic Reference Counting 自动引用计数
基于MRC, 在MRC下,对象的引用计数是由程序员负责的。在ARC下,对象的引用计数工作由编译器来完成。
1.2 ARC的工作原理
在ARC下,堆内存空间的管理依然使用引用计数器的方式。只是ARC下的引用计数器的加减工作不再由程序员来做,而是由编译器来做。
编译器在编译程序期间,会在程序恰当的位置自动加入对对象的retain,release和autorelease的操作。
注意:ARC是 编译期语法或编译器特性(即可以理解为是Xcode的一个功能),而不是运行时特性。
1.3 怎么用
程序员不要对对象的引用计数器进行操作,编译器会帮我们做:
1) 在ARC下,不要在程序中调用retain, release, autorelease方法。
2) 在ARC下,不要重写retain, release, autorelease方法。
3) 在ARC下,不要在dealloc方法中调用父类的dealloc方法。实际上,ARC下,dealloc方法基本没有用了。
总之,一切与内存操作相关的东西都由ARC自动完成。
1.4 ARC的判断原则:(即系统怎么判断对象是否要释放)
- 自动管理内存的判断原则:只要还有一个强指针变量指向对象,对象就会保持在内存中。
- 强指针(强引用):
- 默认情况下所有的指针变量都是强指针
- 被__strong修饰的指针
- 例如:
TRPerson *p1 = [[TRPerson alloc]init]; __strong TRPerson *p2 = [[TRPerson alloc]init]; |
- 弱指针(弱引用)
- 被__weak修饰的指针
- 例如:
__weak TRPerson *p = [[TRPerson alloc]init]; |
- 举例:
//ARC的判断准则:只要没有强指针指向对象,对象就会释放 { TRPerson *p1 = [[TRPerson alloc]init]; __strong TRPerson *p2 = [[TRPerson alloc]init]; __weak TRPerson *p3 = p2; p2 = nil;//p2改变了指向,此时就没有强指针指向对象,对象就会释放 }//出了大括号,局部变量p1就释放,此时就没有强指针指向对象,对象就会释放 |
单个对象的内存管理:如果一个对象不再使用了,就把指向对象的强指针置为nil,对象就会释放。
- 注意:
- 当使用ARC的时候,暂时忘记“引用计数器”,因为判断标准变了。
- 在实际开发中,千万不要使用一个弱指针来保存一个刚刚创建的对象。
{ //p是弱指针,对象会被立即释放 __weak TRPerson *p = [[TRPerson alloc]init];//刚创建就被释放! } |
1.5 ARC中多个对象的内存管理:
- ARC和MRC一样,想拥有某个对象必须用强指针保存对象,但是不需要在dealloc方法中release
@class TRDog; @interface TRPerson : NSObject
//MRC下写法 @property (nonatomic, retain) TRDog *dog; //ARC下写法 @property (nonatomic, strong) TRDog *dog;
@end |
MRC
A对象想使用B对象, 需要对B对象进行一次retain
A对象不用B对象了, 需要对B对象进行一次release
即,property的时候进行retain, dealloc的时候进行release
ARC
A对象想使用B对象, 那么就需要用一个强指针指向B对象(即用strong,表示强引用)
// 在ARC中保存一个对象用strong, 相当于MRC中的retain
@property (nonatomic, strong) Dog *dog;
A对象不用B对象了, 什么都不需要做, 编译器会自动帮我们做
1.6 ARC下循环引用问题
循环引用:指两个对象相互强引用了对方,即retain了对方,从而导致谁也释放不了谁的内存泄露问题。
- ARC和MRC一样,如果A拥有B,B也拥有A,那么必须一方使用弱指针
- 两个强指针互相引用,两个空间就会永不释放!所以必须要一强一弱。
@class TRDog; @interface TRPerson : NSObject //MRC写法 //@property (nonatomic, retain) TRDog *dog; //ARC写法 @property (nonatomic, strong) TRDog *dog; @end
@interface TRDog : NSObject //错误写法,循环引用会导致内存泄露 //@property(nonatomic, strong)TRPerson *owner;
//正确写法,当如果保存的是对象类型建议使用weak //@property(nonatomic, assign)TRPerson *owner; @property (nonatomic, weak) TRPerson *owner; @end |
- ARC下循环引用的案例:
1.7 如果在ARC下定义属性的内存特质(attribute)
- 在MRC下, 与内存相关的属性特质有:assign, retain, copy
- 在ARC下, 与内存相关的属性特质有:
(1)strong 强引用
类似于retain,引用时候会引用计数+1。
strong 案例: |
@property (nonatomic, strong) NSString *str1; @property (nonatomic, strong) NSString *str2; |
self.str1 = @"Hello World"; self.str2 = self.str1; self.str1 = nil; NSLog(@"str2 = %@", self.str2); |
结果是:str2 = Hello World |
(2)weak 弱引用
- 类似于assign,不会改变引用计数,只做简单的赋值。
- weak与assign的区别:
- assign 仅做赋值(默认值),不会解决内存问题。在MRC中可用于对象类型和非对象类型。
- weak只用于ARC下的对象类型(即带*号的指针类型)。weak比assign更完全,会自动把野指针置空。
- assign一般在ARC下用于基本数据类型(即不带*号的类型,包括id类型)
- weak 的特点:
- 声明为weak的指针,指针指向的地址一旦被释放,这些指针都将被赋值为nil。这样的好处能有效的防止野指针。
- 特定情况下,如果内存的释放会出现问题,经常使用weak来解决,比如,最常见的问题是"内存循环引用"
weak 案例: |
@property (nonatomic, strong) NSString *str1; @property (nonatomic, weak) NSString *str2; |
self.str1 = @"Hello World"; self.str2 = self.str1; self.str1 = nil; NSLog(@"str2 = %@", self.str2); |
结果是:str2 = null |
(3)unsafe_unretained (从名字上来看:不安全,不retain)
- 默认的, 可以用于非对象类型/对象类型。
- 用于非对象类型时和assign一样,只做简单的赋值。
- 用于对象类型时类似于weak,但没有weak安全(因为当指向的对象被销毁时,指针不会自动置空,会产生野指针错误)
unsafe_unretained 案例: |
@property (nonatomic, strong) NSString *str1; @property (nonatomic, unsafe_unretained) NSString *str2; |
self.str1 = @"Hello World"; self.str2 = self.str1; self.str1 = nil; NSLog(@"str2 = %@", self.str2); |
没有输出结果!程序崩掉,会报野指针错误(EXC_BAD_ACCESS 坏访问) |
(4)copy 拷贝 (和以前的copy一样)
使用copy: 对NSString
效果其实和retain没什么两样,唯一的区别就是copy只用于NSString而不能用于NSMutableString, 如果当一个类继承NSObject,那么这个类里面的属性需要使用copy。
retain是指针拷贝,copy是内容拷贝。
- strong, weak, unsafe_unretained往往都是用来声明属性的。
如果想声明临时变量就得用__strong, __weak, __unsafe_unretained, __autoreleasing,其用法与上面介绍的类似。
案例: |
__strong NSString *str1 = @"Hello World"; __weak NSString *str2 = str1; __unsafe_unretained NSString *str3 = str2; str1 = nil; //现在str1与str2的指针都为nil,而str3不为nil,但是是野指针。 |
提示:没有两个下划线的放属性里,有两个下划线的放变量前。
- objective-c内存管理中有一条是:谁分配谁释放。__autoreleasing则可以使对像延迟释放。
autoreleasing的应用:在函数内部申请的空间,在函数外部也可以使用
//MRC下 -(NSString *)stringTest { NSString *retStr = [NSString stringWithString:@"test"]; return [[retStr retain] autorelease]; } |
//ARC下 -(NSString *)stringTest { __autoreleasing NSString *retStr = [NSString alloc] initWithString:@"test"]; return retStr; } |
- 总结:
ARC下@property参数:
strong:用于OC对象,相当于MRC中的retain (强引用)
weak:用于OC对象,相当于MRC中的assign (弱引用)
assign:用于基本数据类型,跟MRC中的assign一样。
1.8 ARC下的自动释放池
在ARC下,对象的工厂方法依然会将对象放入自动释放池。当池结束时,向池中的对象发送release消息。
池中的对象什么时候销毁,无法确定,因为编译器会做很多优化。
在iOS开发中,一个事件循环结束,自动释放池会释放一次,池中的对象会收到release消息。