WWDC 2018:iOS 内存深刻研究

WWDC 2018 Session 416:iOS Memory Deep Dive缓存

查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录bash

做者:高老师,纯洁善良有理想的 iOS 开发一枚session

引言

对于咱们的 App 所依赖的设备而言,内存资源是有限的。下降 App 所使用的内存能够提升性能和体验,相反,过大的内存占用可能会致使 App 被系统强制退出。因此每一个 iOS 开发者都应该关注内存问题。这一节新的内容很少,基本上都是一些老的知识点。app

按照 Session 的套路,咱们先看一下提纲:ide

  • 为何要减小内存使用
  • 内存占用
  • 分析内存占用工具
  • 图像
  • 在后台时,对内存优化
  • 演示 Demo

那咱们就按顺序开始啦!工具

为何要减小内存

在探讨内存以前,咱们要知道为何要减小内存。简单的回答是能够有更好的用户体验:更快的启动速度,不会由于内存过大而致使 Crash,可让 App 存活更久等。post

内存占用

并不是全部 App 的内存占用都是相同的。在继续探讨 iOS 上 App 的内存使用以前,咱们先来聊一下Pages Memory性能

Pages Memory

内存是由系统管理,通常以页为单位来划分。在 iOS 上,每一页包含 16KB 的空间。一段数据可能会占用多页内存,所占用页总数乘以每页空间获得的就是这段数据使用的总内存。优化

内存页按照各自的分配和使用状态,能够被分为 CleanDirty 两类。spa

以上面的代码为例,申请一块长度为 80000 字节的内存空间,按照一页 16KB 来计算,就须要 6 页内存来存储。

  • 当这些内存页开辟出来的时候,它们都是 Clean
  • 当向处于第一页的内存写入数据时,第一页内存会变成 Dirty
  • 当向处于最后一页的内存写入数据时,这一页也会变成 Dirty

内存映射文件

当 App 访问一个文件时,系统内核会负责调度,将磁盘上的文件加载并映射到内存中。若是这是只读的文件,它所占用到的内存页是 Clean 的。

以下图所示,一个 50KB 的图片被加载到内存中时,须要分配 4 页内存来存储。其中第四页中有 2KB 的空间会被用来存储这个图片的数据,剩余空间可能会被用来存储其它数据。

典型app内存类型

当内存不足的时候,系统会按照必定策略来腾出更多空间供使用,比较常见的作法是将一部分低优先级的数据挪到磁盘上,这个操做称为 Page Out。以后当再次访问到这块数据的时候,系统会负责将它从新搬回内存空间中,这个操做称为 Page In

然而对于移动设备而言,频繁对磁盘进行IO操做会下降存储设备的寿命。从 iOS7 开始,系统开始采用压缩内存的办法来释放内存空间,被压缩的内存称为 Compressed Memory。下面依次介绍一下 iOS App 一般状况下的三种内存类型:Clean MemoryDirty Memory以及Compressed Memory

Clean Memory

Clean Memory 是指那些能够用以 Page Out 的内存,包括已被加载到内存中的文件,或者是 App 所用到的 frameworks。每一个 frameworks 都有 _DATA_CONST 段,当 App 在运行时使用到了某个 framework,它所对应的 _DATA_CONST 的内存就会由 Clean 变为 Dirty。

Dirty Memory

Dirty Memory 是指那些被 App 写入过数据的内存,包括全部堆区的对象、图像解码缓冲区,同时,相似 Clean memory,也包括 App 所用到的 frameworks。每一个 framework 都会有 _DATA 段和 _DATA_DIRTY 段,它们的内存是 Dirty 的。

值得注意的是,在使用 framework 的过程当中会产生 Dirty Memory,使用单例或者全局初始化方法是减小 Dirty Memory 不错的方法,由于单例一旦建立就不会销毁,全局初始化方法会在 class 加载时执行。

Compressed Memory

当内存吃紧的时候,系统会将不使用的内存进行压缩,直到下一次访问的时候进行解压。

例如,当咱们使用 Dictionary 去缓存数据的时候,假设如今已经使用了 3 页内存,当不访问的时候可能会被压缩为 1 页,再次使用到时候又会解压成 3 页。

Memory Warnings

并不是全部内存警告都是由 App 形成的,例如在内存较小的设备上,当你接听电话的时候也有可能发生内存警告。按照以往的习惯,你可能会在收到内存警告通知的时候去作一些释放内存的事情。然而内存压缩机制会使事情变得复杂。咱们来看看这个例子:

假设代码中的 cache 已被压缩过

事实上,当你尝试去再次访问 cache 对象的时候,系统会先解压这块内存

这个过程当中内存使用会增长,在内存吃紧的时候,这并非咱们想要的。随后,当咱们会执行大量工做去清空 cache,最终获得的内存空间和内存压缩的结果同样

因此,相比以往的缓存手段,更加建议去调整策略,例如减小缓存使用,或者在收到内存警告的时候,将这类事情交由系统去处理。

Caching

咱们对数据进行缓存的目的是想减小 CPU 的压力,可是过多的缓存又会占用过大的内存。因为内存压缩机制的存在,咱们须要根据缓存数据大小以及重算这些数据的成本,在 CPU 和内存之间进行权衡。

在一些须要缓存数据的场景下,能够考虑使用 NSCache 代替 NSDictionary,由于 NSCache 能够自动清理内存,在内存吃紧的时候会更加合理。

小结

一般状况下,咱们所说的内存占用是指 Dirty MemoryCompressed MemoryClean Memory 不须要过多关心。

App 能使用比较多的内存空间,可是上限会根据设备不一样而不一样。Extension 能使用的最大内存则要低不少,因此当你在开发 Extension 的时候尤为要注意内存使用。当使用的内存超出限制的时候,系统会抛出 EXC_RESOURCE_EXCEPTION 异常。

分析内存占用工具

Xcode Memory Gauge

在 Xcode 中,你能够经过 Memory Gauge 工具,很方便快速的查看 App 运行时的内存状况,包括内存最高占用、最低占用,以及在全部进程中的占用比例等。若是想要查看更详细的数据,就须要用到 Instruments 了。

Instruments

Instruments 中,你可使用 AllocationsLeaksVM TrackerVirtual Memory Trace 对 App 进行多维度分析。

Debug Debugger-Memory Resource Exceptions

当你使用 Xcode 10 之前的版本进行调试时,在内存过大时,debug session 会直接终止,而且在控制台打印出异常。从 Xcode 10 开始,debugger 会自动捕获 EXC_RESOURCE RESOURCE_TYPE_MEMORY 异常,并断点在触发异常抛出的地方,十分方便定位问题。

Xcode Memory Debugger

经过这个工具,能够很直观地查看内存中全部对象的内存使用状况,以及相互之间的依赖关系,对定位那些由于循环引用致使的内存泄露问题十分有帮助。

你也能够点击 File->Export Memory Graph 将其导出为 memgraph 文件,在命令行中使用 Developer Tool 对其进行分析。使用这种方式,你能够在任什么时候候对过去某时的 App 内存使用进行分析。

简单介绍一下相关的命令

vmmap - 查看虚拟内存

查看详细报告

vmmap xx.memgraph

查看摘要报告

vmmap --summary xx.memgraph

配合管道命令查看全部动态库的Ditry Pages的总和

vmmap -pages xxx.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'

只显示CG image相关的数据

vmmap xx.memgraph | grep 'CG image'

更多使用方式请查看vmmap的文档

man vmmap

leaks - 查看泄漏的内存

查看是否有内存泄露

leaks xx.memgraph

查看某处内存的泄漏

leaks --traceTree [内存地址] xx.memgraph

更多使用方式请查看 leaks 的文档

man leaks

heap - 查看堆区内存

查看全部堆区对象的内存使用

heap xx.memgraph

默认状况下是按照对象数量进行排序,一般状况下它们不会形成什么内存问题。咱们须要关心的是那些为数很少,却占用了大量内存的对象,这时候就能够增长参数 -sortBySize,按照内存占用大小顺序来查看全部堆区对象的内存使用

heap xx.memgraph -sortBySize

当肯定是哪一个类型的对象占用了太多内存以后,能够获得每一个对象的内存地址

heap xx.memgraph -addresses all | 'XXBigData'

更多使用方式请查看 heap 的文档

man heap

有了这些对象的内存地址以后,咱们还须要另外一样工具帮助咱们作下一步分析。

Enabling Malloc Stack Logging

Product -> Scheme -> Edit Scheme -> Diagnostics 中,开启 Malloc Stack 功能,建议使用 Live Allocations Only 选项

以后 lldb 会记录调试过程当中对象建立的堆栈,配合 malloc_history 工具,就能够定位到那些占用了过大内存的对象是哪里建立的。

malloc_history - 查看内存分配历史

malloc_history xx.memgraph [address]

malloc_history xx.memgraph --fullStacks [address]

更多使用方式请查看 malloc_history 的文档

man malloc_history

选择哪一个工具?

上面讲述了那么多的分析工具,那咱们应该选择哪一种工具呢?苹果的工程师帮咱们作了以下整理:

你们能够根据上图所示,根据不一样的须要进行选择。

图片

对于 iOS 系统而言,绝大部分场景下哪类数据占内存最多呢?固然是图片!须要注意的是,图片所占内存的大小与图片的尺寸有关,而不是图片的文件大小。

例如:有一个 590KB 的图片,分辨率是 2048px * 1536px,它实际使用的内存不是 590KB,而是2048 * 1536 * 4 = 12 MB。。

图片为何会占用这么大的内存呢,这还要从图片在 iOS 上显示的原理提及,具体可移步到 WWDC 2018 Session 219:Image and Graphics Best Practices,也能够直接阅读小伙伴前几天刚发布的文章 WWDC2018 图像最佳实践

图片的格式

  • sRGB:这个是目前比较通用的全色彩图像色域,每一个像素占 4 个字节

  • Wide:每一个像素占 8 个字节,相比 sRGB 能表示的颜色更多

还有占内存更小的格式:

  • 亮度和 alpha 8 格式:每像素 2 个字节,单色图像和 alpha,metal 着色器。

  • Alpha 8 格式:每一个像素 1 个字节,用于单色图像,比 SRGB 小 75%

选择正确的格式能够减小了内存的使用。简单总结一下:

一个字节:Alpha 8
两个字节:亮度和alpha 8
四个字节:SRGB
八个字节:Wide 格式
复制代码

那下一个话题来了,如何选择正确的格式呢?

选择正确的格式

简单的回答是:不须要你来选择格式,而是应该让格式选择你。是否是以为一会儿松了一口气?哈哈😆

用 UIGraphicsImageRenderer 代替 UIGraphicsBeginImageContextWithOptions

使用 UIGraphicsBeginImageContextWithOptions 生成的图片,每一个像素须要 4 个字节表示。建议使用 UIGraphicsImageRenderer,这个方法是从 iOS 10 引入,在 iOS 12 上会自动选择最佳的图像格式,能够减小不少内存。

另外,若是想修改颜色,能够直接修改 tintColor,不会有额外的内存开销。

Downsampling

当你缩小一幅图像的时候,会按照取平均值的办法把多个像素点变成一个像素点,这个过程称为 Downsampling

UIImage 在设置和调整大小的时候,须要将原始图像加压到内存中,而后对内部坐标空间作一系列转换,整个过程会消耗不少资源。咱们可使用 ImageIO,它能够直接读取图像大小和元数据信息,不会带来额外的内存开销。

在后台时,对内存优化

假设在 App 里展现了一张很大图片,当咱们切换到后台去作其它的操做时,这个图片还在占用内存。咱们应该考虑在合适的时机去回收这类占用过大的数据。

演示Demo

Demo主要是用实际例子讲述了上面的知识点,这里就再也不重复讲解了,感兴趣的童鞋能够移步 iOS Memory Deep Dive

总结

内存是一个有限的共享资源,要学会使用 Xcode 分析内存工具,从而了解应用程序内存占用状况,并使用一些缩减应用程序内存占用空间的技巧和窍门。

PS:有理解认知不正确的,欢迎指正!

相关文章
相关标签/搜索