OC之内存管理
Objective-C 的内存管理是通过引用计数实现的,引用计数是一种通过对象的唯一引用,确定对象是否正在被使用的技术!如果引用计数降到 0,对象就被视为不再有用,运行时系统会释放它的内存!
引用计数通过可以递增递减的计数器来管理内存。对象创建好之后,保留引用计数至少为 1,若引用计数为正,则对象继续存活;若引用计数降为 0,则系统释放该对象。
内存管理不当导致的问题:
- 内存泄漏:应当废弃的对象在超出其生存周期后仍然存在! 程序没有释放不再使用的对象会导致内存泄露;浪费内存资源,最终程序耗尽系统内存!
- 悬挂指针:程序释放了还在使用的对象;
Objective-C 提供了两种内存管理机制:手动管理 MRC 和 自动引用计数 ARC。
1.1、MRC
手动管理是一种建立在 对象所有权 概念上的内存管理机制,只要对象的所有者还存在,对象就不会被 Objective-C 运行时环境释放。
我们需要搞清楚 访问和使用对象的方式 以及 访问对象与对象所有权 之间的差别。
1.1.1、 对象引用与对象所有权
Objective-C 对象是通过指向该对象内存地址的指针,以间接方式访问的。
//定义一个 object1 的指针变量,指向一个 NSObject 对象
NSObject *object1 = [[NSObject alloc] init];
指针实现了 Objective-C 对象的间接访问功能,但是它们本身不具备所有权:
/* 声明指针 object2,指向一个名为 object1 的 NSObject 对象
* object2 并没有获取对象的所有权。
* 如果对象 object1 被释放,指针 object2 就是一个悬挂指针,不再指向一个合法对象
*/
NSObject *object2 = object1;
1.1.2、内存管理原则
要正确使用 MRC ,编写代码时就必须在获取对象所有权与释放对象所有权之间进行平衡;需要遵循以下内存管理原则:
-
自己生成的对象,自己持有 : 通过
+alloc、-new、-copy、-mutableCopy
等方法创建对象并持有对象的所有权; -
非自己生成的对象,自己也能持有:通过
-retain
方法可以获取一个对象的所有权; -
不再需要自己持有的对象时释放所有权:自己持有的对象,一旦不再需要,持有者需要释放所有权,使用
-release
或者-autorelease
方法释放该对象所有权;如果对象的引用计数为 0,编译器使用-dealloc
方法将该对象从堆内存清除; - 无法释放非自己持有的对象:倘若在程序中释放了非自己持有的对象就会造成崩溃
/* 非自己生成的对象
* 取得的对象存在,但并不被 array 持有
*/
NSMutableArray *array = [NSMutableArray array]
[array retain];//通过 -retain 持有该对象
[array release];//不再使用时,释放所有权
在 Objective-C 中释放对象有两种方式:
- 调用
-release
方法,使对象的引用计数立即递减; - 调用
-autorelease
方法将该对象加入自动释放池,虽然不再持有该对象所有权,但该对象可能仍然存在,并不会立即释放。清空自动释放池时,系统会向其中的对象发送release
消息。
1.2、ARC
自动引用计数 ARC 是一种强大的内存管理工具,在编译程序时,编译器会分析源代码,确定以动态方式创建的对象的回收需求,然后在已编译代码的必要位置自动插入 retain
和 release
消息。
使用 ARC ,可以提升应用的性能、避免内存管理错误;与 MRC 相比,大幅度简化内存管理工作。但 ARC 仍需遵守一些规则:
- 不能使用
-retain、-release 、-autorelease、-retainCount
等方法; - 不能直接进行
id
和(void *)
类型的互换。ARC 只管理 Objective-C 对象,对于通用指针(void *)
,需要限制!如 Foundation 与 Core Foundation 框架对象的互换;
1.2.1、ARC的所有权修饰符
ARC 下的所有权修饰符:
__strong
:默认修饰符,强引用,持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象随之释放。
__weak
:弱引用,对象随时可以被释放,释放后指针指向 nil
;解决循环引用问题
__unsafe_unretained
:不安全,编译器不负责内存管理;对象释放后指针不会指向 nil
,变为悬挂指针;
__autoreleasing
: 等价于-autoreleasing
,将被修饰的变量注册到 NSAutoreleasePool
Ⅰ、__strong
修饰符_
在 ARC 下,由于 __strong
修饰符 是默认修饰符,因此非自己生成的对象,自己也能持有:
/* 非自己生成的对象,自己也能持有
* NSMutableArray *array 等价于 __strong NSMutableArray *array
* 超出其作用域,强引用失效,释放其所有权
*/
NSMutableArray *array = [NSMutableArray array]
__strong
修饰符在赋值上也能够正确管理其对象的所有权:
// object1 自己生成并持有的对象
NSObject *object1 = [[NSObject alloc] init];
/* 声明指针 object2,指向一个名为 object1 的 NSObject 对象
* object2 获取对象的所有权:因为 object2 被 __strong 修饰
* 即使 object1 释放其所有权,指针 object2 仍然具有所有权
*/
NSObject *object2 = object1;
__strong
修饰符相当于 -retain
方法,要废弃 __strong
变量,只需将指针它处!
NSMutableArray *array = [NSMutableArray array];//对象A
array = [NSMutableArray array];//对象B;此时对象A 被释放
array = nil;//对象B被释放
Ⅱ 、__weak
修饰符
当几个对象互相引用,会导致循环引用问题:
@interface PeopleModel : NSObject
@property (nonatomic ,strong) CarModel *car;
@end
@interface CarModel : NSObject
@property (nonatomic ,strong) PeopleModel * owner;
@end
{
PeopleModel *liSi = [[PeopleModel alloc]init];
CarModel *car = [[CarModel alloc]init];
liSi.car = car;
car.car = liSi;
}
liSi
持有某辆车,该车又指向它的主人!两者互相引用:只有 car
被释放,变量liSi
才能释放掉;而变量 liSi
又等待 car
释放,它才会释放!这是最简单的的循环引用的例子。
ARC 下的循环引用,导致内存泄漏!为避免出现内存泄漏,需要使用弱引用。
弱引用是一种非所有权引用,被弱引用的对象不属于引用它的对象,从而消除循环引用!当弱引用修饰的变量被释放时,其指针指向 nil
,不会造成悬挂指针!
Ⅲ 、__unsafe_unretained
修饰符
__unsafe_unretained
修饰符,是不安全的所有权修饰符!尽管 ARC 下内存管理由编译器负责,但 __unsafe_unretained
修饰修饰的变量不属于编译器管理的范畴!
__unsafe_unretained
变量同 __weak
变量一样,生成的对象并不被自己所持有!
__unsafe_unretained NSMutableArray *array_0;
{
NSMutableArray *array_1 = [NSMutableArray array];
array_0 = array_1;//变量 0 并不具备所有权
}
/* 块作用域结束,数组 array_1 被释放
* 此时指针 array_0 指向的内存被废弃,array_0 是一个悬挂指针
*/
NSLog(@"array_0 === %@",array_0);
Ⅳ 、__autoreleasing
修饰符
内存峰值 是指应用程序在某个特定时间段内的最大内存用量。合理运用自动释放池,可降低应用程序的内存峰值。
自动释放池机制就像栈一样:系统创建好自动释放池之后,就将其推入栈中;而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里!
Cocoa 框架中的 Foundation 框架库中的 NSObject 类担负着内存管理的职责!
Objective-C 内存管理的 autorelease 就是自动释放,类似于 C 语言中的自动变量,而 NSAutoreleasePool 对象的生存周期相当于 C 语言变量的作用域。 对于所有调用过 -autorelease
方法的对象,在释放 NSAutoreleasePool 对象时,都将调用 -release 方法。
在 Cocoa 框架中,相当于程序主循环的 NSRunLoop 或者在其他程序可运行的地方,对 NSAutoreleasePool 对象生成,持有和释放。NSRunLoop 每次循环过程中 NSAutoreleasePool 对象被生成和释放!
ARC 下,使用 @autoreleasepool
块替代 NSAutoreleasePool
类对象。使用 __autoreleasing
修饰变量来替代 变量调用 -autorelease
方法!
显式的附加 __autoreleasing
修饰符在编程中非常罕见,作为方法返回值的对象,编译器会自动将其注册到 autoreleasepool
。
int main(int argc, const char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
return 0;
}
从技术角度来说, main()
函数的自动释放池不是必需的,因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽然如此,但没有这个自动释放池,那么由 UIApplicationMain()
函数所自动释放的那些对象,就没有自动释放池可以容纳,于是系统会发出警告来表明这一信息!所以该自动释放池可以理解为最外围捕捉全部自动释放对象所用的池!
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└─ id objc_object::rootAutorelease2()
└─ static id AutoreleasePoolPage::autorelease(id obj)
└─ static id AutoreleasePoolPage::autoreleaseFast(id obj)
├─ id *add(id obj)
├─ static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └─ id *add(id obj)
└─ static id *autoreleaseNoPage(id obj)
├─ AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└─ id *add(id obj)
ARC 有效,指定编译器属性为 -fobjc-arc
1.3、用僵尸对象调试内存管理
向已释放掉的对象发送消息是不安全的,这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其它内容所覆写。而这块内存有没有移作他用,又无法确定,因此应用程序只是偶尔崩溃,在没有崩溃的情况下,那块内存只是复用了其中一部分,所以对象中的某些二进制数据依然有效。还有另一种可能,该块内存恰好为另一个有效且存活的对象所占据,在这种情况下,运行时系统可能会把消息发到新对象那里,此新对象也许应答、也许不能!如果能,那么程序就不会崩溃,但处理的消息不是你预想的结果!
Cocoa 提供了 “僵尸对象”可以调试上述问题:将环境变量 NSZombieEnabled
设置为 YES
,开启此功能。运行时系统会把所有已经回收的实例转化成特殊的 “僵尸对象”,而不会真正回收它们;这种对象所在的核心内存无法重用,不可能被覆写。僵尸对象收到消息后,会抛出异常:包含发送过来的消息、描述回收之前的对象!