UE5安卓包体优化实战

UE5安卓包体优化实战

看了好几个大佬写的包体优化的文章了,一直没实际操作一下,趁现在手里头没啥活,实操一下(摸鱼一下),巩固一下知识。

打包设置

包体优化,首先需要知道包体里面有哪些东西,以第三人模板为例,引擎版本5.3.2(源码版本引擎),修改一下打包设置,然后打个包。

1773675029564

把游戏资源都打包进入apk

1773675526759

因为是打最小包,所以直接打shipping包

1773675772400

这个就是我们需要的文件了,其他文件暂时先放一边
1773675901048

包体大小分布

查看APK可知,包体主要是包含以下几项:

  • assets文件下的main.obb.png(游戏内资源 Pak)
  • lib文件下的可执行代码(so - lib/arm64-v8a
  • 其他第三方组件拷贝进 APK 内的文件

1773676113059

根据上面这三种资产开始分别优化

压缩 NativeLibs

对于原生 Android 而言,是否在安装时解压 NativeLibs 是由 AndroidManifest.xml 中的 extractNativeLibs 控制的:

1
<application android:allowBackup="true" android:appComponentFactory="android.support.v4.app.CoreComponentFactory" android:debuggable="true" android:extractNativeLibs="false" android:hardwareAccelerated="true" android:hasCode="true" android:icon="@drawable/icon" android:label="@string/app_name" android:name="com.epicgames.ue4.GameApplication" android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true">

在新版引擎中,在 AndroidRuntimeSettings 配置中直接提供了 bExtractNativeLibs 的选项:

1
2
bool bExtractNativeLibs = true;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bExtractNativeLibs", out bExtractNativeLibs);

这个我根据大佬的文章来看,在UE5.3中是默认开启,于是我关闭后打包测试对比一下

1
2
3
//DefaultEngine.ini
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
bExtractNativeLibs = False;

1773677287775

前面是修改后,后面是默认开启

arm64-v8a Shipping 修改后 修改前 变化
libEOSSDK.so 21.12MB 7.91MB 13.21MB
libpsoservice.so0.2 0.23MB 0.08MB 0.15MB
libUnreal.so 100.4MB 39.39MB 60.55MB

代码体积优化

对于 UE 项目而言,优化 so 的大小有以下几种思路:

  1. 禁用不必要模块
  2. 控制代码优化(控制 inline/O3/0z)
  3. 禁用 Module 不必要异常处理
  4. 启用 LTO
  5. 剔除不需要的导出符号

禁用模块

可以把引擎中内置的明确不需要使用的模块在 target.cs 中关闭,具体有哪些可以修改的配置可以在”\Engine\Source\Programs\UnrealBuildTool\Configuration\TargetRules.cs”中查看:

1
2
3
4
5
6
7
8
9
// disable modules  
//物理效果 引擎版本5.1之前默认开启
bUseChaos = false;
bCompileChaos = false;
bCompileAPEX = false;
//导航相关模块
bCompileRecast = false;
bCompileNavmeshSegmentLinks = false;
bCompileNavmeshClusterLinks = false;

按需求关闭,因为第三人称模版中不需要物理效果和导航,所以可以关闭。

关闭异常处理

有些模块中打开了 C++ 异常处理,但是没有 try/catch 的使用:

1
bForceEnableExceptions = false;//默认关闭

修改编译器优化模式

target.cs 中控制 bCompileForSize 的值,可以选择使用 O3 或 Oz 编译代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum OptimizationMode
{
/// <summary>
/// Favor speed
/// </summary>
Speed,

/// <summary>
/// Favor minimal code size
/// </summary>
Size,

/// <summary>
/// Somewhere between Speed and Size
/// </summary>
SizeAndSpeed
}
1
2
//这里选Size,// Favor minimal code size
OptimizationLevel = UnrealBuildTool.OptimizationMode.Size;

启用 LTO

LTO 是 Link Time Optimization 的简称,可以在链接时剔除死代码、优化跨模块的函数调用、内联等。

bAllowLTCG 参数控制,选择是否添加 -flto=thin 的编译参数,thin 是缩减大小与优化耗时的综合版本。

1
2
3
4
5
bAllowLTCG = true; // LTO
if (bAllowLTCG)
{
AdditionalCompilerArguments += " -flto=thin";
}

剔除导出符号

在编译 so 时,除非特殊设置,所有的函数和变量都会被导出,用于被其他的 so 访问。
但在 UE 引擎内,只有极少数的接口,是明确被外部访问的(JNI 相关的接口),所以 libUnreal.so 的符号导出绝大部分是浪费的,剔除掉符号导出可以大幅降低 so 的大小和内存占用

现代编译器提供了 version-script 的链接时控制机制,可以通过传入一个 ldscript 文件来控制链接时的符号行为。

需要在编译过程中先构造出一个 ldscript 文件,填入符号导出控制代码,然后在 target.cs 中,传递给 Linker:

1
2
3
4
5
6
string VersionScriptFile = GetVersionScriptFilename();
using (StreamWriter Writer = File.CreateText(VersionScriptFile))
{
Writer.WriteLine("{ global: Java_*; ANativeActivity_onCreate; JNI_OnLoad; local: *; };");
}
AdditionalLinkerArguments += " -Wl,--version-script=\"" + VersionScriptFile + "\"";

对于 UE 而言,需要允许导出的只有 Java_*/ANativeActivity_onCreate/JNI_OnLoad 这三类匹配符号,,其余的均可剔除。

1773736900093

优化后(对比上一步)

arm64-v8a Shipping 修改后 修改前 变化
libEOSSDK.so 7.91MB 0 7.91MB
libpsoservice.so0.2 0.23MB 0.08MB 0MB
libUnreal.so 39.39MB 33.04MB 9.35MB

内存收益

安卓可以通过 dumpsys meminfo 来查看整个包的 so 占用内存情况,包含了所有已加载的 so,但可以通过优化前后的差值得到实际的内存收益。

优化前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
** MEMINFO in pid 28879 [com.YourCompany.MinAPKTest] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free

------ ------ ------ ------ ------ ------ ------ ------

Native Heap 46449 46376 56 464 47904 86532 62216 18132
Dalvik Heap 4801 3196 1496 25 10488 27461 2885 24576
Dalvik Other 13542 2136 220 528 24860
Stack 2192 2184 8 68 2200
Ashmem 18 0 0 0 1532
Gfx dev 23748 23640 108 0 23752
Other dev 428 316 72 0 1336
.so mmap 110017 12440 92812 20 169000

优化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Uptime: 663111257 Realtime: 1598890414

** MEMINFO in pid 28481 [com.YourCompany.MinAPKDemo] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 43661 43620 4 424 45212 73116 50184 16845
Dalvik Heap 4445 4400 4 3904 9932 11157 2965 8192
Dalvik Other 13238 1536 0 1578 25060
Stack 1984 1984 0 80 1992
Ashmem 19 0 0 0 1540
Gfx dev 28440 28420 20 0 28444
Other dev 217 0 36 0 1300
.so mmap 70732 10272 52188 124 135356
arm64-v8a Shipping 优化前 优化后 减少
so 总内存 110017 70732 39285

优化策略补充

重定位表压缩

SDK 28

在 Android 的 MinSDKVersion 大于等于 28 时(Android9),可以在编译和链接时开启 RELR 重定位表压缩。利用相对地址重定位的特点,对重定位信息进行高效编码,从而减少存储空间占用。

开启方法,需要在编译阶段给 Compiler 和 Linker 传递参数:

1
2
AdditionalCompilerArguments += " -fPIC";  
AdditionalLinkerArguments += " -Wl,--pack-dyn-relocs=android+relr,--use-android-relr-tags";

-Wl,--pack-dyn-relocs=android+relr,--use-android-relr-tags 是 Android 特有的链接器选项,它们是对标准 -Wl,-z,relro-Wl,-z,now 的补充和优化,特别是针对 Android 系统中动态链接和重定位的处理。 它们主要用于进一步减小二进制文件大小和改善加载时间。

优化前后:

可以使用安卓SDK的工具llvm-readelf.exe查看libUnreal.so

1
\..\AppData\Local\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-readelf.exe -d "G:\YourProject\Binaries\Android\libUnreal.so"

1773739579830

1
2
2.8 MB  →  155 KB
≈ 减少 95%+

资源优化:

主流方法是把资源合理规划拆分,默认启动界面作为一个资源加载关卡,其他所有资源拆分成pak包,后续通过下载的方式动态挂载,后面有时间实现一下。

总结

这是我最后在.Target.cs的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;
using System.Collections.Generic;
using System.IO;
using System;

public class MinAPKDemoTarget : TargetRules
{
public MinAPKDemoTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V4;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_3;
ExtraModuleNames.Add("MinAPKDemo");
//bForceEnableExceptions = false; //默认关闭
// 前提:必须已经开启了独占环境
BuildEnvironment = TargetBuildEnvironment.Unique;
// 安卓打包专属优化
if (Target.Platform == UnrealTargetPlatform.Android)
{
// 剔除不需要的引擎功能
bCompileRecast = false;
bCompileNavmeshSegmentLinks = false;
bCompileNavmeshClusterLinks = false;

OptimizationLevel = OptimizationMode.Size;

//开启 LTO 需要源码引擎
bAllowLTCG = true;
if (bAllowLTCG)
{
AdditionalCompilerArguments += " -flto=thin";
//重定位表压缩
AdditionalCompilerArguments += " -fPIC";
AdditionalLinkerArguments += " -Wl,--pack-dyn-relocs=android+relr,--use-android-relr-tags";
}
//剔除导出符号
try
{
// 定义 version-script 文件路径
string VersionScriptFile = Path.Combine(ProjectFile.Directory.FullName, "Intermediate", "Android", "ldscript.version");
string DirectoryPath = Path.GetDirectoryName(VersionScriptFile);
if (!Directory.Exists(DirectoryPath))
{
Directory.CreateDirectory(DirectoryPath);
}
// 写入符号导出控制
File.WriteAllText(VersionScriptFile, "{ global: Java_*; ANativeActivity_onCreate; JNI_OnLoad; local: *; };");
// 传递给链接器
AdditionalLinkerArguments += string.Format(" -Wl,--version-script=\"{0}\"", VersionScriptFile);
Console.WriteLine("Successfully applied Symbol Stripping Version Script: {0}", VersionScriptFile);
}
catch (Exception Ex)
{
Console.WriteLine("Failed to create Version Script: " + Ex.Message);
}
}
}
}