内存扩展:UE中利用IOS新的内存特性

Memory Expansion: Utilize new memory features of IOS in UE

内存优化是游戏开发中经常关注的课题,为了避免App过度分配内存触发OOM被系统强杀,通常的优化手段是从使用层面入手,提升内存的利用效率,裁剪不需要的功能、控制加载的资源等等。

但还有一种情况,提升App触发OOM的阈值,让系统允许我们的App分配更多的内存。在新的IOS版本中,苹果为App引入了新的内存特性,可以允许App扩展寻址空间和提高可分配内存。

本篇文章,我将研究如何把这些特性在UE内利用起来,提高游戏在IOS平台的可分配内存总量。

Apple系列的系统平台中,都提供了一种Entitlements机制,和权限授权不同,它可以授予指定App支持访问特权的能力,如访问HomeKit、地图、iCloud等等,根据具体应用的需要选择性授予,避免能力的滥用。

在Apple Developer中注册App ID时,在创建Identidiers时,会让用户选择App要使用的能力。

Forward

允许App分配更多内存

在IOS15.0+、iPadOS 15.0+的系统版本中,Apple为App开放了一种能力,允许App使用更多的内存,提高App内存分配触发OOM的阈值。

支持版本:iOS 15.0+、iPadOS 15.0+
Entitlement Keycom.apple.developer.kernel.increased-memory-limit

苹果官方的介绍:

将此权利添加到您的应用程序,以通知系统您的应用程序的某些核心功能可能会因超出受支持设备上的默认应用程序内存限制而表现更好。如果您使用此权利,请确保您的应用程序在额外内存不可用时仍能正常运行。

但苹果没说具体的设备上可分配内存数量能提升多少,后面我会用手头的设备在UE中做一些内存分配测试。

扩展虚拟内存地址

从IOS11开始,Apple就强制要求,、所有上架App Store的App都必须支持64位,32位应用不再支持。

64位应用的好处显而易见:指针范围更大,可使用的虚拟内存也更大,也能超越32位的4G内存限制。在目前流行的设备中,应该绝大部分设备都升级到了IOS11+,所以充分利用64位的性能是有必有的。

在IOS14.0+、iPadOS 14.0+的系统版本中,Apple为App开放了一种能力,允许App扩展虚拟内存地址:Extended Virtual Addressing Entitlement

苹果官方的介绍:

如果您的应用有需要更大可寻址空间的特定需求,请使用此权利。例如,内存映射资产以流式传输到 GPU 的游戏可能受益于更大的地址空间。
使用 Xcode 项目编辑器中的“扩展虚拟寻址”功能启用此权利。

启用之后,内核将启用jummbo mode,该模式为进程提供完整的64位地址空间访问权限。

支持版本:iOS 14.0+、iPadOS 14.0+、tvOS 14.0+
Entitlement Keycom.apple.developer.kernel.extended-virtual-addressing

国外有位大佬做了IOS虚拟内存扩展的分析:Size Matters: An Exploration of Virtual Memory on iOS

参考资料

实现方式

Capabilities启用

首先要在Apple Developer的页面里开启对应的Capabilities

添加之后下载新的MobileProvision,可以在UE中导入它。

如果是原生XCode工程,可以在Xcode中添加Entitlements:

以上操作,会在*.xcodeproj*的同级目录创建一个*.entitlements文件,记录着对应的状态:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.developer.kernel.extended-virtual-addressing</key>
        <true/>
</dict>
</plist>

注意,App的Mobile Provision一定要启用对应的能力,不然会在签名时失败。

检查MobileProvision

Mobile Provision是IOS开发的设备描述文件,用于记录证书信息、设备UUID列表、Bundle Identifier等等。

当在App ID中开启大内存支持后,使用MobileProvision打包之前,需要对Mobile Provision中支持的Entitlements进行检查,确保其包含我们需要的两个Key:

1
2
com.apple.developer.kernel.increased-memory-limit
com.apple.developer.kernel.extended-virtual-addressing

*.mobileprovision*文件是二进制格式,不能直接使用文本方式打开。但可以在mac上使用security来查看:

1
security cms -D -i imzlp.mobileprovision

会输出该MobileProvision的具体信息,检查其中是否包含以下两个Key,且值是否位true

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Entitlements</key>
<dict>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
</dict>
</dict>
</plist>%

UE中的Entitlements

UE中,在打包时会生成entitlements,会保存在Intermediate-IOS-*.entitlements

*.entitlements
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>get-task-allow</key>
<true/>
</dict>
</plist>

它是在UBT中生成的,具体代码为:Programs/UnrealBuildTool/Platform/IOS/IOSExports.cs
其中有一个WriteEntitlements函数,解析MobileProvision文件,检测里面是否有指定的Identifier。

默认情况下引擎里没有提供方便的追加元素的方式,只能修改UBT实现。

在UE中开启大内存支持

前面提到了UE中,UE中默认没有可以方便追加元素的方式,只能修改UBT的代码实现扩展:

WriteEntitlements中加入以下代码:

IOSExports.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//++[lipengzha] support memory-limit
Action<string,string> SupportMemoryOpt = (BoolName,IdStr) =>
{
bool bBoolNameValue = false;
// read property in DefaultEngine.ini
PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", BoolName, out bBoolNameValue);
Console.WriteLine(string.Format("Write {0}({1}) {2} entitlements",BoolName, bBoolNameValue?"true":"false" ,IdStr));

if (bBoolNameValue)
{
Text.AppendLine(string.Format("\t<key>{0}</key>",IdStr));
Text.AppendLine(string.Format("\t<{0}/>", bBoolNameValue ? "true" : "false"));
}
};
SupportMemoryOpt("bIncreasedMemoryLimit","com.apple.developer.kernel.increased-memory-limit");
SupportMemoryOpt("bExtendedVirtualAddressing","com.apple.developer.kernel.extended-virtual-addressing");
//--[lipengzha]

然后在项目的DefaultEngine.ini中的[/Script/IOSRuntimeSettings.IOSRuntimeSettings]添加以下项:

DefaultEngine.ini
1
2
bIncreasedMemoryLimit=True
bExtendedVirtualAddressing=True

就可以通过控制项目中的配置,来决定是否启用内存扩展的能力。

在打包时就会有以下的Lod:

IOSExport.cs中的逻辑依托于UBT执行,所以如果修改了entitlement,也要让代码有变动(只要UBT能启动编译就可以),不然会出现以下错误:

1
PackagingResults: Error: Entitlements file "MemoryProfiler.entitlements" was modified during the build, which is not supported. You can disable this error by setting 'CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION' to 'YES', however this may cause the built product's code signature or provisioning profile to contain incorrect entitlements. (in target 'MemoryProfiler' from project 'MemoryProfiler')

也可以删除Intermediate/IOS目录后重试:

在UE5.1及后续版本中,引擎中可以通过控制DefaultEngine.ini中的[XcodeConfiguration]bUseModernXcode值,来控制是否在构建流程中允许修改entitlement的值,避免该错误。引擎中的代码:IOSExports.cs#L301
以及在UE5.2中,引擎的IOSToolChain.cs中将CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION默认设置为YESIOSToolChain.cs#L1807

因为直接在Mac上打包和使用远程构建,最终产生IPA时的逻辑不同。如果是直接在Mac上打包,还需要在IOSExport.csAppendProjectBuildConfiguration函数中添加CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION的定义,它会在xcode工程的BuildSettings-User-Defined中:

UnrealBuildTool/ProjectFiles/Xcode/XcodeProject.cs
1
2
3
4
5
6
7
private void AppendProjectBuildConfiguration(StringBuilder Content, string ConfigName, string ConfigGuid)
{
// ...
//++[lipengzha] support xcode14+
Content.Append("\t\t\t\tCODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;");
//--[lipengzha]
}

测试数据

测试基准:iPhone12,IOS 16.1、iPad Pro 3rd,iPadOS 16.2、iPhoneXs iOS 14.7

测试工程环境:UE4.27.2,空C++工程,不包含任何额外资源。
测试方式:

  1. 默认实现打包,运行游戏、分配内存直到OOM。
  2. 支持Memory-Limit,运行游戏、分配内存直到OOM。

两者工程和代码均一致,仅有Memory-Limit的支持区别。

运行时的内存分配方式:每次分配1M,直到触发系统OOM,统计分配的内存大小。

Original

iPhone12 Original,游戏启动后的内存:

1
2
LogMemoryUsageProfiler: Display: Constants: TotalPhysical 2099.20 MB, TotalVirtual 2099.20 MB PageSize 16384 byte, OsAllocationGranularity 16384, Constants 65536 BinnedAllocationGranularity 0, AddressLimit 100000000
LogMemoryUsageProfiler: Display: Stats: Mem Used 217.73 MB, Texture Memory 8.60 MB, Render Target memory 0.07 MB, OS Free 1881.47 MB

OOM前的内存:

1
2
3
LogMemoryUsageProfiler: Display: AllocSystemMemory: 1 M, Alloced 1889 (M)
LogMemoryUsageProfiler: Display: Constants: TotalPhysical 2099.20 MB, TotalVirtual 2099.20 MB PageSize 16384 byte, OsAllocationGranularity 16384, Constants 65536 BinnedAllocationGranularity 0, AddressLimit 100000000
LogMemoryUsageProfiler: Display: Stats: Mem Used 2098.19 MB, Texture Memory 8.60 MB, Render Target memory 0.07 MB, OS Free 1.01 MB

在距离OOM还有410M时,App触发了系统的内存警告:

1
2
3
4
5
LogMemory: Platform Memory Stats for IOS
LogMemory: Process Physical Memory: 1688.24 MB used, 1688.24 MB peak
LogMemory: Process Virtual Memory: 400957.09 MB used, 400957.09 MB peak
LogMemory: Physical Memory: 1688.24 MB used, 410.96 MB free, 2099.20 MB total
LogMemory: Virtual Memory: 2099.20 MB used, 0.00 MB free, 2099.20 MB total

Memory-limit

iPhone12 Memory-limit版本,游戏启动后的内存:

1
2
LogMemoryUsageProfiler: Display: Constants: TotalPhysical 2867.20 MB, TotalVirtual 2867.20 MB PageSize 16384 byte, OsAllocationGranularity 16384, Constants 65536 BinnedAllocationGranularity 0, AddressLimit 100000000
LogMemoryUsageProfiler: Display: Stats: Mem Used 735.51 MB, Texture Memory 8.60 MB, Render Target memory 0.07 MB, OS Free 2131.69 MB

OOM前的内存:

1
2
3
LogMemoryUsageProfiler: Display: AllocSystemMemory: 1 M, Alloced 2139 (M)
LogMemoryUsageProfiler: Display: Constants: TotalPhysical 2867.20 MB, TotalVirtual 2867.20 MB PageSize 16384 byte, OsAllocationGranularity 16384, Constants 65536 BinnedAllocationGranularity 0, AddressLimit 100000000
LogMemoryUsageProfiler: Display: Stats: Mem Used 2866.86 MB, Texture Memory 8.60 MB, Render Target memory 0.07 MB, OS Free 0.34 MB

在距离OOM线约460M时,App触发了系统的内存警告:

1
2
3
4
5
LogMemory: Platform Memory Stats for IOS
LogMemory: Process Physical Memory: 2407.14 MB used, 2407.14 MB peak
LogMemory: Process Virtual Memory: 401157.25 MB used, 401157.25 MB peak
LogMemory: Physical Memory: 2407.14 MB used, 460.06 MB free, 2867.20 MB total
LogMemory: Virtual Memory: 2867.20 MB used, 0.00 MB free, 2867.20 MB total

总结

启用Memory-Limit机制两个版本对比数据:

Original Memory-Limit
iPhone12(iOS16.1) 1889 2139
iPadPro 3rd(2021, iPadOS 16.2) 4864 7927
iPhoneXs(iOS 14.7) 1892 2144

在iPhone12上,开启之后增加了250M可分配内存。

而在iPad Pro 3rd(2021)上,则增加了惊人的3063M!

启用该特性,在高端设备上提升最为明显。不需要做额外的内存使用优化,就从系统手中捡了一大块物理内存,岂不美哉。

补充

IOS使用的内核也是基于XNU的,以及MacOS使用的均是类Unix内核。

本篇文章介绍的,虚拟内存扩展提升App内存上限两项权利,是在xnu-7195.50.7.100.1内核中支持的,所以,只要是基于该内核的IOS版本,都有提升。

可以从查询IOS版本与XNU内核的对照表:Kernel - The iPhone Wiki

可以看到,从IOS14.3 beta开始,XNU的内核版本就已经升级到了xnu-7195.60.63~22,实际上就具备了内存扩展所支持的内核代码。

实际测试,在iPhone11(iOS 14.7)的版本中同样具有提升效果:

Original Memory-Limit
iPhoneXs(iOS 14.7) 1892 2144

Update

2023.08.14

UE5.3 Roadmap平台相关的更新中,为IOS的extended-virtual-address做了支持,当开启后,可以使用MallocBinned2分配器。

所以理论上来说,也能把Binned2的支持扩展到UE4中,有空研究一下。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:内存扩展:UE中利用IOS新的内存特性
文章作者:查利鹏
发布时间:2023年01月31日 10时37分
本文字数:本文一共有4.2k字
原始链接:https://imzlp.com/posts/56381/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!