In game projects, when we package for various platforms, we always hope that the package for each platform can be minimized for easier distribution, and there are specific size requirements for some platforms.
For UE, it contains massive code and numerous plugins, and during the Build phase, it generates a lot of reflection glue code, resulting in a significant increase in code segments during compilation. Taking the Android platform as an example, this leads to a sharp increase in the size of libUE4.so, putting pressure on both the package size and runtime memory.
Moreover, some necessary and additional resources brought in by the engine can take up hundreds of MB, making the size of an empty APK easily reach several hundred MB! Not only to meet the platform’s requirements, but it is also necessary to trim down the size of the UE package from the perspective of package size and memory optimization.
This article will take Android as an example and introduce optimization ideas and practices for the cuttable parts in the UE package from various aspects, optimizing both the APK size and the runtime memory usage of native libraries. The strategies can also be reused on other platforms.
Package Size Distribution
In the APK, the significant portion of space related to the game consists mainly of the following:
- Executable code (so -
lib/arm64-v8a
) main.obb.png
(game resource Pak, part ofDirectoriesToAlwaysStageAsNonUFS
)- Files of third-party components copied into the APK
Specific optimization strategies need to be formulated for each of the three scenarios listed above.
Compressing NativeLibs
When the APK is installed, there are two processing methods for NativeLibs:
- Unzip so to the application’s internal storage directory (
/data/app/<package_name>/lib/
) - Load so directly from the APK file, which can speed up the installation process
This raises a question: If unzipping during installation is allowed, then the NativeLibs packed into the APK can be executed using compression. Comparing the actual situation of compression versus non-compression shows a significant impact on APK size:
Compression | No Compression |
---|---|
![]() |
![]() |
For native Android, whether to unpack NativeLibs during installation is controlled by extractNativeLibs
in AndroidManifest.xml
:
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"> |
In the new version of the engine, the option bExtractNativeLibs
is provided directly in the AndroidRuntimeSettings
configuration:
1 | bool bExtractNativeLibs = true; |
It is worth noting that for older versions of the engine (4.27 and earlier), after upgrading gradle ( > 4.2), gradle uses useLegacyPackaging
instead of extractNativeLibs
, and the default value of extractNativeLibs
in the manifest is false, which will lead to an increase in APK size.
The solution is to forcibly change the value in the UPL:
1 | <addAttribute tag="application" name="android:extractNativeLibs" value="true"/> |
Note: This only controls whether the so is compressed when packed into the APK and does not actually reduce the size of the so! For optimizing executable programs, it is necessary to continue with the code optimization part below.
Code Size Optimization
Regarding code size optimization on the Android platform, the core goal is to reduce the size of a single so while minimizing any impact on runtime performance.
Optimization ideas for the size of NativeLibs:
- Reduce the number of dynamic link libraries and eliminate unnecessary ones.
- Reduce symbols within the library and decrease the size of code segments.
- Remove debug information.
These optimization strategies can be applied to all sos at compile/link time.
However, for UE projects, the code we can control usually only involves the engine and project code, while the library code needs to be optimized by the library provider. Therefore, the following optimization strategies only apply tolibUE4.so
/libUnreal.so
.
Reducing libUE4.so
During packaging, since a complete compilation needs to be performed, and UE defaults to a Monolithic
mode at runtime, all code is compiled into the same executable file. (A previous article detailed this: UE Plugin and Tool Development: Basic Concepts)
The compilation process encapsulated by UBT, along with the provided configuration parameters in target.cs
/build.cs
, allows us to have some control over the compilation of engine and project code, achieving our goal to optimize so size.
For UE projects, there are several approaches to optimize the so size:
- Disable unnecessary modules
- Control code optimization (control inline/O3/Oz)
- Disable unnecessary exception handling in Modules
- Enable LTO
- Eliminate unnecessary export symbols
Disabling Modules
You can disable modules in the engine that are clearly not needed in target.cs:
1 | // disable modules |
At the same time, you need to sort out unnecessary runtime plugins included in the project to reduce the number of modules participating in compilation, thereby reducing the actual code that is compiled.
Disabling Inline
Inlining is an optimization during the compile phase to enhance runtime execution efficiency by replacing function calls directly with function code instead of conventional function calls. This can reduce the overhead of function calls and theoretically improve program execution efficiency.
However, inlining can increase the size of the .text
section, and it can be disabled as appropriate.
- Modify
target.cs
:bUseInlining = false;
(only effective on iOS/Linux/Mac/Win) - Modify UBT and control inlining during Android compilation by adding the compilation parameter
-fno-inline-functions
1 | if (TargetInfo.Platform == UnrealTargetPlatform.Android) |
Note: Disabling inlining may lead to performance loss if certain functions are called frequently; in non-frequent situations, the performance impact of inlining is minimal. In my testing results, the impact of inlining on frame rate is negligible.
Disabling Exception Handling
Some modules have C++ exception handling enabled without using try/catch
:
1 | bEnableExceptions = false; |
It can be turned off, which can reduce the size of .eh_frame
within the so.
Using O3/Oz Compilation
In target.cs
, you can control the value of bCompileForSize
to choose between O3 or Oz for compiling code:
1 | // optimization level |
Differences between O3 and Oz:
-O3
: performance-oriented, aggressively inlines and unrolls loops-Oz
: size-oriented, avoids inlining and maintains loops
You can choose which method to use based on the actual performance situation of the project.
Enabling LTO
LTO stands for Link Time Optimization, which can eliminate dead code, optimize cross-module function calls, inline, etc. In the engine’s build.cs
, you can enable bAllowLTCG
, which is an implementation of LTO but only effective on iOS/Linux/Mac/Win (UE4.25).
For Android support, you also need to modify UBT (AndroidToolChain.cs), adding a control parameter bAllowLTCG
to select whether to include the compilation parameter -flto=thin
, where thin
is a combination of reduced size and optimization time.
1 | bAllowLTCG = true; // LTO |
Eliminating Export Symbols
When compiling so, unless specifically set, all functions and variables will be exported for other sos to access. However, within the UE engine, only a very few interfaces are explicitly accessed from the outside (JNI-related interfaces), so the symbol export of libUE4.so
is largely wasted. Eliminating symbol exports can significantly reduce so size and memory usage!
Modern compilers provide a version-script
control mechanism at link time, which can control symbol behavior by passing a ldscript
file.
You need to construct an ldscript
file during the compilation process, fill in the symbol export control code, and then pass it to the linker in target.cs
:
1 | string VersionScriptFile = GetVersionScriptFilename(); |
For UE, only the symbols matching Java_*/ANativeActivity_onCreate/JNI_OnLoad
should be allowed to be exported, and all others can be eliminated.
Optimizing Data
After the series of code size optimizations introduced above, the benefits are apparent.
so Size After Compression
As mentioned earlier, NativeLibs can be compressed when packed into the APK, so by reducing the original size of the so, we can also reduce the size after compression.
After the optimization, in Shipping mode, the original size of the so decreased from 258M
to 146M
! The size after compression reduced from 74.3M
to 44.67M
, a reduction of 29.63M
! The executable program file has significantly shrunk.
Comparison of readelf before and after optimization (partial data):
arm64-v8a Shipping | Before Optimization | After Optimization | Reduction |
---|---|---|---|
.text | 104.364 | 76.56 | 27.2 |
.dynsym | 14.52 | 0.0185 | 14.5 |
.gnu.version | 1.22 | 0.0016 | 1.22 |
.gnu.hash | 4.02 | 0.00047 | 4.02 |
.hash | 4.72 | 0.0063 | 4.71 |
.dynstr | 50.0 | 0.0198 | 49.98 |
.rodata | 11.13 | 6.35 | 4.78 |
.rela.dyn | 24.76 | 22.79 | 1.97 |
.got | 0.89 | 0.21 | 0.68 |
Memory Benefits
Optimizing the size of so also reduces the memory occupied by the loaded so, yielding additional memory benefits.
On Android, you can use dumpsys meminfo
to check the memory usage of all loaded sos in the entire package. This includes all loaded sos, but you can derive the actual memory benefits from the differences before and after optimization.
Before Optimization:
1 | 127|PD2324:/ $ dumpsys meminfo com.xxx.yyy |
After Optimization:
1 | ** MEMINFO in pid 31482 [com.xxx.yyy] ** |
arm64-v8a Shipping | Before Optimization | After Optimization | Reduction |
---|---|---|---|
libUE4.so Size | 246 | 145 | 101 |
meminfo (total so memory) | 178.165 | 140.04 | 38.125 |
Additional Optimization Strategies
Relocation Table Compression
SDK 28
When the MinSDKVersion
of Android is greater than or equal to 28 (Android 9), you can enable RELR
relocation table compression during compile and link time. This efficiently encodes the relocation information using the characteristics of relative address relocation, thereby reducing storage space.
To enable this, you need to pass parameters to the Compiler and Linker during the compilation phase:
1 | AdditionalCompilerArguments += " -fPIC"; |
-Wl,--pack-dyn-relocs=android+relr,--use-android-relr-tags
is an Android-specific linker option that complements and optimizes the standard-Wl,-z,relro
and-Wl,-z,now
, particularly for dynamic linking and relocation handling in the Android system. They aim to further reduce binary file size and improve loading times.
To verify if this takes effect, you can use readelf -d libUE4.so
to check for the existence of the RELR
field:
Before Optimization, the size of the relocation table (25.82M):
1 | 8 .rela.dyn 0189c708 000000000000c720 000000000000c720 0000c720 2**3 |
After Optimization, the size of the relocation table (280K):
1 | 8 .rela.dyn 00013852 000000000000c6d8 000000000000c6d8 0000c6d8 2**3 |
The size of the relocation table after optimization is reduced from 25.82M to 280K, which directly results in a so size reduction of about 25M, also reducing the APK size by around 4M, with extremely noticeable optimization effects.
Moreover, its optimization effect on memory is also very significant: from 190.49M in Development to 161.06M, a reduction of 29.43M.
Before Optimization (Development: 190.49MB):
1 | ** MEMINFO in pid 16293 [com.xxx.yyy] ** |
After Optimization (Development: 161.06MB):
1 | ** MEMINFO in pid 16294 [com.xxx.yyy] ** |
It positively optimizes runtime performance rather than reducing it, as it improves code loading speed and reduces memory usage by decreasing the number of runtime relocations.
SDK 23
If the project requires a specific SDK version and cannot be upgraded to 28, you can use another alternative compression parameter, requiring SDK version >= 23.
1 | AdditionalCompilerArguments += " -fPIC"; |
This also significantly reduces the size of the relocation table (though not as much as RELR down to a few hundred K), and can also greatly lower the memory occupancy of the so:
Compressed after (Development: 3.41M):
1 | [ 8] .rela.dyn LOOS+0x2 000000000000aca0 0000aca0 |
Runtime memory situation (Development: 163.19M), down from the original 190.49M, also reduced by 27.3M, slightly lower than RELR:
1 | ** MEMINFO in pid 11492 [com.tencent.tmgp.fmgame] ** |
Shipping Memory
After enabling relocation table compression, the total runtime memory of the Shipping package’s so dropped to 134.74M
!
1 | ** MEMINFO in pid 13929 [com.xxx.yyy] ** |
Resource Trimming
Files in the APK
Some third-party plugins copy files into the APK, which is also an area for optimization.
You need to analyze the actual usage of the project to handle:
- Remove unnecessary third-party components
- For required components, eliminate unnecessary files
Component Trimming: Taking GVoice as an Example
If the project integrates the GCloud component, model files of GCloudVoice
are extensively copied into the APK file.
After compression, the assets/GCloudVoice
directory in the APK occupies about 13.5M:
- wave_dafx_data.bin is for 3D voice. If no 3D functionality is needed, it can be removed.
- wave_3d_data.bin is also for 3D voice. If no 3D functionality is needed, it can be removed.
- cldnn_spkvector.mnn is used for voiceprint extraction; this function is not used by default and can be removed.
- libwxvoiceembed.bin is for civilization voice; if civilization voice is not needed, it can be removed.
- libgvoicensmodel.bin is the noise suppression algorithm model and cannot be deleted.
- decoder_v4_small.nn and encoder_v4_small.nn are used for aicodec; if aicodec is not needed, they can be removed.
- dse_v1.nn, dse_v1_align.nn, dse_v1_mono.nn are resources for the new algorithm used under wwise; if there is a size limit for packaging, they can also be removed.
You can eliminate model files for functionalities that are not utilized in the project. From an implementation perspective, it is best not to delete files directly but to modify the copy logic in GVoice_APL.xml
like this:
1 | <resourceCopies> |
In-game Resources
In-game resources refer to the resources/files dependent on the UE engine or components, packed into PAK or copied into the main.obb
.
- In the PAK: assets that need to be reviewed for necessity, those that can be eliminated or delayed loaded.
- DirectoriesToAlwaysStageAsNonUFS: not included in PAK, but packed into the main.obb.
Resources in PAK
To be more precise, this refers to the resources in the installation package’s PAK.
All necessary resources for the engine are in pakchunk0
. In addition to pakchunk0
, UE can package chunks split by utilizing PrimaryAssetLabel
into the installation package.
However, resources or files in pakchunk0
still need to be optimized:
- Retain only essential engine resources (key assets in
/Engine
, ini files, GlobalShader, project ShaderLibrary, startup map, GameFramework assets, etc.). My previous article (UE Resource Management: Analysis of Engine Packed Resources) provides more detailed insights. - Remove resources that are not necessary during startup.
- Modify the engine to delay loading certain files (like L10N localization language loading).
- Separate startup phase from in-game resources (like fonts), packaging in-game fonts separately and using dynamic downloads.
The engine’s own unpacking logic also has significant limitations; for example, ShaderLibrary defaults to generating one for the entire project, which can become a bottleneck for optimizing package size when the scale is large. Details on this content can be found in my earlier article (Resource Management: Restructuring UE’s Package Splitting Solutions).
Additionally, during the resource management and packaging phases, it is possible to remove all resources from the installation package for Android, converting them to a dynamic download/mounting mechanism without affecting iOS.
When using methods like PrimaryAssetLabel to split pak, the engine provides a built-in method for Android to remove Pak from the installation package:
1 | ; Config/DefaultEngine.ini |
However, the official support is limited to Android, and it isn’t as convenient for other platforms. In a previous article, I introduced my developed HotChunker extension, which allows easy implementation of a generic package filtering scheme for cross-platform support of custom inclusion control policies.
StageAsNonUFS
In the engine’s packaging configuration, there is an option called DirectoriesToAlwaysStageAsNonUFS
, specifying directories not to be packaged into the PAK but will be included in the main.obb. Currently, only the Content/Movies
directory is copied to main.obb
.
As for the ini read during packaging, it also has a hierarchical logic, allowing for platform differentiation during packaging configuration! If you want to differentiate between Android/iOS, this mechanism can also be utilized.
You can adjust the packaging strategy as follows: unless necessary videos (like those immediately played at startup), other in-game MP4s can be packed separately into PAK, converted to dynamic downloads.
This can significantly reduce the size of MP4s in the APK, allowing for hot updates.
Optimization Results
By comprehensively applying various package size optimization techniques mentioned above, we successfully reduced the game’s APK size from 1.23G to 130M, and the original so size decreased from 258M to 132M.
Runtime memory has also reduced by several tens of MB! Furthermore, it includes all third-party components, game functionalities, and resources can be dynamically downloaded, turning the main installation package into a minimized downloader, making it easier to spread and distribute.
The specific optimization strategies to adopt depend on the actual needs of the project and the balance between package size and performance, such as inline control and optimization level, as well as extreme resource trimming (like L10N) may require modifications to the engine.
Additional Resources
Historical articles mentioned in this blog:
- UE Plugin and Tool Development: Basic Concepts
- Resource Management: Restructuring UE’s Package Splitting Solutions
- UE Resource Management: Analysis of Engine Packed Resources
Additional reading related to UE build systems:
Other external resources:
- Epic’s official documentation on optimizing package size: Reducing Packaged Game Size, which can be used in conjunction with this article.
- There are clear size requirements for publishing on the Google Play platform: Google Play’s App Size Limits.