《iOS应用开发》——2.3节内存管理

简介:

本节书摘来自异步社区《iOS应用开发》一书中的第2章,第2.3节内存管理,作者【美】Richard Warren,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.3 内存管理
iOS应用开发
我不是吓唬你们。在iOS 5.0系统之前,内存管理毫无疑问是iOS开发最困难的部分。简而言之,问题是这样的。无论何时你创建了一个变量,你就要在内存中给它分配一定的空间。对于局部变量来说,我们通常使用栈上的内存,这些内存是自动管理的,当函数返回时,函数中定义的任何局部变量都会从内存中自动删除。

这听起来很棒,但是栈有两个严重的局限。首先,它的空间非常有限,如果用尽了内存,应用程序就会崩溃。其次,这些变量很难共享。请记住,函数使用值传参和返回。这意味着所有传入函数或者从函数传出的内容都是复制值。如果你仅仅处理整型和浮点数据,这不会有问题。但是如果开始处理大型、复杂的数据结构时会发生什么呢?从一个函数传递一个参数到另一个函数,并且你会发现自己在复制一个副本的副本。这会迅速浪费很多时间和内存。

还有另外一个方法,我们可以在堆中声明变量并且使用指针来访问其内存空间。这有几个优点。在堆上我们有更多的可用内存空间,并且我们可以自由地传递指针,只有指针本身被复制了,而不是整个数据结构。

然而,当我们使用堆时,我们必须手动管理它的内存。当我们需要一个新的变量时,我们必须在堆上请求足够的内存空间。接着,当我们使用完毕时,必须释放这个空间,让它能够被重复使用。这会导致两个常见的内存相关的故障。

首先,你释放了内存但是却不小心继续使用了它。这就叫做野指针。这一类型的故障很难查找出来。释放一个变量并不需要改变储存在堆中的实际值。它只是告诉OS:这块内存可以再次被使用了。貌似被释放内存的指针还可以继续正常使用,只会在系统最终重新使用这块内存,并且覆写了原来的值的时候才会使你遇到麻烦。

这会在完全不相关部分的代码产生很奇怪的错误。想象这种情况:你在堆中释放了A对象的内存但是不小心地继续在使用它的指针。同时,一个完全不相关部分的代码在堆上请求了一个新的对象B。它被分配了一块内存,部分地覆盖了对象A。现在你改变对象A的一个值,这在对象B中的某个部分保存了新的数据,让B变成了无效的状态。下一次你想要从对象B读取值的时候,就会出错(或者有可能得到非常诡异的结果)。

第二个常见的内存错误是忘记释放内存,这称为内存泄漏,并且这会导致应用程序在运行过程中占用越来越多的内存。

不过,情况并没有那么糟糕。当应用程序退出时所有的内存都会被释放。我曾经听说过有的服务器软件故意泄露它的所有内存。毕竟,如果你同时运行成千上万的服务器副本,周期性地杀死和启动新的副本比冒着因野指针导致程序崩溃要容易处理得多。

然而,在iOS系统中,我们不能那么奢侈地使用内存。所有的iOS设备都有严格的内存限制。使用过多内存,应用就会被系统杀死。因此正确和有效的内存管理是一个至关重要的技能。

对我们来说,我们主要关注的是对象。所有的对象都是在堆中创建的。是的,你也可以在堆上创建C数据类型变量或者结构,但是这是非常罕见的(可以找一本关于C语言编程的好书看下具体的细节)。大部分情况下,iOS内存管理就是管理我们对象的生命周期。

2.3.1 对象和保留计数
最大的问题之一仅仅是确定代码的哪个部分应该负责对象的内存释放。在简单的例子中,解决方案总是显得微不足道。然而,一旦你开始传递对象,将它们保存在其他对象的实例变量中或将它们放置在集合中,很快就开始陷入困境了。

要解决这个问题,Objective-C程序习惯使用引用计数。当我们创建一个对象的时候,引用计数从1开始。我们可以通过在对象中调用retain方法来增加引用计数。类似地,我们可以通过调用release方法来减少引用计数。当引用计数等于0时,系统从内存上删除这个对象。

这让传递对象成为可能,并且不用担心归属问题。如果你想要持有一个对象,你就保留它。当你完成使用,你就释放它。只要你正确地使用所有的保留和释放方法(更不用说自动释放和自动释放池了),你就既能够避免野指针也能够避免泄露内存。

当然,让一切运转正常并不像看上去的那么容易。Objective-C的内存管理规定有很多规则和奇怪的情况。此外,作为开发者,我们必须总是保证100%的正确性。所以每当我们创建一个新的变量,经常会机械地遵循以下的步骤。这是非常令人生厌的。它创建了很多机械的代码,其中很多严格意义上来说并不是必需的,但是如果我们不是每次都严格遵守每个步骤,就会有忘记某些真正重要的东西的风险。

当然,苹果公司也试图提供帮助。Instruments有一系列追踪分配和释放、查找内存泄漏的工具。在近期的Xcode版本中,静态分析器在分析代码和查找内存管理错误方面已经做得越来越好了。

这也提出一个问题:如果静态分析器可以找到这些错误,为什么不能直接修复它们呢?毕竟内存管理是一件枯燥、针对细节的任务,需要遵循一套严格的规则,正是编译器所擅长的任务。于是就有了一种非常新的管理内存的技术:自动引用计数(ARC)。

2.3.2 介绍ARC
ARC是一个为Objective-C提供自动内存管理的编译器特征。理论上来说,ARC遵循了保留和释放内存管理的惯例(查看苹果公司的Memory Management Programming Guide以获得更多的信息)。当你的程序在编译时,ARC分析代码并且自动地添加必需的retain,release和autorelease方法调用。

对于开发者来说,这是非常好的消息。我们不再需要担心自己管理内存。取而代之的是,我们可以将时间和注意力集中在应用程序真正有意思的部分,比如实现新的特征,简化用户界面,或者提高稳定性和性能等等。

此外,苹果公司已经在ARC模式下提高了内存管理的性能。例如,ARC的自动保留和释放调用比同等的手动内存管理要快了2.5倍。新的@autoreleasepool程序块比老的NSAutoReleasePool对象要快了6倍,并且objec_msgSend()甚至都要比原先快了33%。最后这一点是尤其重要的,因为objec_``nsgSend()是用来调度应用中每个Objective-C方法的调用。

总体来说,ARC让Objective-C更容易学,生产效率更高,更易于维护,更加安全、更加稳定并且更加快速。这就是我所说的多赢局面。大部分情况下,我们甚至不需要考虑内存管理。我们仅仅需要写代码,创建和使用对象就可以了。ARC会为我们处理所有复杂的细节。

比较ARC和垃圾收集

ARC不是垃圾收集。它们有同一个目标,两者都是自动内存管理技术,让应用开发变得更加简单。然而,它们用了不同的方法。垃圾收集在运行时追踪对象。当它确定对象不再使用时,它就会从内存中删除对象。

不妙的是,这产生了几个潜在的性能问题。基础代码需要监视并且删除对象,这便在我们应用中加入了额外的负担。我们同样也几乎控制不了垃圾收集器启动扫描。虽然现代的垃圾收集器尝试减少它们对应用程序性能产生的影响,但是它们天生就是无法确定的。这意味着垃圾收集器会导致你的应用程序变慢或者在应用程序执行过程中会随时暂停。

而ARC在编译的时候就完成了所有的内存管理。在运行的时候就没有额外的负担,事实上,由于大量的优化算法,ARC代码比手动内存管理运行地更快。此外,内存管理系统是完全确定的,这就意味着不会存在无法预料的事情发生。

找到并且消除循环保留
虽然ARC代表了内存管理的巨大进步,但是它没有完全让开发者们不用思考有关内存的问题。在ARC的模式下仍然有可能造成内存泄漏,ARC仍然容易受保留循环的影响。

为了弄清楚这个问题,我们需要窥视隐藏在背后的东西。默认情况下,所有ARC中的变量都是采用强引用。当你把一个对象赋给强引用时,系统会自动保留该对象。当你从引用中删除中这个对象时(通过赋值一个新的对象给这个变量或者通过赋值nil给这个变量),系统就会释放这个对象。

当两个对象通过强引用直接或间接互相引用对方时,就会产生保留循环。这通常在父子层级中会发生,当子对象持有父对象的引用时。

想象一下我有一个person对象,定义如下:

@interface Person : NSObject
@property (nonatomic, strong) Person* parent;
@property (nonatomic, strong) Person* child;
+ (void)myMethod;
@end
然后myMethod实现如下:

+ (void)myMethod {
     Person* parent = [[Person alloc] init];
     Person* child = [[Person alloc] init];
          parent.child = child;
     //使用parent和child做一些有用的事
}

在这个例子中,当我们调用myMethod的时候,两个Person对象就创建了。每个都以保留计数1开始。接着我们将child对象赋值给parent的child功能。这使child的保留计数增加到2。

ARC自动地在方法的末尾插入释放的调用。这让parent的保留计数降为0,child的保留计数降为1。由于parent的保留计数现在等于0,它就被删除了。接着,ARC自动释放了所有parent的属性。因此,child对象的保留计数也降为0并且它也被删除了,所有的内存都被释放了,就像我们所期望的一样。

现在,让我们添加一个保留循环。如下所示改变myMethod:

+ (void)myMethod {
     Person* parent = [[Person alloc] init];
     Person* child = [[Person alloc] init];
          parent.child = child;
**child.parent = parent;**
     //使用parent和child做一些有用的事
}

和之前一样,我们创建了parent和child对象,每个对象的保留计数都是1。这次,parent获得了child的一个引用,child也获得了parent的一个引用,这让双方的引用计数都增加到2。在程序的末尾,ARC自动释放了对象,让两个对象的引用计数都减少到1。由于两个对象的引用计数都不是0,所以两个对象都没有被删除,所以我们的保留循环就造成了内存泄漏。

幸运的是,ARC有一个解决办法。我们只需要重新定义parent属性,让它使用置零弱引用(zeroing weak reference)。本质上,这意味着将属性的特性从strong改为weak。

@interface Person : NSObject
@property (nonatomic, **weak**) Person* parent;
@property (nonatomic, strong) Person* child;
+ (void)myMethod;
@end

置零弱引用有两个优点。首先,它们不会增加对象的保留计数,因此它们也不会延长对象的生命周期。其次,当它们指向的对象删除后,系统就会自动将它们设为nil。这也避免了野指针的产生。

应用一般都会有从一个根对象延伸出对象层级图。当引用图的上层对象(任何更靠近图的根对象)时,我们通常会使用置零弱引用。此外,对于所有委托和数据源,我们应该也使用置零弱引用(请看本章后面的“委托”一节)。这是Objective-C的一个标准惯用法,因为它有助于避免由于粗心大意造成的保留循环。

目前为止,我们所见过的循环都是很明显的,但是并不总是这样。保留循环可以包含任何多的对象,只要它最终指向自身就行。一旦你超过了三或四层的引用,追踪可能的循环几乎就变得不可能了。幸运的是,开发工具又一次拯救了我们。

在Xcode 4.2中,Instruments目前有搜索保留循环的能力,并且会图形化呈现出保留循环。

只要分析一下你的应用程序。在主菜单中选择Product > Profile。在Instrument的弹出窗口中选择Leaks模板并点击Profile(见图2.3)。这样会同时启动Instruments和应用程序。

Instruments将会以两个诊断工具开始。Allocations会追踪内存分配和释放,Leaks会查找泄漏的内存。Leaks将会在Leaks instrument中以红色条块的形式显示。选择Leaks instrument行,详情就会显示在下面。在跳转工具条中,把细节视图从Leaks改为Cycles,然后Instruments就会显示保留循环(见图2.4)。现在就可以双击这个讨厌的引用,Xcode会自动定位它。修复它(通过将其转变为置零弱引用,或者通过重构应用程序),并且再次测试。我们将会在补充章节B节中详细讨论Instruments(www.freelancemadscience.com/book)。


ddd7f72924268d821e53ae73f44d48766f75f931

使用规则
ARC使用起来难以置信地容易。大部分时间里,我们不需要做任何事。Xcode 4.2的所有项目模板都默认使用ARC,虽然我们如果需要的话可以在某个文件上启用手动内存管理。这让我们自由地将ARC和非ARC代码结合在一起,并且ARC可以在使用iOS 4.0及以上版本的项目中使用(会有一些限制)。Xcode甚至还有工具可以转换非ARC引用的应用程序(选择Edit > Refactor > Convert to Objective-C ARC)。

为了让ARC正确地运转,编译器必须正确地理解我们的意图。这就需要我们遵守一些额外的规则来帮助消除所有的歧义性。如果你不理解这些规则的有关内容(这样的情况在日常的编程中极少发生),不用担心。最重要的是,编译器会执行所有的这些规则,打破它们就会产生错误。这就让问题很容易找出来并修复。

所有的规则如下列所示。

不要在C结构体中使用对象指针。而是创建一个Objective-C的类来存储这些数据。
不要创建名称以“new”开头的属性。
不要调用dealloc、retain、release、retainCount或者autorelease方法。
不要实现自己的retain、release、retainCount或者autorelease方法。你可以实现dealloc方法,但是通常没有必要。
在ARC中实现dealloc方法时,不要调用[super dealloc``]。编译器会自动调用。
不要使用NSAutoreleasePool对象,而是使用新的@autoreleasepool``{}程序块。
一般来说,不要在id和void*之间做类型转换。当在Objective-C对象和Core Foundation类型之间移动数据,应该使用增强的类型转换或者宏指令(参考下一节)。
不要使用NSAllocateObject或者NSDeallocateObject。
不要使用内存区(memory zones)或者NSZone。
ARC和自由桥接(Toll-Free Bridging)
首先,要明白ARC只能应用于Objective-C对象,这是非常重要的。如果在堆上给任何C数据结构分配内存,你还是需要自己管理内存。

当使用Core Foundation数据类型时,真正的问题才会发生。Core Foundation是一个低级的C API,提供了许多基本的数据管理和OS服务。有个类似的Objective-C框架也命名为Foundation(你困惑了吗?)。不必惊讶,Foundation和Core Foundation是有着紧密的联系的。事实上,Foundation为很多Core Foundation服务和数据类型提供了Objective-C 接口。

现在让我们来看一些很酷的东西。很多Foundation和Core Foundation数据类型都是可以相互转换的。本质上,NSString和CFString是完全相等的。我们可以随意地将CFString的引用传递给接受NSString类型参数的Objective-C方法,或者将NNString引用传递给接受CFString类型参数的Core Foundation中的函数。这种互操作性称为自由桥接(参见苹果公司的Cocoa Fundamentals Guide > Cocoa Objects > Toll-Free Bridging以便查看更多信息)。

不妙的是,当来来回回进行类型转换,内存管理就变得混乱了。哪个数据由谁来管理?我们要使用CFRetain()和CFRelease()来手动管理内存吗,还是就让ARC自己完成?

有三个基本情况我们需要注意:由Objective-C方法返回的Core Foundation数据;使用Core Foundation内存管理函数的代码;没有使用任何内存管理函数把数据传入Core Foundation或者从其接收数据的代码。

第一个情况比较简单。如果我们能够通过调用Objective-C方法访问Core Foundation对象,我们就不需要做任何事了。我们可以随意把数据做类型转换,并且编译器也不会产生任何错误。

这样是能运行的,因为所有的Objective-C方法都遵循Objective-C的内存管理约定(参见苹果公司的Memory Management Programming Guide以便查看更多信息)。ARC能理解这些规定,正确地解释出数据的归属,并且进行正确的操作。

UIImage* myImage = [UIImage imageNamed:@"myPhoto"];
id myCGImage = (id)[myImage CGImage];
[photoArray addObject:myCGImage];

在这个例子中,我们调用了UIImage的CGImage属性。它返回了一个CGImageRef。然后我们把这个C数据类型转换成Objective-C的id,并且将其放在一个数组中。因为我们用Objective-C方法来访问C数据,所以编译器允许简单的类型转换,然后剩下的事情由ARC来处理。

第二个情况中,如果我们调用任何Core Foundation内存管理函数,我们需要让ARC知道。这些函数包括CFRetain``()、CFRelease()和任何在名称中含有Create和Copy的函数(参见苹果公司的Memory Management Programming Guide关于Core Foundation的内容获取更多的信息)。

如果Core Foundation函数保留了数据(CFRetain、Create或者Copy),我们需要使用_bridge_transfer标记做类型转换。如果Core Foundation释放了这些数据(CFRelease),然后我们就需要_bridge_ retain标记做类型转换。下面是这两个操作的示例:

// Core Foundation函数保留了数据
// 当我们完成时需要让ARC释放它
NSString* myString =
     (__bridge_transfer NSString*)CFStringCreateMutableCopy(NULL, 0,
                                                                   myCFString);
//...现在使用myString来做些有用的事情
// 这次,Core Foundation代码会释放对象
// 我们需要让ARC来保留它
//CFStringRef myCFString = (__bridge_retain NSString*)[myString copy];
...现在用myCFString来做些有用的事情
CFRelease(myCFString);
最后一种情况,如果我们用其他的方式获得了Core Foundation数据(不是Create或者Copy函数,也不是Objective-C方法),并且我们没有调用CFRetain``()或者CFRelease()函数,我们只需要在类型变换中加入_``bridge标记就可以了,如下所示。ARC就会像平常一样为我们管理内存。

// 从Objective-C到Core Foundation
CFStringRef myCFString = (__bridge CFStringRef) myString;
CFShow(myCFString);
...
// 从Core Foundation到Objective-C
myLabel.text = (__bridge NSString*)myCFString;
认识参考文档

花一些时间来熟悉Xcode的参考文档是非常值得的。初学Objective-C时,这是一个非常好的资源,并且它在你的iOS开发职业生涯中,会一直起着不可或缺的参考指南的作用。

让我们来做一个小练习。打开参考文档并且查找UIWindow,接着从结果中选择UIWindow Class Reference。类参考分为好几个部分。它以类的简单描述开始,包括它所继承的类层级,它所实现的协议,以及一些使用该类的示例代码。

下面是关于类的概览。这个部分描述了它的基本用途,包括所有重要的实现和使用细节。

接着就可以看到任务列表了。它将类的特性和方法按照它们的预期用途分组(对于UIWindow来说,这些分组就是Configuring Windows、Making Windows Key、Converting Coordinates和Sending Events)。列表上的每一项都有一个超级链接,链接到文档后面部分更加详细的描述信息。

然后就是描述类特性、类方法、常量和通知的部分了。(不是所有的类都有这些类别。例如UIWindow就没有类方法。)每个部分都包含了该类定义的所有相关的公有条目。如果想要看父类中所声明的条目(或者是类层级中更高的类),你就需要查找相应的类参考文档。

每项的详细条目呈现了该项是如何声明的,描述了它的用途,并且提供了可用性相关的信息。例如,通过UIWindow的rootViewController属性可以访问管理该窗口内容的UIViewController。它的默认值是nil,并且它只有在iOS 4.0和更高的版本中才可用。

相比之下,CGPoint和UIWindow类不一样,CGPoint没有自己单独的参考。它是在CGGeometry参考中被描述的。和类参考一样,这个参考也是以简短的描述和概览开始。接着它列出了按照任务分组的几何函数。最后参考文档列出了所有相关的数据类型和常量。

CGPoint的条目包含了结构体的声明和它所有的域和可用性的描述。

当我们在学习这本书时,会定期地抽时间看看参考文档中的新类和结构。我会尝试展示一些类的典型用途的好例子,但是大部分类都包含太多的方法和特征了,不可能详细地讲解每个细节。此外,经常温习参考文档将会拓宽和启迪你的思路,有助于你在开发自己的应用程序时找到更好的方法。

相关文章
|
iOS开发 异构计算
如何增加 iOS APP 虚拟地址空间及内存上限?XNU 内核源码解读
1. 引言 最近一段时间在做钉钉 iOS 内存专项治理,解决内存不足时的 jetsam 事件及 malloc 的异常崩溃。在进程创建时系统会为每个 app 设定内存最大使用上限,内核会维护一个内存阈值优先级列表,当设备内存不足时低优先级的 app 会首先被内核中止进程。在阅读 XNU 内核源码过程中我们发现提供系统了两种能力可以扩展 App 的虚拟地址空间(com.apple.developer.kernel.extended-virtual-addressing)和增加内存使用上限(com.apple.developer.kernel.increased-memory-limit)。
1463 0
如何增加 iOS APP 虚拟地址空间及内存上限?XNU 内核源码解读
|
存储 算法 Java
iOS开发 - 穿针引线之内存管理(二)
iOS开发 - 穿针引线之内存管理
194 0
|
存储 程序员 C语言
iOS开发 - 穿针引线之内存管理(一)
iOS开发 - 穿针引线之内存管理
160 0
|
存储 算法 iOS开发
|
存储 监控 Unix
iOS-底层原理36:内存优化(一) 野指针探测
iOS-底层原理36:内存优化(一) 野指针探测
576 0
iOS-底层原理36:内存优化(一) 野指针探测
|
存储 安全 API
iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析
iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析
182 0
iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析
|
iOS开发
iOS-底层原理 33:内存管理(二)强引用分析
iOS-底层原理 33:内存管理(二)强引用分析
167 0
iOS-底层原理 33:内存管理(二)强引用分析
|
存储 编译器 Serverless
iOS-底层原理 33:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
iOS-底层原理 33:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
195 0
iOS-底层原理 33:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析
|
存储 安全 程序员
iOS-底层原理 24:内存五大区
iOS-底层原理 24:内存五大区
129 0
iOS-底层原理 24:内存五大区
|
存储 机器学习/深度学习 算法
iOS-底层原理 05:内存对齐原理
iOS-底层原理 05:内存对齐原理
215 0
iOS-底层原理 05:内存对齐原理