摘自Unite Beijing 2018 —— iOS底层内存解析
有兴趣可以踩踩我的小站:
理解IOS内存构成(Understanding iOS Memory)
内存的种类(Kind of Memory)
System Memory
Physical Memory:物理内存
就是实际的物理限制,现在的操作系统都不会直接去操作物理内存
Virtual Memory:虚拟内存
每当启动一个进程的时候都会创建一个logical address space,和物理内存或者其他应用程序的虚拟地址都不对称。
系统将地址空间分成想呕吐那个大小的块,称作页(page)。进程和内存管理单元包含了一个分页表(page table)来管理分页,在程序运行的时候就通过这个分页表转换到实际的硬件内存地址。
早版本的iOS页的大小,页的尺寸是4kb,在最新的iOS,A7和A8的系统开放了16kb的64位用户空间对应了4kb的物理页,A9的时候开放了16kb的页并且对应了16kb的物理页。
虚拟内存包含很多区域,包括代码部分(Code Segments),动态库(Dynamic Libraries),GPU驱动内存(GPU Driver Memory),malloc堆(malloc heap)和其他的。
GPU 驱动内存(gpu driver memory)
由虚拟内存组成,用于驱动,本质上就是IOS的显存。
iOS中所谓统一架构(unified architecture),CPU和GPU共享相同的内存(虽然现代硬件的GPU有更大的传输带宽)大多数内存申请都在驱动中完成,并且大多数都是贴图和网格信息。
Malloc堆(malloc heap)
堆内存是虚拟内存中应用能够申请的部分(通过malloc和calloc函数)。
也就是内存申请允许访问的地方。
苹果没有最大程度上地开放堆内存,理论上虚拟地址只被指针大小限制(比如64位那就有2的64次方的bytes),这是进程架构决定的。实际上这个限制被ios的版本限制了,远远小于我们所认为的大小。一个普通的app能申请到的最大内存如下:
常驻内存
常驻内存是游戏实际使用的物理内存数量。
一个进程能够申请一个虚拟内存块,但是系统实际上是给了一个相符的物理内存块然后进行写入。这种情况,这个申请的物理内存块就是这个程序的常驻内存。
分页
分页是移动物理内存页从内存中放到后台储存中。
进程申请内存的时候会将空闲的内存块申请出来并且标志为常驻内存。
当一个进程申请了块虚拟内存,系统会寻找在物理内存中的空闲的内存页并且将它们映射到已申请的虚拟内存页上(因此将这些内存页作为程序的常驻内存)
如果在物理内存中已经没有可使用的部分的话,系统将根据平台尝试释放已经存在的页,以保证有足够的空间申请新的页。通常情况下,一些使用比较少的页会被移动到后备储存中,并且像一般的文件一样进行储存下来这被称作 paging out.
但在iOS上没没有后台储存,所以页不会page out。但是只读也依旧可以被从内存中移除并且在需要的情况下从磁盘中重载,进程的这种行为被称为page in
如果当前请求的应用程序申请的地址并不在当前的物理内存上,会产生一个页错误。当这种事情发生时,虚拟内存系统调用一个特殊的也错误处理器来应对这种情况,定位一个空闲物理内存,从后备储存中加载包含所需数据的页,更新page table,然后归还代码的控制权。
Clean Memory
Clean Memory
是一个应用常驻内存的只读内存页集,iOS能够安全地从磁盘中移除或重载。
内存申请时将以下的这些看做是Clean的:
- 系统framework
- 程序的二进制可执行文件
- 内存映射文件
当一个应用程序链接到framework上,Clean Memory集合会增加二进制framework文件的尺寸。但大多时候,只有一部分二进制文件被加载到物理内存中。
因为Clean Memory是只读的所以,应用程序可以共享framework以及library,就像其他只读或者写时拷贝的页一样。
Dirty Memory
DirtyMemory是无法被系统主动移除的常驻内存部分。
(因为是脏的数据……
<…>
交换压缩内存(Swapped Compressed Memory)
swapped(Compressed Memory)是Dirty Memory的一部分,是被系统认为用的比较少并且放在一个被压缩的区域。
用于计算移动和压缩这些内存块的算法并没有被开放出来,但是测试显示iOS经常频繁调用这个算法,以此来降低Dirty Memory的数量。
Unity内存
Unity是一个带了.Net脚本虚拟机的C++引擎。Unity为C++object以及虚拟机所需的object申请内存。另一方面,第三方的插件也能从虚拟机内存池中申请内存。
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关闭的可能。
各种Profile工具
1. Unity Memory Profiler
2. MemoryProfiler (on BitBucket)
3. MemoryProfiler Extension (on Github)
4. Xcode memory gauge in Debug Navigator view
5. VM Tracker Instrument
6. Allocations Instrument
(后面都是工具的使用部分了,大家有兴趣可以自己进行尝试