![腾讯游戏开发精粹Ⅲ](https://wfqqreader-1252317822.image.myqcloud.com/cover/420/52521420/b_52521420.jpg)
3.3.3 内存优化
整体上对内存占用的目标是,内存占用尽可能少,这不仅是UE在运行时的一个内存占用的考量,当UE退出或被销毁或在后台运行时,需要尽量减少系统资源的使用,以降低系统负担。这里的挑战在于实际的内存预算是有限的,尤其是存在于App中的SDK。
3.3.3.1 内存分配体系
UE的整个内存分配体系如图3.19所示[7]。通过Operator New和Operator Delete能够执行大部分基本的内存分配操作,整个UObject体系使用自己的一套内存分配方法。这些内存分配方法最终实际使用的都是底层的某种内存分配器。移动端可用的分配器选择只有几种,并不是所有的内存分配器都可以被支持,对于PC平台都是可以的。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_33.jpg?sign=1739085938-u3lisPraURfZmFenUzDEAgQSWURQY6Ha-0-5c2afaa8a803260adf602a82629b7095)
图3.19 内存分配体系简介
3.3.3.2 内存碎片化
内存碎片化指内存中存在一些不连续的小块空闲内存。操作系统底层是以页为单位进行操作的,通常是指固定大小的内存块。一页中有很多槽,只要有一个槽被占据,这一页就不能被释放掉,如图3.20所示。
Binned和Binned2是UE的两种内存分配器。
Binned分配器是一种基于固定大小的内存块的分配器,它将内存块分成不同的大小类别,并将它们存储在不同的“桶”中。当需要分配内存时,Binned分配器会从适当的桶中选择一个内存块,并将其返回给调用者。
Binned2分配器是Binned分配器的改进版本,它使用了更高效的算法来管理内存分配。与Binned分配器不同,Binned2分配器可以动态地调整内存块的大小,并且可以在多个线程之间共享内存池。Binned2分配器的多线程设计使得它的速度要优于Binned分配器。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_34.jpg?sign=1739085938-2jZvwG7HHr4MdmMkEuJqS70OqiPsTLxD-0-0ae5750024819bb84838dd7130d880cd)
图3.20 内存碎片化图示
这里想测试的点在于,哪种分配器在实际的业务场景下内存的使用率更高,从两个方面进行比较,UE运行时的状态和UE退出之后的状态。
表3.7和表3.8展示了不同状态下的内存分配结果。在UE运行的时候,两种分配器的内存使用效率都是很高的,基本没有低于90%的情况。但是在引擎退出的时候,不同内存分配器的使用率都有所降低,对比之后,Binned方式更加符合需求,这里希望引擎退出的时候内存占用尽量少,所以这里选择了第一代Binned分配器。上线测试Binned分配器发现有问题,进行了修复,最终选择了修改过后的Binned算法。由于Binned2分配器是多线程的,所以表格中有两组数据。
表3.7 UE退出状态的内存分配结果
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_35.jpg?sign=1739085938-v3NNOL1VaOMHL33Rv4y4DCGAP4PkY5uU-0-1d9b326be33810c1272b6e3573a5dccc)
表3.8 UE运行时状态的内存分配结果
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_36.jpg?sign=1739085938-rvwKhaNMzOdTkpK43ggWQEIcpuf0wqZP-0-f874b992253548e921f8a0afb5828d87)
从表3.7中可以看到,UE在退出的时候依然会占据80MB左右的内存,实际占用60MB左右的内存。由于在方案采用的引擎退出策略中,有大量的反射数据并没有被析构掉,UStruct、UClass,以及一些Plugin模块的内容仍然存在,所以这部分的内存开销是相对合理的。
3.3.3.3 有限的地址空间
一般情况下,实际程序中使用的地址空间会远远大于实际物理内存的使用量,如图3.21所示[8]。比如,把一个50MB左右的二进制代码文件加载进来,它是会占用使用的地址空间的。Code段中的这个二进制代码文件中的很多代码都没有被执行,所以在物理内存中实际是没有分配50MB这么多的。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_37.jpg?sign=1739085938-WH8WbopLLCN7L7KQblo7UhbGkPB44YN2-0-f5ac10cba4b0131045f474dd685675b4)
图3.21 地址空间与物理地址空间
那为什么要讲地址空间的事情呢,原因是,在测试中(iOS的环境下)遇到了虚拟地址空间不足的问题。实际问题是,分配一张渲染目标的时候,由于内存不足创建失败了,导致程序崩溃。这个时候看实际的物理内存占用只有700MB,而测试的机器都是3GB内存的机器。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_38.jpg?sign=1739085938-VnCeG6m5ldK7zN8oGHYUrlfWToTQNQYP-0-5018e9a744b3ad285a0849aa74cc8431)
对于iOS的设备来说,一般达到物理内存的上限是设备内存的一半,对于3GB的机器来说就是1.5GB,但是实际的物理内存使用量为700MB,远没有达到上限。那么首先想到的原因是上文提到的内存碎片化的问题,是不是由于碎片化过于严重导致内存分配失败呢?并不是,在一些测试场景下,UE运行一会儿程序就崩溃了,所以这里推测不大可能是内存碎片问题导致的。于是又在iOS设备上做了更多的测试,发现在没有UE的情况下,也会出现一样的问题。
翻阅iOS内核代码(xnu):
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_39.jpg?sign=1739085938-WcWnexz1WYRXGMAdsp6VE9hVmLacJMB8-0-f878d0db7fa523c3ae5a8022baf72a41)
从代码中可以看到,这里实际上是有限制的。iOS进程要有4GB的PAGE_ZERO和4GB的Shared Region的占用,导致实际可用的地址空间需要减去8GB,如表3.9所示。
表3.9 地址空间大小
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_40.jpg?sign=1739085938-Y29ptdMpShKd7lDLlf7pjxy8CQX7vQmX-0-c45ee3c97e417f5b412227b3d0fb7233)
但是上文也分析过,UE的内存分配器的效率是非常高的,如果3.375GB已经被占满了,实际物理内存的占用率也会很高,但实际情况并不是这样的。于是又做了一些测试,发现在QQ启动的时候,虚拟地址空间已经占用3GB多了,留给UE的只剩700多MB,这也就比较好地解释了为什么UE刚刚拉起,并且在进入游戏的时候,非常容易发生这种崩溃的问题,它并不是UE本身的问题。
在iOS 14以上的版本中可以通过下面这个属性增加虚拟地址空间的使用上限[9]:
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_41.jpg?sign=1739085938-EDn2PDKu55mKFrL771AthfJPdK3m416g-0-cdf13919818b544bf5904cbb4bc64133)
在iOS 15以上的版本中可以通过下面这个属性增加物理内存的使用上限:
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_42.jpg?sign=1739085938-0bBw2MOOH9KXY6n87VtMYHIhVxOoKJUy-0-7ce91adcf42ccaa86d399efb3fc30531)
3.3.3.4 在Android中分离二进制代码文件
通过分析Linkmap可以发现,游戏侧业务的代码消耗了相当一部分的内存,无论是Code段还是Data段,因此想到了修改UBT分离主体和游戏侧业务逻辑的代码。比如在QQ秀的业务上,并不需要任何游戏业务的代码内容,那这部分代码是不需要被加载进来的。如图 3.22所示,流程上在UBT中分析UE的主体代码,编译成SO,再把游戏侧业务的代码也编译成一个SO,为了避免符号表导出冗余,先收集游戏侧业务代码需要导入的符号表的内容(输出Version Script),然后再重新进行链接操作,主体SO导出的符号表就只包含游戏侧业务代码用到的内容了,保证最终导出的两个SO相比最初的SO不会增加过多冗余。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_43.jpg?sign=1739085938-BMu3lwxkdnq04UFCq1aCD9J5IkGOxWNU-0-55fa8cbae993344bbd9bde9b2071bfa7)
图3.22 分离SO流程
在Android端做了这部分处理,好处是可以根据需求动态地加载/卸载这部分库文件,增加了灵活性,分离的结果如图 3.23所示。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_44.jpg?sign=1739085938-5Xj87WJUA0kc0mi5JAb3hbd6wlfhGvJq-0-63b51c37467c7808e769fc9b05530e54)
图3.23 分离SO的结果
3.3.3.5 其他优化项
还可以从其他方面着手降低内存的使用,这些方式与游戏开发的优化手段相似,这里只做部分罗列,不再展开详述。
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_45.jpg?sign=1739085938-hwz8awSdNjpgKCRnbuuIc8Xjk0EJvjja-0-f7ff2c01881f19ca6beabbccf905f968)
3.3.3.6 数据结果
表3.10展示了两个版本经过一系列优化迭代后的内存数据对比分析,可以看到,内存占用相比较早期版本有了大幅优化。
表3.10 内存优化前后的数据对比(iPhone 13 Pro Max)
![](https://epubservercos.yuewen.com/0BF2CA/31154700307361506/epubprivate/OEBPS/Images/txt004_46.jpg?sign=1739085938-GCh6i8Sh4VssQEY9AEgyyUMfRV5MNlyz-0-9ebd93fc6953d0ed75bb6f86fe6316a5)