热门搜索 :
考研考公
您的当前位置:首页正文

[ WWDC2018 ] - 深入解析iOS内存 iOS Mem

来源:东饰资讯网

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

  • 在 不再更新之后,这个 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,而是利用引导分区所有可用的磁盘空间。

  • 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

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 对象的内存泄露,而且也可以扩展以检测其它类型的对象。

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 且其值表示压缩前的占用大小。

此外,结合 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

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

此外,关于下采样(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

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

参考

Top