iPhone 和 iPad 设备的内存资源非常有限。如果某个应用的内存使用量超过了单个进程的上限,那么它就会被操作系统终止使用。1正是由于这个原因,成功的内存管理在 iOS 应用的实现过程中扮演着核心的角色。
苹果公司在2011 年的全球开发者大会上指出,90% 的应用崩溃与内存管理有关。其中最主要的原因是错误的内存访问和保留环所引起的内存泄漏。
与(基于垃圾回收的)Java 运行时不同,Objective-C 和Swift 的iOS 运行时使用引用计数。使用引用计数的负面影响在于,如果开发人员不够小心,那么可能会出现重复的内存释放和循环引用的情况。
内存消耗指的是应用消耗的 RAM。
iOS 的虚拟内存模型并不包含交换内存,与桌面应用不同,这意味着磁盘不会被用来分页内存。最终的结果是应用只能使用有限的 RAM。这些 RAM 的使用者不仅包括在前台运行的应用,还包括操作系统服务,甚至还包括其他应用所执行的后台任务。
一、栈大小
应用中新创建的每个线程都有专用的栈空间,该空间由保留的内存和初始提交的内存组成。栈可以在线程存在期间自由使用。线程的最大栈空间很小,这就决定了以下的限制。
• 可被递归调用的最大方法数每个方法都有其自己的栈帧,并会消耗整体的栈空间。例如,如例 1 所示,如果你调用main,那么main 将调用method1,而method1 又将调用method2,这就存在三个栈帧了,且每个栈帧都会消耗一定字节的内存。图 2-1 展示了线程栈随时间的变化。
例1调用树
main()
{
method1();
}
method1()
{
method2();
}
• 一个方法中最多可以使用的变量个数所有的变量都会载入方法的栈帧中,并消耗一定的栈空间。
• 视图层级中可以嵌入的最大视图深度渲染复合视图将在整个视图层级树中递归地调用layoutSubViews 和drawRect 方法。如果层级过深,可能会导致栈溢出。
- 包含每个方法的栈框架的栈
二、堆大小
每个进程的所有线程共享同一个堆。一个应用可以使用的堆大小通常远远小于设备的 RAM值。例如,iPhone 5S拥有大约1GB的RAM,但分配给一个应用的堆大小最多不到 512MB。应用并不能控制分配给它的堆。只有操作系统才能管理堆。
使用NSString、载入图片、创建或使用 JSON/XML 数据、使用视图等都会消耗大量的堆内存。如果你的应用大量使用图片(与 Flickr 和 Instagram 应用类似),那么你需要格外关注平均值和峰值内存使用的最小化。
图 2 展示了可能出现在一个应用某个时刻的一个典型堆。
在图 2 中,由main 方法启动的主线程创建了UIApplication。我们假设某个时间点的窗体包含了一个UITableView,当必须渲染表格中的一行时,UITableView 调用了UITableViewDataSource的tableView:cellForRowAtIndex: 方法。
通过名为photos 的NSArray 属性,数据源引用了全部的照片。如果处理不够谨慎,这个数组将会非常大,从而导致很高的峰值内存使用。解决方案之一是在数组中存储固定数量的图片,并在用户滚动视图时换入或换出图片。这个固定的数值将决定此应用的平均内存使用。
图2
图2:在 UITableViewDataSource 中展示 HPPhoto 模型使用情况的堆
数组中的每一项都是HPPhoto 类型,代表了一张照片。HPPhoto 储存了与对象有关的数据,如照片的尺寸、创建日期、拥有者信息、标签、与照片关联的网络 URL(图中没有展示)、对本地缓存的引用(图中没有展示),等等。
与通过类创建的对象相关的所有数据都存放在堆中。
类可能包含属性或值类型的实例变量(iVars),如int、char 或struct。但因为对象是在堆内创建的,所以它们只消耗堆内存。
当对象被创建并被赋值时,数据可能会从栈复制到堆。类似地,当值仅在方法内部使用
时,它们也可能会被从堆复制到栈。这可能是个代价昂贵的操作。例
2
重点展示了从栈
复制到堆以及从堆复制到栈的情况。
保持应用的内存需求总是处于 RAM 的较低占比是一个非常好的主意。虽然没有强制规定,但强烈建议使用量不要超过 80%~85%,要给操作系统的核心服务留下足够多的内存。不要忽视 didReceiveMemoryWarning 信号。