内存
物理内存
CPU访问内存是一个慢速过程。
CPU在需要访问内存时,先是访问自己的缓存(L1Cache、L2Cache……),当全部Miss之后,CPU会去主内存拿一段完整的指令到CPU的缓存中。因此,我们需要尽可能保证CPU的指令是连续的,防止CPU过多地与主内存之间的内存交换产生IO。
Unity为了处理上述问题,减少Cache Miss,推出了ECS和DOTS,把分散的内存数据变成整块、连续的数据。(但DOTS目前还不稳定)
虚拟内存
电脑在物理内存不够的时候,操作系统会把一些不用的数据(DeadMemory)交换到硬盘上,称之为内存交换。
但是手机是不做内存交换的,一是因为移动设备的硬盘IO速度比PC慢很多,二是因为移动设备的硬盘可擦写次数更少;因此手机如果做内存交换一是慢,而是减少设备寿命看,所以Android机上没有做内存交换。IOS可以把不活跃的内存进行压缩,使得实际可用的内存更多,而安卓没有这个能力。
移动设备和pc
移动设备(手机)与PC的区别在于,手机没有独立显卡、独立显存。手机上无论是CPU还是GPU都是共用一个缓存,而且手机的内存更小、缓存级数更少、大小更小。台式机的三级缓存大约8~16M,而手机只有2M。
综上,手机上的内存,不论从哪个角度看,都是比PC要小很多的。所以,手机上更容易出现内存不够的问题。
andriod内存管理
Android是基于Linux开发的,所以Android的内存管理和Linux很相似。
Android的内存管理基本单位是Page(页),一般是4k 一个Page。内存的回收和分配都是以 Page为单位进行操作,也就是4k。Android内存分用户态和内核态两个部分,内核态的内存是用户严格不能访问的。
内存杀手
内存杀手Low Memory Killer (LMK)
当手机的内存使用量过多时,就会出现LMK,对当前手机的各种App、服务进行关停。安卓的各种应用、服务分为以下一些类别:
- Native:系统内核
- System:系统服务
- Persistent: 用户服务,比如电话、蓝牙、Wifi等。
- Foreground:前台应用,当前正在使用的Activity
- Perceptible:辅助应用,音乐、搜索、键盘等;
- Service:驻后台线程的服务,云同步、垃圾回收等;
- Home键;
- Previous:上一个使用的应用;
- Cached:后台,之前使用过的各种应用。
这个也是Android系统的应用优先度排序,编号越小优先级越高。当LMK开始工作的时候,会从优先度最低的应用开始Kill。即最先中断各种Cached,最后才会到Native。
例如当Cached被杀掉之后,现象就是当你切换到后台的那些应用时,你会发现那些应用重启了。
当Home被杀死的时候,你发现当你回到桌面时,桌面会重启,你的桌面图标会重建,或者壁纸没了。
到Perceptible的时候,可能你的音乐、键盘不见了。
再往上进行,到Foreground时,当前前台应用就会被杀死,这个时候就会出现应用闪退。
在往上手机就开始重启了。
内存指标
RSS:Resident Set Size
你当前的APP所应用到的所有内存。除了你自己的APP所使用的内存之外,你调用的各种服务、共用库所产生的内存都会统计到RSS之中。
PSS:Proportional Set Size
与RSS不同的是,PSS会把公共库所使用的内存平摊到所有调用这个库的APP上。(可能你自己的应用没有申请很多内存,但是你的调用的某个公共库已经有了很大的内存分配,平摊下来就会导致你自己的APP的PSS虚高。)
USS:Unique Set Size
只有此APP所使用的内存,剔除掉公共库的内存分配。
我们在实际工作中更多要做的是对USS的优化,有时也会注意一下PSS。
ios内存管理
Virtual Memory (VM)
这是app能够见到的地址空间。当一个进程启动后,操作系统会创建一块逻辑地址空间(或者叫虚拟地址空间)供游戏使用。它之所以被称之为“虚拟”是因为这块暴露给进程的地址空间并没有和设备的物理地址空间对齐。
MMU(Memory Management Unit)拥有一张映射表用来将虚拟地址空间映射到物理地址空间。当代码想要访问一段内存地址时(实际是访问一段虚拟内存地址),MMU使用映射表将这段地址翻译成真实物理设备的内存地址。
操作系统会将地址空间切分为许许多多的块,这些块被称为“页(page)”。这些页细分为4KB(早期IOS版本)或16KB(A7之后)。一组连续排布的页(例如一次性申请了比较大的内存,超过了4KB或16KB)称之为区域(VM Region),这些页都有相同的属性,例如可读写等。
Resident Memory
这部分内存是app实际使用的物理内存(Physical Memory),也就是虚拟内存(Virtual Memory)中被映射到真实物理内存的部分。已经被映射的虚拟内存也可称之为“committed”,没有映射的称为“reserved”。
Clean Memory and Dirty Memory
page有可能是dirty的也有可能是clean的。简单的说,dirty的页就是app对这个page的内容进行了修改即分配了内存同时也修改了内存的内容,常见的就是malloc在heap上分配的内存。这部分内存是不能被回收的,因为这些数据显然需要被保存在内存中以保证程序正常的运行。
而clean的页则是没有对其内容进行修改,可以被系统收回和重新创建的。例如内存映射文件(Memory-mapped file)、app的二进制文件(binaries),如果操作系统需要更多的内存,那么就可以将其丢弃。因为系统总是可以从磁盘中重新加载它,创建内存空间和磁盘上文件的映射关系。clean的内存是可以被释放和重新创建的。
多个app之间可以共享clean的页。
Graphics Memory (VRAM)
手机使用的是统一内存架构,GPU和CPU共享物理内存。VRAM这部分内存由显卡驱动申请,供图形渲染使用,他们大部分是由贴图和网格数据组成,并且大部分是dirty的。
Malloc Heap
app使用malloc和calloc方法申请的虚拟内存,Unity使用的就是这部分的内存。
Swapped (compressed) Memory
当内存吃紧时,会回收clean page。而dirty page是不能被回收的,那么如果dirty memory过多会如何呢?在iOS7之前,如果进程的dirty memory过高则系统会直接终止进程。iOS7之后,引入了Compressed Memory的机制。由于iOS没有传统意义上的disk swap 机制(mac OS有),因此在苹果的Profiler工具中看到的Swapped Size指的其实就是Compressed Memory。
iOS7之后,操作系统可以通过内存压缩器来对dirty内存进行压缩。首先,针对那些有一段时间没有被访问的dirty pages(多个page),内存压缩器会对其进行压缩。但是,在这块内存再次被访问时,内存压缩器会对它解压以正确的访问。举个例子,某个Dictionary使用了3个page的内存,如果一段时间没有被访问同时内存吃紧,则系统会尝试对它进行压缩从3个page压缩为1个page从而释放出2个page的内存。但是如果之后需要对它进行访问,则它占用的page又会变为3个。
Native (Unity) Memory
Unity是一个使用C++编写的.Net虚拟机。Native Memory就是Unity申请的Malloc Heap。
Native Plugins
第三方插件自己管自己申请内存,申请的内存也是在Malloc Heap。同时他们的代码文件是clean的。
Mono Heap
.Net虚拟机使用到的内存,为Native Memory的一部分。包含了所有由C#申请的内存,由GC所持有。Mono Heap不是一块在Native Memory中大的连续的内存区域。它以一种块(Block)的形式被申请分配。如果一个块(Block)在8个GC周期后还没有被使用,它所占据的物理内存就会被系统回收。
IOS的内存管理
IOS是一个多任务的操作系统。每个应用程序拥有一块自己的虚拟内存空间地址,它们被映射到物理内存的一些位置。
当可使用的物理内存达到低位时(比如有很多应用在后台,或者前台应用使用了过多物理内存),操作系统就会试图去减小内存压力,它会做以下几件事
- 首先,系统会移除一些Clean Memroy pages
- 如果应用使用了太多的Dirty Memory,系统就会对应用发送警告以期望应用自己去释放一些内存
- 如果在数次警告之后,应用程序还是继续使用大量的Dirty Memory,系统就会杀掉这个应用
这里说的Dirty Memory实际上还囊获了Compressed Memory,也就是说判断是否发出警告并杀掉应用的依据来源于Dirty Memory与Comporessed Memory之和,这部分亦称为Memory Footprint,是苹果推荐的内存度量及优化的指标。
OOM
OOM的全称是out of memory,字面意思也就是指内存超出了限制。在iOS中的OOM是由操作系统的Jetsam机制出发的crash的一种。由OOM导致的crash无法通过监控singal获取异常信息,所以对于OOM的监控只能间接实现。
Jetsam机制
Jetsam时iOS系统的单独的进程,对于内存管理则是BSD层创建的优先级最高的常驻线程VM_memorystatus,可以管理系统的内存占用,当发现内存紧张时候会根据优先级杀掉其他应用程序进程。可以简单理解为内存管理的的奔溃处理机制就是Jetsam机制。
macOS或者windows系统,当应用程序紧张的时候,可以通过SWAP内存交换机制实现把物理内存中的一部分内容交换到磁盘上去,利用磁盘空间扩展内存空间。对于移动设备来说一般没有内存交换机制,原因在于移动设备的存储介质也就是闪存,而闪存的性能和使用寿命是无法和电脑硬盘相比的,所以当内存紧张时,就会系统的Jetsam就会杀死应用程序。
Compressed memory
iOS 上没有Disk swap机制,取而代之使用 Compressed memory。从 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 ,这也就是我们需要并且能够尝试去减少的内存占用。
NSCache 分配的内存实际上是 Purgeable Memory,可以由系统自动释放。NSCache 与 NSPureableData 的结合使用既能让系统根据情况回收内存,也可以在内存清理的同时移除相关对象。
Jetsam机制杀死进程的顺序
Jetsam机制杀死进程的顺序一般基于应用程序的优先级确定的,优先级低的进程先于优先级高的进程被杀死。在iOS系统中应用程序的优先级时不可能高于操作系统和内核的。前台的应用程序的优先级高于后台应用程序,对于多个后台程序的优先级也是不完全一样的,系统会对每一个进程的优先级进行动态调整。例如如果耗费CPU太多就降低优先级,如果一个线程过度挨饿CPU则会提升其优先级。
需要注意的是,JETSAM不一定只杀一个进程,他可能会大杀特杀,杀掉N多进程。
设备信息
以下是摘抄的部分OOM时的Memory Footprint Limit Line(发出警告,并进一步导致OOM的安全线)
device | crash amount/total amount/percentage of total |
---|---|
iPad1 | 127MB/256MB/49% |
iPad2 | 275MB/512MB/53% |
iPad3 | 645MB/1024MB/62% |
iPad4 | 585MB/1024MB/57% (iOS 8.1) |
iPad Mini 1st Generation | 297MB/512MB/58% |
iPad Mini retina | 696MB/1024MB/68% (iOS 7.1) |
iPad Air | 697MB/1024MB/68% |
iPad Air 2 | 1383MB/2048MB/68% (iOS 10.2.1) |
iPad Pro 9.7 | 1395MB/1971MB/71% (iOS 10.0.2 (14A456)) |
iPad Pro 10.5 | 3057/4000/76% (iOS 11 beta4) |
iPad Pro 12.9” (2015) | 3058/3999/76% (iOS 11.2.1) |
iPad Pro 12.9” (2017) | 3057/3974/77% (iOS 11 beta4) |
iPod touch 4th gen | 130MB/256MB/51% (iOS 6.1.1) |
iPod touch 5th gen | 286MB/512MB/56% (iOS 7.0) |
iPhone4 | 325MB/512MB/63% |
iPhone4s | 286MB/512MB/56% |
iPhone5 | 645MB/1024MB/62% |
iPhone5s | 646MB/1024MB/63% |
iPhone6 | 645MB/1024MB/62% (iOS 8.x) |
iPhone6+ | 645MB/1024MB/62% (iOS 8.x) |
iPhone6s | 1396MB/2048MB/68% (iOS 9.2) |
iPhone6s+ | 1392MB/2048MB/68% (iOS 10.2.1) |
iPhoneSE | 1395MB/2048MB/69% (iOS 9.3) |
iPhone7 | 1395/2048MB/68% (iOS 10.2) |
iPhone7+ | 2040MB/3072MB/66% (iOS 10.2.1) |
iPhone8 | 1364/1990MB/70% (iOS 12.1) |
iPhone X | 1392/2785/50% (iOS 11.2.1) |
iPhone XS | 2040/3754/54% (iOS 12.1) |
iPhone XS Max | 2039/3735/55% (iOS 12.1) |
iPhone XR | 1792/2813/63% (iOS 12.1) |
iPhone 11 | 2068/3844/54% (iOS 13.1.3) |
iPhone 11 Pro Max | 2067/3740/55% (iOS 13.2.3) |
iPhone 12 Pro | 3054/5703/54% (iOS 16.1) |
Unity内存
Unity内存的分类
Unity内存分为 Native Memory和 Managed Memory (托管内存)。值得注意的是,在Editor下和在Runtime下Unity的内存分配是完全不同的。不但分配内存的大小会有不同,统计看到的内存大小不同,甚至是内存分配时机和方式也不同。
比如一个AssetBundle,在编辑器下是你一打开Unity就开始加载进内存,而在Runtime下则是你使用时才会加载,如果不读取,是不会进内存的。(Unity2019之后做了一些Asset导入优化,不使用的资源就不会导入)。 因为 Editor 不注重 Runtime 的表现,更注重编辑器中编辑时的流畅。
但如果游戏庞大到几十个 G,如果第一次打开项目,会消耗很多时间,有的大的会几天,甚至到一周。
Unity的内存还可以分为引擎管理的内存和用户管理器的内存两类。引擎管理的内存一般开发者是访问不到的,而用户管理的内存才是使用者需要关系和优先考虑的。
还有一个Unity监测不到的内存:用户分配的 Native 内存内存是Unity的Profile工具监测不到。例如:
- 自己写的 Native 插件(C++ 插件), Unity 无法分析已经编译过的 C++ 是如何去分配和使用内存的。
- Lua 完全由自己管理内存,Unity 无法统计到内部的使用情况。
Native 内存
Unity 重载了所有分配内存的操作符(C++ alloc、new),使用这些重载的时候,会需要一个额外的 memory label (Profiler-shaderlab-object-memory-detail-snapshot,里面的名字就是 label:指当前内存要分配到哪一个类型池里面)
- 使用重载过的分配符去分配内存时,Allocator 会根据你的 memory label 分配到不同 Allocator 池里面,每个 Allocator 池 单独做自己的跟踪。因此当我们去 Runtime get memory label 下面的池时就可以问 Allocator,里面有多少东西 多少兆。
- Allocator 在 NewAsRoot (Memory “island”(没听清)) 中生成。在这个 Memory Root 下面会有很多子内存:shader:当我们加载一个 Shader 进内存的时候,会生成一个 Shader 的 root。Shader 底下有很多数据:sub shader、Pass 等会作为 memory “island” (root) 的成员去依次分配。因此当我们最后统计 Runtime 的时候,我们会统计 Root,而不会统计成员,因为太多了没法统计。
- 因为是 C++ 的,因此当我们 delete、free 一个内存的时候会立刻返回内存给系统,与托管内存堆不一样。
Scene
Unity 是一个 C++ 引擎,所有实体最终都会反映在 C++ 上,而不是托管堆里面。因此当我们实例化一个 GameObject 的时候,在 Unity 底层会构建一个或多个 Object 来存储这个 GameObject 的信息,例如很多 Components。因此当 Scene 有过多 GameObject 的时候,Native 内存就会显著上升。
当我们看 Profiler,发现 Native 内存大量上升的时候,应先去检查 Scene。
Audio
DSP Buffer
DSP Buffer 相当于音频的缓冲。
当一个声音要播放的时候,它需要向 CPU 去发送指令——我要播放声音。但如果声音的数据量非常小,就会造成频繁地向 CPU 发送指令,会造成 I\O。
当 Unity 用到 FMOD 声音引擎时(Unity 底层也用到 FMOD),会有一个 Buffer,当 Buffer 填充满了,才会向 CPU 发送“我要播放声音”的指令。
DSP buffer 会导致两种问题:
- 如果(设置的) buffer 过大,会导致声音的延迟。要填充满 buffer 是要很多声音数据的,但声音数据又没这么大,因此会导致一定的声音延迟。
- 如果 DSP buffer 太小,会导致 CPU 负担上升,满了就发,消耗增加。
Foce to Mono
Foce to Mono 强制单声道
当两个声道完全相同时可以Force To Mono,可以节省一半的内存。
在导入声音的时候有一个设置,很多音效师为了声音质量,会把声音设为双声道。但 95% 的声音,左右声道放的是完全一样的数据。这导致了 1M 的声音会变成 2M,体现在包体里和内存里。因此一般对于声音不是很敏感的游戏,会建议改成 Force to mono,强制单声道。
Format
例如IOS对MP3有硬解支持的,所以MP3的解析会快很多(Android 没有)。
Compressiont Format
声音文件在内存的存在形态(解压的、压缩的等)。
Code Size
代码也是需要加载进内存的,使用时要注意减少模板泛型的滥用。因为模板泛型在编译成C++时,会把同样的代码排列组合都编译一边,导致Code Size 大幅上升。
AssetBundle
TypeTree
Unity 的每一种类型都有很多数据结构的改变,为了对此做兼容,Unity 会在生成数据类型序列化的时候,顺便会生成 TypeTree:当前我这一个版本里用到了哪些变量,对应的数据类型是什么。在反序列化的时候,会根据 TypeTree 来进行反序列化。
- 如果上一个版本的类型在这个版本中没有,TypeTree 就没有它,因此不会碰到它。
- 如果要用一个新的类型,但在这个版本中不存在,会用一个默认值来序列化,从而保证了不会在不同的版本序列化中出错,这个就是 TypeTree 的作用。
Build AssetBundle 中有开关可以关掉 TypeTree。当你确认当前 AssetBundle 的使用和 Build Unity 的版本一模一样,这时候可以把 TypeTree 关掉。
- 例如如果用同样的 Unity 打出来的 AssetBundle 和 APP,TypeTree 则完全可以关掉。
TypeTree 好处:
- 内存减少。TypeTree 本身是数据,也要占内存。
- 包大小会减少,因为 TypeTree 会序列化到 AssetBundle 包中,以便读取。
- Build 和运行时会变快。源代码中可以看到,因为每一次 Serialize 东西的时候,如果发现需要 Serialize TypeTree,则会 Serialize 两次:
- 第一次先把 TypeTree Serialize 出来
- 第二次把实际的东西 Serialize 出来
- 反序列化也会做同样的事情,1. TypeTree 反序列化,2. 实际的东西反序列化。
- 当你确定你当前的AssetBundle和你的Unity是同一个版本的时候,就可以关掉TypeTree。关掉TypeTree之后可以减少内存大小、包大小、加快运行速度。
压缩方式
使用Lz4,而不是Lzma
- Lz4 (https://docs.unity3d.com/Documentation/ScriptReference/BuildCompression.LZ4.html)
- LZ4HC “Chunk Based” Compression. 非常快
- 和 Lzma 相比,平均压缩比率差 30%。也就是说会导致包体大一点,但是(作者说)速度能快 10 倍以上。
- Lzma (https://docs.unity3d.com/2019.3/Documentation/ScriptReference/BuildCompression.LZMA.html)
- Lzma 基本上就不要用了,因为解压和读取速度上都会比较慢。
- 还会占大量内存,因为是 Steam based 而不是 Chunk Based 的,因此需要一次全解压;Chunk Based 可以一块一块解压,如果发现一个文件在第 5-10 块,那么 LZ4 会依次将 第 5 6 7 8 9 10 块分别解压出来,每次(chunk 的)解压会重用之前的内存,来减少内存的峰值。
- 中国版 Unity 中有基于 LZ4 的Addressables( AssetBundle) 加密,只支持 LZ4。https://mp.weixin.qq.com/s/s9lQyunpRPJZnnaLSb9qOQ
Size & Count
AssetBundle 包打多大是很玄学的问题,但每一个 Asset 打一个 Bundle 这样不太好。
有一种减图片大小的方式,把 png 的头都剔除出来。因为头的色板是通用的,而数据不通用。AssetBundle 也一样,一部分是它的头,一部分是实际打包的部分。因此如果每个 Asset 都打 Bundle 会导致 AssetBundle 的头比数据还要大。
官方的建议是每个 AssetBundle 包大概 1M~2M 左右大小,考虑的是网络带宽。但现在 5G 的时候,可以考虑适当把包体加大。还是要看实际用户的情况。
Resources文件夹
不要使用,除非在 debug 的时候
Resource 和 AssetBundle 一样,也有头来索引。Resource 在打进包的时候会做一个红黑树,来帮助 Resource 来检索资源在什么位置,
如果 Resource 非常大,那么红黑树也会非常大。
红黑树是不可卸载的。在刚开始游戏的时候就会加载进内存中,会持续对游戏造成内存压力。
会极大拖慢游戏的启动时间。因为红黑树没加载完,游戏不能启动。
Texture
Upload Buffer:和声音的DSP Buffer很像,设置填充满多大之后再推向CPU/GPU。
Read/Write : 不使用就关闭它。
Texture 没必要就不要开 read and write。正常 Texture 读进内存,解析完了,放到 upload buffer 里后,内存里的就会 delete 掉。
但如果检测到你开了 r/w 就不会 delete 了,就会在显存和内存中各一份。
Mip Map : 像UI这些不需要的就关闭它,可以省大量内存。。
Mesh
Read/Write :同上Texture
Compression:虽然写的是压缩,但实际效果并不一定有用,有些版本 Compression 开了不如不开,内存占用可能更严重,具体需要自己试。
Unity Managed Memory (托管内存)
VM内存池
Mono虚拟机的内存池,实际上VM是会返回给操作系统。
返还条件是什么?
GC 不会把内存直接返还给系统
内存也是以 Block 来管理的。当一个 Block 连续六次 GC 没有被访问到,这块内存才会被返还到系统。(mono runtime 基本看不到,IL2cpp runtime 可能会看到多一点)
不会频繁地分配内存,而是一次分配一大块。
GC机制
Unity的GC机制是Boehm内存回收,是不分代的,非压缩式的。(之所以是使用Boehm是因为Unity和Mono的一些历史原因,以及目前Unity主要精力放在IL2CPP上面)
GC机制考量
- Throughput((回收能力),一次回收,会回收多少内存
- Pause times(暂停时长),进行回收的时候,对主线程的影响有多大
- Fragmentation(碎片化),回收内存后,会对整体回收内存池的贡献有多少
- Mutator overhead(额外消耗),回收本身有 overhead,要做很多统计、标记的工作
- Scalability(可扩展性),扩展到多核、多线程会不会有 bug
- Protability(可移植性),不同平台是否可以使用
BOEHM
- Non-generational(不分代的)
- 分代是指:大块内存、小内存、超小内存是分在不同内存区域来进行管理的。还有长久内存,当有一个内存很久没动的时候会移到长久内存区域中,从而省出内存给更频繁分配的内存。
- Non-compacting(非压缩式)
- 当有内存被回收的时候,压缩内存会把图中空的地方重新排布。
- 但 Unity 的 BOEHM 不会!它是非压缩式的。空着就空着,下次要用了再填进去。
- 历史原因:Unity 和 Mono 合作上,Mono 并不是一直开源免费的,因此 Unity 选择不升级 Mono,与实际 Mono 版本有差距。
- 下一代 GC
- Incremental GC(渐进式 GC)
- 现在如果我们要进行一次 GC,主线程被迫要停下来,遍历所有 GC Memory “island”(没听清),来决定哪些 GC 可以回收。
- Incremental GC 把暂停主线程的事分帧做了。一点一点分析,主线程不会有峰值。总体 GC 时间不变,但会改善 GC 对主线程的卡顿影响。
- SGen 或者升级 Boehm?
- SGen 是分代的,能避免内存碎片化问题,调动策略,速度较快
- IL2CPP
- 现在 IL2CPP 的 GC 机制是 Unity 自己重新写的,是升级版的 Boehm
- Incremental GC(渐进式 GC)
Memory fragmentation 内存碎片化
为了防止内存碎片化(Memory Fragmentation),在做加载的时候,应先加载大内存的资源,再加载小内存的资源(因为Bohem没有内存压缩),这样可以保证最大限度地利用内存。
- 为什么内存下降了,但总体内存池还是上升了?
- 因为内存太大了,内存池没地方放它,虽然有很多内存可用。(内存已被严重碎片化)
- 当开发者大量加载小内存,使用释放多次,例如配置表、巨大数组,GC 会涨一大截。
- 建议先操作大内存,再操作小内存,以保证内存以最大效率被重复利用。
Zombie Memory(僵尸内存)
- 内存泄露说法是不对的,内存只是没有任何人能够管理到,但实际上内存没有被泄露,一直在内存池中,被 zombie 掉了,这种叫 Zombie 内存。
- 无用内容
- Coding 时候或者团队配合的时候有问题,加载了一个东西进来,结果从头到尾只用了一次。
- 有些开发者写了队列调度策略,但是策略写的不好,导致一些他觉得会被释放的东西,没有被释放掉。
- 找是否有活跃度实际上并不高的内存。
- 没有释放
- 通过代码管理和性能工具分析,查看各个资源的引用
最佳实践
- 用Destory而不是NULL 。
- 多使用Struct。
- 使用内存池:VM 本身有内存池,但建议开发者对高频使用的小部件,自己建一个内存池。例如UI、粒子系统、子弹等。
- 闭包和匿名函数:减少使用。所有的闭包和匿名函数最后都会变成一个Class。
- 协程:只要不被释放,里面所有引用的所有内存都会存在。(用的时候生产一个,不用的时候扔掉)。
- 配置表:减少一次性使用的配置表数量;不要把整个配置表都扔进去,是否能切分下配置表。
- 单例:慎用,游戏一开始到游戏死掉,一直在内存中。