说到程序内存性能优化,老方法论了,须要建立起对进程内存机制的系统认识。主要分为六个部分(从高地址开始):命令行操作与环境变量,栈,堆,未初始化数据段,已初始化数据段,代码段。
图:来源 https://www.geeksforgeeks.org/
其中堆作为内存优化的核心问题,最后介绍,先讲其他部分。
命令行操作与环境变量
略。
代码段
- 即编译好的代码,一次性被载入的操作指令。
- 代码段是只读的,以防止程序以外修改指令。
- 文本段在堆栈的下面,防止堆栈溢出覆盖它。
- 通常代码段是共享的。
数据段:已初始化
- 可以理解为编译型静态数据区。
- 全局的。
- 已初始化的。
- 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局常量(public const)和已初始化全局变量(public static)。
数据段:未初始化
- 又称为BBS段。
- 全局的。
- 可以理解为运行时静态数据区。比如存储全局运行时只读型变量(public static readonly)和未初始化的全局变量(public static)。
栈
空间有限,数据类型简单,生命周期短,存储机制简单(压栈 / 出栈:后进先出FILO(First In Last Out))。
堆
速度慢,频繁使用容易产生碎片,减小命中率。
Unity的堆内存空间分为两部分,一部分是托管堆(Managed Heap),集成了垃圾回收器(GC机制);另一部分是本机堆(Native Heap),内存回收需要显式明确的调用相应接口。
托管堆
New生成的容器,实例中的各种声明的变量等,有自动GC(Unity 2019.1新功能: 增量式垃圾回收)机制。
一些优化方式:
- 缓存/对象池
- List或则Dictionary使用Clear(),减少New()
- 在Update函数里频繁New对象
- 降低逻辑执行频率
- 减少装箱拆箱的类型转换操作
- 使用更简单的数据类型
- string的本质是字符数组,+操作开销大,可以考虑StringBuilder
- coroutine的使用注意:yield return 0/null,new WaitForSeconds()
- for遍历时,提前定义好循环的总次数
- 直接把对象作为引用,改为使用对象的索引
- 在合适的场合使用Struct替代Class
托管堆的GC机制
触发条件:
- 极端情况,如:系统内存耗尽
- 主动调用GC函数
- 主动申请
- 被动申请
新申请堆内存时,先检测有没现成可分配的内存块,若没有,再GC,如果GC之后仍没有可分配内存,再向操作系统申请(用户态转向内核态)。
所以尽可能不触发GC机制,目前的GC算法肯定是一种综合策略,其中标记整理算法会由于定义的对象,对象的引用数量过多而消耗太多CPU时间。
GC的步骤:1,检查堆中的每一个对象;2,检索每一个当前对象的引用以判断是否可以删除;3,标记可以删除的对象;4,删除被标记的对象,回收内存。
本机堆
Unity自己的内存管理,模型,贴图,音效等原始资源,主动回收通过Destroy,Resources.UnloadAsset(Obj),Resources.UnloadUnusedAssets和AB.Unload(true/false)实现。
图:Unity AssetBundle的内存管理(PS:现在不要用WWW来下载,通过HttpRequest性能更好)
Destroy
对应上图中Instantiate的那部分Objects。除非缓存和对象池的对象,否则我们一般都会使用这个API进行资源卸载,比如UI。
Resources.UnloadAsset(Obj)
卸载Resources.Load(一般只会是logo和splash一类的资源)和AB.Load的Asset备份
Resources.UnloadUnusedAssets
一般在场景切换的时候调用(Unity会隐式调用),并不能释放AB镜像,AB镜像通过AB.Unload(false)释放。
AB.Unload:主动实现的引用计数机制
因为一些大型的游戏对内存管理的要求比较高,如果不做当前场景内的内存清理,很容易内存占用过大,导致系统性能下降,所以需要使用到API:AB.Unload。
而因为有Unload(true)和Unload(false)两种方式,所以这里就有两种回收粒度的策略。
- Unload(true):(慎用!!!)同时清理AB镜像和AB.Load出来的Asset备份,需要对AB和AB.Load出来的Asset备份做引用计数。
- Unload(false):只清理AB镜像,只需要对AB做引用计数。
当统计的某个资源对象引用计数为0时,即时处理,如果延时处理复杂度就上去了。
PS:降低资源质量,这是有损优化,一般作为最后的手段。