Understanding Memory on iOS 翻译

理解iOS内存管理

 

  正确管理 iOS 设备上的内存非常重要,因为不这样做会导致操作系统终止游戏,
从而导致用户认为崩溃。用户通常不喜欢游戏崩溃并倾向于留下一星评价。
为了评估游戏的内存消耗,建议经常在目标设备上分析游戏,寻找崩溃和内存泄漏。
为此,Unity 和 Apple 提供了分析工具,如果使用得当,可以告诉 Unity 开发人员她需要了解的有关游戏的所有信息。
提供的工具如下:

 

当开发人员问“我的游戏使用了多少内存?”这个问题时,通常这些工具中的一个有一个很好的答案。 然而,问题是这个问题是模棱两可的——“记忆”这个词可能意味着几种不同的记忆。 因此,了解问题所指的内存类型至关重要。 不同类型内存的存在造成了一层混乱,并认为 iOS 上的内存主题太复杂而无法打扰。

本文档描述了 iOS 中内存的性质,并详细介绍了上述工具提供的数据。 提供的信息适用于其他平台,但此处不讨论实现上的差异。

 - What kind of memory? 内存种类

系统内存(物理内存RAM和虚拟内存VM)

• Physical/Resident Memory: 物理内存(真实内存)

物理内存是 iPhone 或 iPad 内芯片上的物理设备内存。它有物理限制(例如 512Mb 或 1Gb)并且无法容纳更多数据。每个正在运行的应用程序都占用一定数量的物理内存,但在现代操作系统(如 iOS)中,应用程序从不直接使用片上内存。相反,它们处理所谓的虚拟内存,操作系统将其无缝映射到物理内存。
• Virtual Memory  :虚拟内存 

虚拟内存是游戏中的地址空间,它可以分配内存并保存指向该内存的指针。

当一个进程启动时,操作系统为该进程创建一个逻辑地址空间(或“虚拟”地址空间)。之所以称为“虚拟”,是因为暴露给进程的地址空间不一定与机器的物理地址空间对齐,甚至与其他应用程序的虚拟地址空间也不一定对齐。

操作系统将此地址空间划分为大小均匀的内存块,称为页面。处理器及其内存管理单元 (MMU) 维护一个页表,以将进程逻辑地址空间中的页面映射到计算机 RAM 中的硬件地址。当应用程序的代码访问内存中的地址时,MMU 使用页表将指定的逻辑地址转换为实际的硬件内存地址。这种转换是自动发生的,并且对正在运行的应用程序是透明的。

在早期版本的 iOS 中,页面的大小为 4 KB。在更高版本的 iOS 中,基于 A7 和 A8 的系统向由 4 KB 物理页面支持的 64 位用户空间公开 16 KB 页面,而 A9 系统向由 16 KB 物理页面支持的 16 KB 页面公开。

虚拟内存由多个区域组成,包括代码段、动态库、GPU 驱动程序内存、malloc 堆等。

GPU 驱动程序内存

• GPU Driver Memory: GPU 驱动程序内存

GPU 驱动程序内存由驱动程序使用的虚拟内存中的分配组成,本质上是 iOS 上的视频内存。

iOS 具有所谓的统一架构,其中 CPU 和 GPU 共享相同的内存(尽管在现代硬件上,GPU 具有更高的内存带宽)。分配由驱动程序完成,主要由纹理和网格数据组成。

• Malloc Heap Malloc堆

Malloc 堆是一个虚拟内存区域,应用程序可以在其中使用 malloc 和 calloc 函数分配内存。

换句话说,这是一块可用于应用程序内存分配的虚拟地址空间。

Apple 没有公布 Malloc Heap 的最大大小。 理论上,虚拟内存地址空间仅受指针大小的限制,指针大小由处理器架构定义,

即在 32 位处理器上大约为 4 GB 的逻辑内存空间,在 64 位处理器上为 18 艾字节。 但实际上,实际限制似乎取决于设备和 iOS 版本,并且远低于人们的想象。 一个连续分配虚拟内存的简单应用程序提供以下值:

Device

iOS

SoC

Heap

iPad Mini 2

11.2.1

A7

2.08 GB

iPhone 6

10.3.3

A8

2.08 GB

iPhone SE

11.1.2

A9

3.43 GB

iPad Pro 9.7

11.2.5

A9X

3.47 GB

iPhone 7

11.2.6

A10

3.40 GB

iPad Pro 10.5

11.2.5

A10X

7.40 GB

iPhone 8

11.2.1

A11

3.40 GB

iPhone X

11.3

A11

3.42 GB

理论上,使用过多的内存映射文件可能会耗尽虚拟内存地址空间。

Resident Memory 常驻内存

常驻内存是游戏实际使用的物理内存量。

一个进程可以从虚拟内存中分配一块内存,但操作系统要实际保留相应的物理内存块,进程必须写入该块。 在这种情况下,分配的内存块将成为应用程序驻留内存的一部分。

Paging 分页

分页是移动物理内存页从内存中放到后台储存中。

进程申请内存的时候会将空闲的内存块申请出来并且标志为常驻内存。

当一个进程申请了块虚拟内存,系统会寻找在物理内存中的空闲的内存页并且将它们映射到已申请的虚拟内存页上(因此将这些内存页作为程序的常驻内存)

如果在物理内存中已经没有可使用的部分的话,系统将根据平台尝试释放已经存在的页,以保证有足够的空间申请新的页。通常情况下,一些使用比较少的页会被移动到后备储存中,并且像一般的文件一样进行储存下来这被称作 paging out.

但在iOS上没没有后台储存,所以页不会page out。但是只读也依旧可以被从内存中移除并且在需要的情况下从磁盘中重载,进程的这种行为被称为page in

 

如果当前请求的应用程序申请的地址并不在当前的物理内存上,会产生一个页错误。当这种事情发生时,虚拟内存系统调用一个特殊的也错误处理器来应对这种情况,定位一个空闲物理内存,从后备储存中加载包含所需数据的页,更新page table,然后归还代码的控制权。

Clean Memory

Clean Memory 是一组来自应用程序驻留内存的只读内存页面,iOS 可以在需要时安全地从磁盘中删除和重新加载这些页面。

为以下数据分配的内存被认为是干净的:

1.System framework 系统框架,

2.Application’s binary executable 应用程序的二进制可执行文件,

3.Memory mapped files 内存映射文件。

 当应用程序链接到框架时,Clean Memory 集将增加框架二进制文件的大小。 但大多数时候,只有一部分二进制文件加载到物理内存中。

 由于 Clean Memory 是只读的,因此应用程序可以共享 Clean Memory 的一部分,例如通用框架和库,以及其他只读或写时复制页面。

Dirty Memory 脏内存

脏内存是常驻内存的一部分,不能被操作系统删除。

Swapped Compressed Memory 交换压缩内存

当物理内存不够用时,iOS 会将部分物理内存压缩,在需要读写时再解压,以达到节约内存的目的。而压缩之后的内存,就是所谓的 compressed memory。苹果最开始只是在 OS X 上使用这项技术,后来也在 iOS 系统上使用。

实际上,随着虚拟内存技术的发展,很多桌面操作系统早已经应用了内存压缩技术,比如 Windows 中的 memory combining 技术。这本质上来说和内存交换机制类似,都是是一种用 CPU 时间换内存空间的方式,只不过内存压缩技术消耗的时间更少,但占用 CPU 更高。不过在文章最开始,我们就已经谈到由于 CPU 算力过剩,在大多数场景下,物理内存的空间相比起 CPU 算力来说显然更为重要,所以内存压缩技术非常有用。

根据 OS X Mavericks Core Technology Overview 官方文档来看,使用 compressed memory 能在内存紧张时,将目标内存压缩至原有的一半以下,同时压缩和解压消耗的时间都非常小。对于 OS X,compressed memory 也能和内存交换技术共用,提高内存交换的效率,毕竟压缩后再进行交换效率明显更高,只是 iOS 没有内存交换,也就不存在这方面的好处了。

本质上来讲,compressed memory 也属于 dirty memory。

swapped(Compressed Memory)是Dirty Memory的一部分,是被系统认为用的比较少并且放在一个被压缩的区域。

用于计算移动和压缩这些内存块的算法并没有被开放出来,但是测试显示iOS经常频繁调用这个算法,以此来降低Dirty Memory的数量。

Unity Memory Unity内存

Unity 是一个带有 .NET 脚本虚拟机的 C++ 游戏引擎。 Unity 为本机 (C++) 对象分配内存,并从虚拟内存中分配虚拟机所需的内存。此外,第三方插件可以从虚拟内存池中进行分配。

Native Memory

游戏虚拟内存中的Native内存是由native(C++)部分进行申请的——在这里Unity申请了它所需要的所有页,包括了Mono堆的。

 

在内部,Unity有一系列专门的内存申请器来管理虚拟内存的申请,包括短期用途和长期用途的。所有游戏当中的资源都在Native Memory中进行储存,并且在.Net虚拟机中开放出轻量级接口。换句话说,当一张Texture2D在C#中被创建出来,最大的那部分,实际上是贴图信息,在Native内存中被申请了,而并非在Mono堆中(虽然大多时间他会被上传到GPU然后被丢弃)。

Mono Heap

Mono堆是Native内存的申请的一部分,用于.Net虚拟机。它包括了所有托管C#申请的内存,并且由垃圾回收器管理。

Mono 堆由大小相似的储存着各种对象的内存块中进行申请。每一个块能储存一定数量的object如果它在几轮的GC中保持为空(在iOS中为8次GC),这个Block会从内存中被释放(它的物理内存被归还给系统)。但是被GC所使用的虚拟内存地址空间永远不会被释放,并且也不能被任何游戏内存申请器使用。

现在存在的问题是,很多情况下申请的内存块是分散的,也许很大的一块尺寸仅仅使用了很小的一部分。这些块被认为是正在被使用的,所以他们引用的物理内存就无法被正常释放。不幸的是,这种情况经常在实际使用中遇到,也很容易人为地就产生Mono堆常驻内存快速增长的情况。

iOS内存管理

iOS是多任务操作系统,它允许多个应用程序在同一环境中共存。每个应用程序都有它自己的虚拟地址空间映射到物理内存的一些部分。

当物理内存不足的时候(或者是过多的应用程序被加载,或者是前台程序消耗了太多的物理内存),iOS开始尝试降低内存压力。

1. 首先,iOS尝试卸载部分Clean Memory页

2. 如果应用程序使用了过多的Dirty Memory,iOS会发送一个内存过低的预警给应用程序,让它自己释放一些内存。

3. 在若干次警告之后,如果应用程序依旧占用了过多的内存,iOS将会终止这个应用程序。

不幸的是,杀死进程的决定并不是透明的。但是它看上去就是由内存压力、内核内存管理器的内部状态以及操作系统已经尝试了多少次减少内存压力的操作决定的。只有当所有的储存空间使用完毕之后,它会决定杀死当前进程。这就是为什么有时候应用程序在申请了多于30%的内存的时候很快就退出了。

尝试调查闪退的最重要的部分就是Dirty Memory的控制,因为iOS无法主动移除脏页来提供更多空间给新的申请需求。这就意味着,修复内存释放问题,开发者必须做到以下几点:

1. 找出在游戏中有多少Dirty Memory,并且是否还随着时间增长。

2. 算出什么对象在贡献游戏的dirty memory 并且无法被压缩

不同的iOS设备上的Dirty Memory尺寸有相应合理的限制(从我们能广泛看到的结果):

  • 512MB设备中180MB
  • 1GB设备中360MB
  • 2GB设备中1.2GB

记下这些推荐限制值,被iOS关闭依旧是有可能的但是可以大大减少被iOS关闭的可能。

 

posted @ 2021-12-09 14:59  星傲蝶恋  阅读(255)  评论(0)    收藏  举报