内存简介
在Instruments的Allocations模板中,可以看到主要统计的是All Heap & Anonymous VM的内存使用量。All Heap就是App运行过程中在堆上分配的内存,Anonymous VM是什么呢?按照官方描述,它是和App进程关联比较大的VM regions(虚拟内存)。
当我们向系统申请内存时,系统并不会给你返回物理内存的地址,而是给你一个虚拟内存地址。只有我们开始使用申请到的虚拟内存时,系统才会将虚拟地址映射到物理地址上,从而让程序使用真实的物理内存。比如进程A和B都拥有1到4的虚拟内存。系统通过虚拟内存到物理内存的映射,让A和B都可以使用到物理内存。
系统会对虚拟内存和物理内存进行分页,虚拟内存到物理内存的映射都是以页为最小粒度的。
虚拟内存分页(Virtual Page, VP) 有两种类型:
1.Clean -指的是能够被系统清理出内存且在需要时能重新加载的数据,包括 Memory mapped files、Frameworks 中的 __DATA__CONST 部分、应用的二进制可执行文件。
2.Dirty -指的是不能被系统回收的内存占用,包括
所有堆上的对象图片解码缓冲数据(Decoded image buffers)、Frameworks 中的 __DATA 和 __DATA_DIRTY部分。
为了更好的管理内存页,系统将一组连续的内存页关联到一个VMObject上,我们在Instruments的Anonymous VM里看到的每条记录都是一个VMObject或者也可以称之为VM Region。堆区会被划分成很多不同的VM Region,不同类型的内存分配根据需求进入不同的VM Region。除了MALLOC_LARGE和MALLOC_SMALL外,还有MALLOC_TINY, MALLOC metadata等等
- Dirty Size:如果一个内存页想要被复用,必须将内容写到硬盘上的话,这个内存页就是Dirty的。
- Swapped Size:交换到硬盘上的大小,仅OSX可用
- Resident Size:实际使用物理内存的大小
- Virtual Size:顾名思义,就是虚拟内存大小,将一个VM Region的结束地址减去起始地址就是这个值
但 iOS 系统从 iOS 7 开始使用 Compressed memory技术,在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度.本质上,Compressed memory 也是 Dirty memory.通常情况下,我们所说的减少内存占用是指 Dirty Memory 和 Compressed Memory。
当内存占用超过一定值的时候会收到内存警告。按照以往的习惯,你可能会在收到内存警告通知的时候去做一些释放内存的事情。然而内存压缩机制会使事情变得复杂。 假设一个 App 的 Dirty memory 中有一个NSDictionary 对象占用了3个 page的内存空间,当 App 处于非活跃状态时系统将其压缩至1个 page的压缩大小
,系统获得了2个 page 大小的可用内存。
但是,如果这时因为一些原因收到内存警告,我们可能会决定将 NSDictionary中的一些数据移除,这时我们重新访问了压缩后的page,它被解压 - 释放对象 -
然后内存占用又回到了1个page大小。也就是说,我们努力释放了一些对象却没有增加可用内存空间,甚至可能会加剧内存紧张的态势,也增加了 CPU 的时间开销。
相比于使用字典缓存,苹果 更推荐使用NSCache。因为 NSCache 可以自动清理内存,在内存吃紧的时候会更加合理。
内存分析工具
Instruments
Xcode的Instruments为我们提供了一系列的工具来帮助我们debug。leaks可以帮我们检测出Leaked memory(无主内存),但在ARC时代更多的是循环引用导致的Abandoned memory,这部分可以用Allocations的 Mark Generation的方式来检测。还可以追踪程序的虚拟内存占用和堆信息,调用栈信息等。
Memory Debug Graph
Xcode 8 新增了 Memory Debug Graph ,开发测试一段时间后可以看下Xcode Runtime issue,是否有警告。
Product -> Scheme -> Edit Scheme -> Diagnostics 中,开启 Malloc Stack 功能,建议使用 Live Allocations Only 选项
File->Export Memory Graph 将其导出为 memgraph 文件。
介绍一下相关的命令:
//查看摘要报告
vmmap --summary xx.memgraph
//只显示CG image相关的数据
vmmap xx.memgraph | grep 'CG image'
//查看是否有内存泄漏
leaks xx.memgraph
//查看某处的内存泄漏
leaks --traceTree [内存地址] xx.memgraph
//更多使用查看leaks文档
man leaks
//按照内存大小查看所有堆区对象的内存使用
heap xx.memgraph -sortBySize
//查看内存分配历史
malloc_history xx.memgraph [address]
MLeaksFinder:
可以不侵入代码也不用打开Instruments。自动检测UIViewController 和UIView对象的内存泄露,而且也可以扩展以检测其它类型的对象。而且只在debug下开启,完全
不影响你的 release 包。
他的原理是当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的view,
view 的 subviews等等将很快被释放,于是,只需在
一个 ViewController 被 pop 或 dismiss 一小段
时间后,看看该 UIViewController,它的view,view 的 subviews等等是否还存在。但对于有些ViewController,在被 pop 或dismiss后,不会被释放(比如
单例),因此需要提供机制让开发者指定哪个对象不会被释放,这里可以通过重载上面的-willDealloc 方法,直接 return NO即可。某些系统的私有View,
不会被释放(可能是系统bug或者是系统出于某些原
因故意这样做的,因此需要建立白名单。
MLeaksFinder 提供了一个手动扩展的机制,可以从 UIViewController 跟UIView出发,去检测其它类型的
对象的内存泄露。如下所示,我们可以检测 UIViewController 底下的 View Model:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}
宏 MLCheck() 做的事就是为传进来的对象建立 View-ViewController stack 信息,并对传进来的对象调用 -willDealloc 方法。
结合对象的生命周期来分析内存的真正使用情况:
(1)单例或者被 cache 起来复用。在第一次 pop 的时候报 Memory Leak,在之后的重复 push 并 pop 同一个 ViewController 过程中,即不报 Object Deallocated,也不报 Memory Leak。
(2)释放不及时。在第一次 pop 的时候报 Memory Leak,在之后的重复 push 并 pop 同一个 ViewController 过程中,对于同一个类不断地报 Object Deallocated 和 Memory Leak。
(3)真正的内存泄漏。在第一次 pop 的时候报 Memory Leak,在之后的重复 push 并 pop 同一个 ViewController 过程中,不报 Object Deallocated,但每次 pop 之后又报 Memory Leak。这种情况下每回进入并退出一个页面后,就报有新的内存泄漏,同时被报泄漏的对象又从来没有释放过。
图片
对于 iOS 系统而言,绝大部分场景下图片数据占内存最多,值得注意的是图片所占内存的大小与图片的尺寸有关,而不是图片的文件大小。
例如:有一个 590KB 的图片,分辨率是 2048px * 1536px,它实际使用的内存不是 590KB,而是2048 * 1536 * 4 = 12 MB。。
在 iOS 设备上支持四种图片渲染格式,每种格式有着不同的 bitsPerComponent 和适用场景:
- sRGB:这个是目前比较通用的全色彩图像色域,每个像素占 4 个字节
- Wide:每个像素占 8 个字节,相比 sRGB 能表示的颜色更多,但是因为其较大的内存开销需要谨慎使用
- 亮度和 alpha 8 格式:每像素 2 个字节,单色图像和 alpha,metal 着色器。
- Alpha 8 格式:每个像素 1 个字节,用于单色图像,适用于如阴影、无emoji文字等
(1)这么多的格式,苹果 建议我们使用在 iOS 10 引入的 UIGraphicsImageRenderer 类替换UIGraphicsBeginImageContextWithOptions完成绘制任务,它在 iOS 12 中会根据场景自动选择最合适的渲染格式,更加合理地使用内存。(在使用UIGraphicsBeginImageContext和UIGraphicsEndImageContext时必须成双出现,不然会造成context泄漏。另外XCode的Analyze也能扫出这类问题)
参考资料
iOS Memory Deep Dive
wereadteam.github.io/2016/07/20/…
作者简介
就职于甜橙金融(翼支付)信息技术部,负责 iOS 客户端开发