一. 简述
手机的内存资源是非常有限的,如果一个应用的内存使用量超过了单个进程的上限,就会被操作系统终止使用,正式这个原因,内存管理在iOS中扮演着核心的角色。
与(基于垃圾回收的)Java 运行时不同,Objective-C 和 Swift 的 iOS 运行时使用引用计数。 使用引用计数的负面影响在于,如果开发人员不够小心,那么可能会出现重复的内存释放 和循环引用的情况。
1.内存消耗
内存消耗指的是应用消耗的 RAM。应用中的内存消耗分为两部分:栈大小和堆大小。
1.1 栈大小
可被递归调用的最大方法数
每个方法都有其自己的栈帧,并会消耗整体的栈空间。如果你调 用 main,那么 main 将调用 method1,而 method1 又将调用 method2,这就存在三个栈帧 了,且每个栈帧都会消耗一定字节的内存。
一个方法中最多可以使用的变量个数
视图层级中可以嵌入的最大视图深度
渲染复合视图将在整个视图层级树中递归地调用 layoutSubViews 和 drawRect 方法。如 果层级过深,可能会导致栈溢出。
1.2 堆大小
每个进程的所有线程共享同一个堆。一个应用可以使用的堆大小通常远远小于设备的 RAM 值。例如,iPhone 5S 拥有大约 1GB 的 RAM,但分配给一个应用的堆大小最多不到 512MB。应用并不能控制分配给它的堆。只有操作系统才能管理堆。
使用 NSString、载入图片、创建或使用 JSON/XML 数据、使用视图等都会消耗大量的堆 内存。如果你的应用大量使用图片(与 Flickr 和 Instagram 应用类似),那么你需要格外关 注平均值和峰值内存使用的最小化。
didReceiveMemoryWarning
建议使用量不要超过 80%~85%,要给操作系统的核 心服务留下足够多的内存。
2.内存管理模型
内存管理模型基于持有关系的概念。如果一个对象正处于被持有状态,那它占用的内存就 不能被回收。
当一个对象创建于某个方法的内部时,那该方法就持有这个对象了。如果这个对象从方法 返回,则调用者声称建立了持有关系。这个值可以赋值给其他变量,对应的变量同样会声称建立了持有关系。
一旦与某个对象相关的任务全部完成,那么就是放弃了持有关系。这一过程没有转移持有关系,而是分别增加或减少了持有者的数量。当持有者的数量降为零时,对象会被释放, 相 关 的 内存会被回收。
这种持有关系计数通常被正式称为引用计数。当你亲自管理时,它被称为手动引用计数 (manual reference counting,MRC)。虽然现在已经十分罕见,但 MRC 对理解问题很有帮助。现如今的应用大都使用自动引用计数(automatic reference counting ,ARC)。
3.自动释放对象
自动释放对象让你能够放弃对一个对象的持有关系,但延后对它的销毁。当在方法中创建 一个对象并需要将其返回时,自动释放就显得非常有用。自动释放可以帮助在 MRC 中管 理对象的生命周期。
-(NSString *) address |
当创建一个对象并将其从非 alloc 方法返回时,应使用 autorelease。这样 可以确保对象将被释放,并尽量在调用方法执行完成时立即释放。
4.自动释放池
自动释放池块是允许你放弃对一个对象的持有关系、但可避免它立即被回收的一个工具。当从方法返回对象时,这种功能非常有用。
它还能确保在块内创建的对象会在块完成时被回收。这在创建了多个对象的场景中非常有 用。本地的块可以用来尽早地释放其中的对象,从而使内存用量保持在较低的水平。
自动释放池块用 @autoreleasepool 表示。
int main(int argc, char * argv[]) { |
块中收到过 autorelease 消息的所有对象都会在 autoreleasepool 块结束时收到 release 消 息。更加重要的是,每个 autorelease 调用都会发送一个 release 消息。这意味着如果一 个对象收到了不止一次的 autorelease 消息,那它也会多次收到 release 消息。这一点很 棒,因为这能保证对象的引用计数下降到使用 autoreleasepool 块之前的值。如果计数为 0,则对象将被回收,从而保持较低的内存使用率。
看了 main 方法的代码后,你会发现整个应用都在一个 autoreleasepool 块中,这意味着所 有的 autorelease 对象最后都会被回收,不会导致内存泄漏。
Cocoa 框架希望代码能在 autoreleasepool 块内执行,否则 autorelease 对象将无法被 释放,从而导致应用发生内存泄漏。
AppKit 和 UIKit 框架将事件 - 循环的迭代放入了 autoreleasepool 块中。因此,通常 不需要你自己再创建 autoreleasepool 块了。
在一些特定情况下,你很可能想创建自己的 autoreleasepool 块,例如以下这些情况。
当你有一个创建了很多临时对象的循环时
在循环中使用 autoreleasepool 块可以为每个迭代释放内存。虽然迭代前后最终的内存 使用相同,但你的应用的最大内存需求可以大大降低。{
@autoreleasepool {
NSUInteger *userCount = userDatabase.userCount;
for(NSUInteger *i = 0; i < userCount; i++) {
@autoreleasepool {
Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.fname;
if(fname == nil) {
fname = [self askUserForFirstName];
}
NSString *lname = p.lname;
if(lname == nil) {
lname = [self askUserForLastName];
}
//...
[userDatabase updateUser:p];
}
}
}
}
当你创建一个线程时
每个线程都将有它自己的 autoreleasepool 块栈。主线程用自己的 autoreleasepool 启 动,因为它来自统一生成的代码。然而,对于任何自定义的线程,你必须创建自己的 autoreleasepool。
-(void)myThreadStart:(id)obj {
@autoreleasepool {
//新线程的代码
}
}
//其他地方
{
NSThread *myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadStart:) object:nil];
[myThread start];
}5. 自动引用计数
不能实现或调用 retain、release、autorelease 或 retainCount 方法。这一限制不仅针 对对象,对选择器同样有效。因此,[obj release]或@selector(retain)是编译时的错误。
可以实现 dealloc 方法,但不能调用它们。不仅不能调用其他对象的 dealloc 方法,也 不能调用超类。[super dealloc] 是编译时的错误。
但你仍然可以对 Core Foundation 类型的对象调用 CFRetain、CFRelease 等相关方法。不能调用 NSAllocateObject 和 NSDeallocateObject 方法。应使用 alloc 方法创建对象, 运行时负责回收对象。
不能在 C 语言的结构体内使用对象指针。
不能在 id 类型和 void * 类型之间自动转换。如果需要,那么你必须做显示转换。
不能使用 NSAutoreleasePool,要替换使用 autoreleasepool 块。
不能使用 NSZone 内存区域。
属性的访问器名称不能以 new 开头,以确保与 MRC 的互操作性。
虽然总的来说需要避免许多事情,但仍然可以混合使用 ARC 和 MRC 代码。
6. 引用类型
强引用
强引用是默认的引用类型。被强引用指向的内存不会被释放。强引用会对引用计数加 1,从而扩展对象的生命周期。
弱引用
弱引用是一种特殊的引用类型。它不会增加引用计数,因而不会扩展对象的生命周期。 在启用了 ARC 的 Objective-C 编程中,弱引用格外重要。
6.1 变量限定符
ARC 为变量供了四种生命周期限定符。
__strong
这是默认的限定符,无需显示引入。只要有强引用指向,对象就会长时间驻留在内存 中。可以将 __strong 理解为 retain 调用的 ARC 版本。
__weak
这表明引用不会保持被引用对象的存活。当没有强引用指向对象时,弱引用会被置为 nil。可将__weak 看作是 assign 操作符的 ARC 版本,只是对象被回收时,__weak 具有 安全性——指针将自动被设置为 nil。
__unsafe_unretained
与 __weak 类似,只是当没有强引用指向对象时,__unsafe_unretained 不会被置为 nil。 可将其看作 assign 操作符的 ARC 版本。
__autoreleasing
__autoreleasing用于由引用使用id *传递的消息参数。它预期了autorelease方法会 在传递参数的方法中被调用。
// 创建对象后引用计数为 1,并且对象在 p1 引用期间不会被回收。
Person * __strong p1 = [[Person alloc] init];
// 创建对象后引用计数为 0,对象会被立即释放,且 p2 将被设置为 nil。
Person * __weak p2 = [[Person alloc] init];
// 创建对象后引用计数为 1,对象会被立即释放,但 p3 不会被设置为 nil。
Person * __unsafe_unretained p3 = [[Person alloc] init];
// 创建对象后引用计数为 1,当方法返回时对象会被立即释放。
Person * __autoreleasing p4 = [[Person alloc] init];6.2 属性限定符
strong
默认符,指定了__strong关系
weak
指定了__weak关系
assign
copy
暗指了 __strong 关系。此外,它还暗示了 setter 中的复制语义
retain
指定了 __strong 关系。
unsafe_unretained
指定了 __unsafe_unretained 关系。
7. 僵尸对象
僵尸对象是用于捕捉内存错误的调试功能。
通常情况下,当引用计数降为 0 时对象会立即被释放,但这使得调试变得困难。如果开启 了僵尸对象,那么对象就不会立即释放内存,而是被标记为僵尸。任何试图对其进行访 问的行为都会被日志记录,因而你可以在对象的生命周期中跟踪对象在代码中被使用的 位置。
NSZombieEnabled 是一个环境变量,可以控制 Core Foundation 的运行时是否将使用僵尸对 象。不应长期保留 NSZombieEnabled,因为默认情况下不会有对象被真正析构,这会导致应 用使用大量的内存。特别说明一点,在发布的构建包中一定要禁用 NSZombieEnabled。
要想设置 NSZombieEnabled 环境变量,需要进入 Product → Scheme → Edit Scheme。选择 左侧的 Run,然后在右侧选取 Diagnostics 标签页。选中 Enable Zombie Objects 选项。
个人博客: 🏡 ForgetSou