[ WWDC2018 ] - 深入解析iOS内存 iOS Memory Deep Dive

Session 416 由三位苹果软件工程师 Kyle Howarth, James Snee, Kris Markel 为我们带来 iOS 内存相关的一些内容

  • Memory Usage Performance Guidelines 不再更新之后,这个 Session 简单介绍了一下 iOS 的虚拟内存机制的变化,如 Compressed memory 的使用等,分析了开发者应该减少哪部分内存占用。
  • Xcode 10 现在可以捕获内存超限的 EXC_RESOURCE_EXCEPTION 事件,其底层记录内存信息的 memgraph 文件与命令行工具的结合使用,使得内存相关的调试更加灵活高效。
  • 推荐开发者通过新的 API 让系统选择最佳的图片渲染格式来合理使用内存;相比于UIImage的绘制,图片下采样时使用ImageIO来减少损耗。
  • 减少应用处在后台时较大的内存占用,主要通过监听 App 生命周期的通知或利用VC的生命周期方法实现,使系统或其他进程获得更多的可用内存。

1. Why Reduce Memory?

开门见山,我们为什么要减少内存(占用)?

为了更好的用户体验

内存是有限且系统共享的资源,一个程序占用更多,系统和其他程序所能用的就更少。程序启动前都需要先加载到内存中,并且在程序运行过程中的数据操作也需要占用一定的内存资源。减少内存占用也能同时减少其对 CPU 时间维度上的消耗,从而使不仅你所开发的 App,其他 App 以及整个系统也都能表现的更好。

2. Memory Footprint

我们需要明确的是,这里的减少内存指减少 iOS App 的虚拟内存(Virtual Memory) 占用。

iOS 以及 macOS 都采用了虚拟内存技术来突破物理内存(RAM) 的大小限制,每个进程都拥有一段由多个大小相同的 page 所构成的逻辑地址空间。处理器和内存管理单元 MMU(Memory Management Unit) 维护着由逻辑地址空间到物理地址的 page 映射表,当程序访问逻辑内存地址时由 MMU 根据映射表将逻辑地址转换为真实的物理地址。在早期的苹果设备中,每个 page 的大小为 4KB;基于 A7 和 A8 处理器的系统为 64 位程序提供了 16KB 的虚拟内存分页和 4KB 的物理内存分页;而在A9之后,虚拟内存和物理内存的分页大小都达到了 16KB。

虚拟内存分页(Virtual Page, VP) 有两种类型:

1.Clean - Data that can be paged out of memory
指的是能够被系统清理出内存且在需要时能重新加载的数据,包括:

  • Memory mapped files
  • Frameworks 中的 __DATA_CONST 部分
  • 应用的二进制可执行文件

2.Dirty - Any memory that has been written to by your app
指的是不能被系统回收的内存占用,包括

  • 所有堆上的对象
  • 图片解码缓冲数据(Decoded image buffers)
  • Frameworks 中的 __DATA 和 __DATA_DIRTY部分

Frameworks you link actually use clean memory and dirty memory

由于闪存容量和读写寿命的限制,iOS 上没有Disk swap机制,取而代之使用 Compressed memory。

Disk swap 是指在 macOS 以及一些其他桌面操作系统中,当内存可用资源紧张时,系统将内存中的内容写入磁盘中的backing store (Swapping out),并且在需要访问时从磁盘中再读入 RAM (Swapping in)。与大多数 UNIX 系统不同的是,macOS 没有预先分配磁盘中的一部分作为 backing store,而是利用引导分区所有可用的磁盘空间。

苹果最初只是公开了从 OS X Mavericks 开始使用 Compressed memory 技术,但 iOS 系统也从 iOS 7 开始悄悄地使用。从 OS X Mavericks Core Technology Overview 文档中可以了解到该技术在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,其特点可以归结为:

  • Shrinks memory usage 减少了不活跃内存占用
  • Improves power efficiency 改善电源效率,通过压缩减少磁盘IO带来的损耗
  • Minimizes CPU usage 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销
  • Is multicore aware 支持多核操作

本质上,Compressed memory 也是 Dirty memory

因此, memory footprint = dirty size + compressed size ,这也就是我们需要并且能够尝试去减少的内存占用。

1 (4).png

当 memory footprint 超过一定值时(这里给出了不同机型的测试结果),就会收到内存警告(Memory Warnings)。对于Extension来说,限制值更小,因此使用也需要更加谨慎。⚠️一些情况下,如果内存使用增长过快,App 有可能在尚未响应内存警告的情况下就已经被系统杀掉进程了。

Kyle 在这一部分给出了几点关于内存警告的看法:

(1).你的 App 不一定是真正的“凶手”

在一些 RAM 容量较低的机型上,App 使用过程中接到一个电话,也有可能触发内存警告。

(2).内存压缩技术的存在使得释放内存变得复杂

假设一个 App 的 Dirty memory 中有一个 NSDictionary 对象占用了3个 page 的内存空间,当 App 处于非活跃状态时系统将其压缩至1个 page 的压缩大小,系统获得了2个 page 大小的可用内存。

2.png
3.png

但是,如果这时因为一些原因收到内存警告,我们可能会决定将 NSDictionary 中的一些数据移除,这时我们重新访问了压缩后的page,它被解压 - 释放对象 - 然后内存占用又回到了1个page大小。也就是说,我们努力释放了一些对象却没有增加可用内存空间,甚至可能会加剧内存紧张的态势,也增加了 CPU 的时间开销。

4.png

(3).缓存策略

缓存选择实际上是 CPU 和内存性能开销的博弈,相比于使用字典缓存,Kyle 更推荐使用NSCache。NSCache 分配的内存实际上是 Purgeable Memory,可以由系统自动释放。这点在 Effective Objective 2.0 一书中也有推荐,NSCache 与 NSPureableData 的结合使用既能让系统根据情况回收内存,也可以在内存清理的同时移除相关对象。

3. Tools for profiling footprint

(1).为了更好寻找能够减少的内存占用,Xcode 和 Instruments 一直以来提供了一系列工具帮助我们进行 Debug:

  • Xcode memory gauge
    在 Xcode 的 Debug navigator 中可以通过 Xcode memory gauge 直接看到正在 debug 程序的内存占用情况,以及其他程序占用内存和系统总内存。为了查看更为详细的内存占用变化,可以使用 Instruments 相关工具。

  • Allocations
    追踪程序的虚拟内存占用和堆信息,提供对象的类名、大小以及调用栈等信息。

  • Leaks
    用于检测程序运行过程中的内存泄露,并记录对象的历史信息。

在检测内存泄露方面,三方库 MLeaksFinder 较为流行,能够不入侵代码且不用打开 Instruments,自动检测 UIViewController 和 UIView 对象的内存泄露,而且也可以扩展以检测其它类型的对象。

  • VM Tracker
    能够区分程序运行时前文所述的各种内存类型占用情况,Instruments User Guide 中给出了各个参数的具体定义。

  • Virtual Memory Trace
    隐藏在 System Trace 中的 Virtual Memory Trace 工具能够从 page 层面更深层次剖析应用程序的虚拟内存操作。 Syetem Trace in Depth-WWDC 2016 中给出了详细的介绍。

5.png

(2).在 Xcode 10 中,内存占用触发限制时,会有 EXC_RESOURCE_EXCEPTION 事件被捕捉到,继而可以利用各种手段分析研究内存占用情况,更有助于寻找问题根源。此外,从 Xcode 8 开始引入的 Debug memory graph 也更新了更好的布局方式。

6.png

(3).Xcode 使用 memgraph 的文件格式来储存应用程序的占用信息,因此导出 memgraph 文件可以结合命令行工具进行分析。能够虽然可视化工具已经能够直观的表现我们想要了解的内存占用信息,但是在终端中不仅可以灵活地利用各种命令和 flag 突出我们想要的内容,更可以快速的实现信息查找和文本化交互。这一部分苹果工程师为我们介绍了4个常用命令:

18.png
  • vmmap
vmmap App.memgraph
vmmap --summary App.memgraph
7.png

vmmap 能够打印出进程信息,所有分配给该进程的 VMRegions 以及 VMRegion 的种类、内存占用信息等内容。利用 --summary 则能够根据不同的 region type 打印出详细的内存占用类型和信息。这里需要注意的是 SWAPPED SIZE 在 iOS 上指的是 Compressed memory size 且其值表示压缩前的占用大小。

系统中将一系列连续的内存页关联到一个 VMObject 进行管理,VMRegion 即 VMObject 所管理IDE区域。 Finding iOS Memory 中对每种 VMRegion 作出了详细的解释。

此外,结合 grep 和 awk 命令,还可以进行制定 VMRegion 的信息查找。例如,以下命令以 page 而非字节为单位打印 App 中所有动态库所占内存大小。

vmmap -pages App.memgraph | grep .dylib | awk '{ sum += $6} END { print "Total Dirty Pages:" sum}'
output:Total Dirty Pages:1387
  • leaks
 leaks App.memgraph
 leaks --traceTree [address] App.memgraph
8.png

leaks 追踪堆中的对象,打印出进程中内存泄露情况、调用堆栈以及循环引用信息。利用 --traceTree 和指定对象的地址,leaks还能以树形结构打印出对象的相关引用。

9.png
  • heap
heap App.memgraph
heap App.memgraph -sortBySize
heap App.memgraph -address all | <classes-pattern>
10.png

heap 会打印出所有在堆上的对象信息,默认按类数量排序,也可以通过 -sortBySize 按大小排序,对于追踪堆中较大的对象十分有帮助。找到目标对象后,通过 -address 获得所有/指定类的地址,继而可以利用 malloc_history 寻找其调用堆栈信息。

11.png
  • malloc_history
malloc_history App.memgraph --fullStacks [address]

使用上述命令能够获得我们知道地址的对象的调用堆栈信息,它能够得到的比 memory inspector 中 Backtrace 更加详细。但是需要开启 Dignostics 中的 Malloc Stack 选项,才能通过 malloc_history 获得 memgraph 记录的调用堆栈信息。

12.png
  • To see object creation: malloc_history
  • To see what references an object in memory: leaks
  • To see how large a region or an instanceSize: heap & vmmap

上述命令都有着不同的适用场景,与可视化工具的结合能够更大的发挥它们的作用。比如当进入 Debug memory graph 模式时,可以直接通过点击下方导航栏的叹号查看内存泄露,可视化的内存泄露更为直观。发现泄露对象后,可以再使用命令行查看相对复杂的引用关系和调用堆栈。或者当程序运行后,用 vmmap/heap 查看详细的内存占用情况,然后进一步查看具体占用的 region type/class name 并得到对象地址,用 malloc_history 获取调用堆栈发现问题。

接下来两部分,苹果工程师针对内存的使用给出了一些建议。

4. Images

图片在内存使用上很容易产生较大的占用,如下图所示,一个图片文件从硬盘到展示需要经历加载-解码-渲染三步。以一个590KB大小、2048 * 1536 像素的图片为例,在3x设备上解码后的内存占用能够达到10MB(2048 * 3 * 1536 * 3 * 4 Bytes/pixel)之多。更深层次的图像相关实践在 Image and Graphics Best Practices 中介绍,这里我们需要知道:

Memory use of an image is related to the dimensions, not the file size in disk

13.png

因此,在解码图片时要注意所选择的图片分辨率大小,对于一些分辨率过大的图片,可以先进行下采样降低分辨率再进行解码渲染等。在 iOS 设备上支持四种图片渲染格式,每种格式有着不同的 bitsPerComponent 和适用场景:

  • SRGB format :每个像素占用 4 字节,分别表示红、绿、蓝通道以及 alpha 通道
  • Wide format:iOS 硬件设备支持的更生动的色域的渲染格式,每个通道占用 2 字节,每像素占用 8 字节。iOS 7 以上的设备可以拍摄这类照片,他们可以栩栩如生地还原美好。但是因为其较大的内存开销需要谨慎使用
  • Luminance and alpha 8 format:每像素占用 2 字节,分别表示灰度和透明度,适用于 Metal Apps 中的阴影等
  • Alpha 8 format:每像素只占用 1 字节,单色,适用于如阴影、无emoji文字等
    那么我们该如何选择合适的渲染格式呢?

Don’t pick the format, let the format pick you

相比于总是使用默认的 SRGB 格式的 UIGraphicsBeginImageContextWithOptions 方法,Kyle 建议我们使用在 iOS 10 引入的 UIGraphicsImageRenderer 类完成绘制任务,它在 iOS 12 中会根据场景自动选择最合适的渲染格式,更加合理地使用内存。UIGraphicsImageRenderer 可以创建 UIImage 对象或者进行 JPEG/PNG 格式的编码。

此外,关于下采样(downsampling),虽然上述 API 能够合理使用渲染方案,但 UIImage 在修改图片尺寸时的性能逊于 ImageIO。

  • UIImage 会首先把图片解码加载到内存,内部空间坐标转换也会带来巨大损耗
14.png
  • ImageIO 能够在不产生 dirty memory 的情况下读取到图片尺寸和元信息,其内存损耗等于缩减后的图片尺寸产生的内存占用
15.png

5. Optimizing when in background

最后Kyle给出了一点建议就是优化 App 的后台相关行为,即在 App 进入后台时释放内存占用较大的资源,进入前台时重新加载。这里的实现有两种方式:

(1)App life cycle - 对于一些正在显示的view对象,可以监听 UIApplicationDidEnterBackground 和 UIApplicationDidEnterForeground 系统通知

16.png

(2)UIViewController appearance cycle - 利用 VC 的生命周期方法,更适用于UITabBarController、UINavigationController 等有多个子vc的场景,因为你可能会有多个同一层级的 vc,但同一时间内又只有一个页面在展示

17.png

两种方式都可以在用户没有感知的情况下减少后台行为下的内存占用,让系统能够获得更多可用内存。除了苹果工程师为我们提供的建议外,内存占用也还有更多的优化可能。在对进行现有问题的追踪优化基础上,开发应用的过程中,我们更要注意对对象和文件的使用方式,避免引入显而易见的内存问题。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,601评论 1 180
  • 一种特定的颜色,大面积渲染以后,它可以影响人的气的运动,进一步影响人的心理变化,使人产生不同的心理感受和生理反应。...
    心栖息阅读 161评论 0 1
  • 最近听说学车又出新规了,赶紧找小伙伴和相熟的教练咨询相关事宜。经过 一番对比之后,终于锁定最后2家驾校。A驾校离公...
    trista_chow阅读 252评论 0 0
  • 啦啦啦
    粒粒米阅读 246评论 0 2