Posted onInUnrealEngine
,
AndroidViews: Symbols count in article: 18kReading time ≈44 mins.
In a previous article, I introduced the development notes and some engineering practices for Mac/iOS: UE4 Development Notes: Mac/iOS Edition. This article serves as a sister piece, documenting the standardized environment, debugging tools, engineering practices, and analysis of related engine code that I used while developing on Android with UE. It records some pitfalls encountered in the project, primarily organized from my previous notes notes/ue. Future Android-related content will also be updated in this article.
The development environment for Android requires a combination of JDK/NDK/SDK/Ant/Gradle. The UE documentation mentions that NVDIA’s CodeWorks must be installed. However, due to network issues in China, it’s challenging to download, and some components are not necessary, so I packaged an Android development environment for quick deployment.
Component versions:
JDK 18077
NDK r14b
SDK android19-26
Ant 1.8.2
Gradle 4.1
Download link: AndroidSDK_1R7u1_20190923.7z, after unzipping, there is an AddToPath.bat script to add the necessary environment variables with one click.
However, different versions of the UE engine support different Android systems. In the Project Setting - Android, you can set the Minimum SDK Version, which is the lowest system version supported for packaging Android in UE.
In UE4.25, the minimum level can be set to 19, which is Android 4.4. In engine versions prior to 4.25, the minimum supported level is 9, which is Android 2.3.
The minimum support for Android in UE4 is SDK9, which corresponds to Android 2.3.
Engine Requirements for Android NDK
UE requires the NDK environment installed on the system while packaging Android, but different engine versions have different requirements for the NDK version.
Using an unsupported NDK version will result in the following error:
1 2
UATHelper: Packaging (Android (ETC2)): ERROR: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended) PackagingResults: Error: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended)
It indicates that the current NDK version in the system is not supported and displays the supported versions.
The NDK version detection during UE packaging occurs within UBT, specifically in the file UnrealBuildTool/Platform/Android/AndroidToolChain.cs.
It defines the minimum and maximum supported NDK versions for the current engine version:
UE4 supports Android NDK versions r14b-r18b, but in UE4.22.3, setting r18b was recognized by the engine as r18c:
1 2 3 4 5 6 7 8 9 10
UATHelper: Packaging (Android (ETC2)): Using 'git status' to determine working set for adaptive non-unity build (C:\Users\imzlp\Documents\Unreal Projects\GWorldClient). UATHelper: Packaging (Android (ETC2)): ERROR: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended) PackagingResults: Error: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended) UATHelper: Packaging (Android (ETC2)): Took 7.4575476s to run UnrealBuildTool.exe, ExitCode=5 UATHelper: Packaging (Android (ETC2)): ERROR: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt) UATHelper: Packaging (Android (ETC2)): (see C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\Log.txt for full exception trace) PackagingResults: Error: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt) UATHelper: Packaging (Android (ETC2)): AutomationTool exiting with ExitCode=5 (5) UATHelper: Packaging (Android (ETC2)): BUILD FAILED PackagingResults: Error: Unknown Error
The reason for needing to change the NDK version is that the compilers included in different NDK versions have varying levels of support for the C++11 standard.
NDK
clang version
r14b
clang 3.8.275480 (based on LLVM 3.8.275480)
r17c
clang version 6.0.2
r18b
clang version 7.0.2
r20b
clang version 8.0.7
Tools
ADB
ADB is a powerful debugging tool for Android, and familiarizing yourself with some ADB commands can greatly enhance efficiency. First, download ADB.
Install APK
1
$ adb install APK_FILE_NAME.apk
Launch App
The installed renderdoccmd does not have a desktop icon. To start it yourself, you can only use the following ADB command:
1
adb shell am start org.renderdoc.renderdoccmd.arm64/.Loader -e renderdoccmd "remoteserver"
This method requires knowing the app’s package name and activity name. The package name can be easily found, but if you don’t know the activity, you can obtain it through the following procedure:
First, use a decompiling tool to unpack the APK (you can use the earlier mentioned apktools):
1
apktool.bat d -o ./renderdoccmd_arm64 org.renderdoc.renderdoccmd.arm64.apk
Then, open the AndroidManifest.xml file in the org.renderdoc.renderdoccmd.arm64 directory and find the Application entry:
This contains all registered Activity entries. For an APK without a UI, there is typically only one activity; hence the main activity for the renderdoccmd is .Loader.
If it’s a UI app, there may be multiple activities. You can check the Category in AndroidManifest.xml or identify the main activity by looking for the one with main in its name. Typically, it starts with the launcher, leads to the main activity, or some may navigate to a login interface.
PS: The main activity for a game packaged with UE is com.epicgames.ue4.SplashActivity, which can be launched using the following command.
1
adb shell am start com.imzlp.GWorld/com.epicgames.ue4.SplashActivity
Transfer Files
To transfer files from your computer to the mobile device using ADB:
Using logcat allows you to see log information from the Android device.
1
$ adb logcat
This will print out all information from the current device, but during app debugging, we don’t need to see so much. We can use find to filter results (case sensitive):
If there are too many accumulated logs from frequent runs, you can clear them:
1
adb logcat -c
Extract Installed APKs from the Device
Note: When executing the following commands, ensure that developer permissions are enabled on your phone. You must allow the verification fingerprint prompts.
# Check connected devices $ adb devices List of devices attached b2fcxxxx unauthorized # List all installed apps on the phone $ adb shell pm list package # If prompted with the error below, execute adb kill-server error: device unauthorized. This adb servers $ADB_VENDOR_KEYS is not set Try 'adb kill-server'if that seems wrong. Otherwise check for a confirmation dialog on your device. # Normally, it should list many entries like this C:\Users\imzlp>adb shell pm list package package:com.miui.screenrecorder package:com.amazon.mShop.android.shopping package:com.mobisystems.office package:com.weico.international package:com.github.shadowsocks package:com.android.cts.priv.ctsshim package:com.sorcerer.sorcery.iconpack package:com.google.android.youtube
# Find the APK location for the specified app $ adb shell pm path com.github.shadowsocks package:/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk # Then pull that file to your local machine $ adb pull /data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk /data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/...se.apk: 1 file pulled. 21.5 MB/s (4843324 bytes in 0.215s)
Flash Recovery
Download ADB, and use the following commands based on your circumstances (if you’re already in bootloader, you don’t need to execute the first command).
You can use the command objdump -tT libUE4.so to output the contents of the SO symbol table.
SessionFrontEnd Real-Time Analysis
There are several prerequisites:
USB connection between PC and phone
ADB installed
First, port mapping is needed because SessionFrontEnd communicates with the game through listening ports, and the phone and PC are not in the same subnet. Therefore, you need to forward the PC’s listening port to the phone’s port through ADB.
The listening port for SessionFrontEnd can be obtained via port analysis on UE4Editor.exe:
You need to map port 1985 on the PC to port 1985 on Android, so when the mobile app connects to 0.0.0.0 on port 1985, it connects to the port on the PC.
Use the following ADB command:
1
adb reverse tcp:1985 tcp:1985
Then specify the launch parameters for the app on the phone:
1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device"
Save these texts as a UE4Commandline.txt file in the project data directory, specifically at:
1
/sdcard/UE4Game/PROJECT_NAME/
Afterward, directly start the app, and you will see the device’s data in SessionFrontEnd on the PC.
The previous section introduced using SessionFrontEnd for real-time analysis on Android. During actual testing, it was found to be somewhat unstable, potentially causing crashes in the game. In newer versions of the engine, UE also provides a new performance analysis tool called Unreal Insights, which allows for more convenient and intuitive profiling. Documentation:
After launching the game, listen to 127.0.0.1 in Unreal Insights.
Engineering Practices
Link libraries that support both armv7 and arm64
When developing for Android, there is a requirement to support both arm64 and armv7, and both libraries need to be added to PublicAdditionalLibraries in the Build.cs:
However, there isn’t a method in UE’s ModuleRules to determine the architecture currently being compiled (the ModuleRules constructor only runs once during compilation), leading to linker errors when compiling for arm64, as it finds the armv7 libraries:
14>ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkAudioLib.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkLEngine.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkAudioLib.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkLEngine.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgr.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgrBase.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgr.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgrBase.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkStreamMgr.a(AkStreamMgr.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkStreamMgr.a(AkStreamMgr.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMusicEngine.a(AkMusicRenderer.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMusicEngine.a(AkMusicRenderer.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSpatialAudio.a(AkSpatialAudio.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSpatialAudio.a(AkSpatialAudio.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkAudioInputSource.a(AkFXSrcAudioInput.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkAudioInputSource.a(AkFXSrcAudioInput.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkVorbisDecoder.a(AkVorbisLib.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkVorbisDecoder.a(AkVorbisLib.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMeterFX.a(InitAkMeterFX.o) is incompatible with aarch64linux ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMeterFX.a(InitAkMeterFX.o) is incompatible with aarch64linux ld.lld: error: too many errors emitted, stopping now (use -error-limit=0 to see all errors) clang++: error: linker command failed with exit code 1 (use -v to see invocation) 14>Execution failed. Error: 1 (0x01) Target: 'C:\BuildAgent\workspace\FGameClientBuild\Client\Binaries\Android\FGame-arm64.so'
Thus, a method is needed to allow the compiler to automatically match the correct path for linking when using PublicAdditionalLibraries with both the arm64 and armv7 libraries.
Looking through the UBT code, it’s discovered that UE performs pattern matching on the paths of link libraries for Android:
staticboolIsDirectoryForArch(string Dir, string Arch) { // make sure paths use one particular slash Dir = Dir.Replace("\\", "/").ToLowerInvariant();
// look for other architectures in the Dir path, and fail if it finds it foreach (KeyValuePair<string, string[]> Pair in AllArchNames) { if (Pair.Key != Arch) { foreach (string ArchName in Pair.Value) { // if there's a directory in the path with a bad architecture name, reject it if (Regex.IsMatch(Dir, "/" + ArchName + "$") || Regex.IsMatch(Dir, "/" + ArchName + "/") || Regex.IsMatch(Dir, "/" + ArchName + "_API[0-9]+_NDK[0-9]+", RegexOptions.IgnoreCase)) { returnfalse; } } } }
// if nothing was found, we are okay returntrue; }
publicoverride FileItem[] LinkAllFiles(LinkEnvironment LinkEnvironment, bool bBuildImportLibraryOnly, IActionGraphBuilder Graph) { // ... // Add the library paths to the additional path list foreach (DirectoryReference LibraryPath in LinkEnvironment.LibraryPaths) { // LinkerPaths could be relative or absolute string AbsoluteLibraryPath = Utils.ExpandVariables(LibraryPath.FullName); if (IsDirectoryForArch(AbsoluteLibraryPath, Arch)) { // environment variables aren't expanded when using the $( style if (Path.IsPathRooted(AbsoluteLibraryPath) == false) { AbsoluteLibraryPath = Path.Combine(LinkerPath.FullName, AbsoluteLibraryPath); } AbsoluteLibraryPath = Utils.CollapseRelativeDirectories(AbsoluteLibraryPath); if (!AdditionalLibraryPaths.Contains(AbsoluteLibraryPath)) { AdditionalLibraryPaths.Add(AbsoluteLibraryPath); } } }
Based on the regex rule above, you can change the paths of the linked libraries to:
1 2
XXXX/armeabi-v7a/ XXXX/armeabi-v8a/
As long as the paths for the libraries added for Android match this rule, UE will automatically use the corresponding architecture’s libraries (.a and .so can both use this rule).
UE is quite tricky here; this requirement isn’t documented, making it a completely unspoken rule.
Android Runtime Permission Requests
ActivityCompact has a requestPermission method that can handle such situations.
To retrieve the APK path at runtime, since UE does not have existing interfaces, JNI must be used to get it from Java. By checking the Android Developer documentation, it is found that ApplicationInfo contains the sourceDir property, which records the APK path. This can be fetched using the PackageManager call to getApplicationInfo for a specific package name.
Using HTTP requests to upload data on Android P will show the following error message:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo W/CrashReport: java.io.IOException: Cleartext HTTP traffic to android.bugly.qq.com not permitted at com.android.okhttp.HttpHandler$CleartextURLFilter.checkURLPermitted(HttpHandler.java:115) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:458) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:127) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:258) at com.tencent.bugly.proguard.ai.a(BUGLY:265) at com.tencent.bugly.proguard.ai.a(BUGLY:114) at com.tencent.bugly.proguard.al.run(BUGLY:355) at com.tencent.bugly.proguard.ak$1.run(BUGLY:723) at java.lang.Thread.run(Thread.java:764) 2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo E/CrashReport: Failed to upload, please check your network. 2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo D/CrashReport: Failed to execute post. 2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo E/CrashReport: [Upload] Failed to upload(1): Failed to upload for no response! 2018-10-10 16:39:21.313 31611-31646/com.xfhy.tinkerfirmdemo E/CrashReport: [Upload] Failed to upload(1) userinfo: failed after many attempts
This function internally creates an FArchive object to manage the current file, which has an IFileHandle object called Handle. On the Android platform, it’s FFileHandleAndroid.
Writing to files in FArchive calls Serialize, which in turn calls Handle‘s Write:
// read in the command line text file from the sdcard if it exists FString CommandLineFilePath = GFilePathBase + FString("/UE4Game/") + (!FApp::IsProjectNameEmpty() ? FApp::GetProjectName() : FPlatformProcess::ExecutableName()) + FString("/UE4CommandLine.txt"); FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r"); if(CommandLineFile == NULL) { // if that failed, try the lowercase version CommandLineFilePath = CommandLineFilePath.Replace(TEXT("UE4CommandLine.txt"), TEXT("ue4commandline.txt")); CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r"); }
In simple terms, this means writing startup parameters into UE4Game/ProjectName/ue4commandline.txt, and the engine will read from this file during startup and add it to FCommandLine.
Android Set Aspect Ratio
The value under Project Settings - Platforms - Android - Maximum support aspect ratio defaults to 2.1, but in full screen situations, there may be black edges.
This value controls the value in AndroidManifest.xml:
Note: Enable FullScreen Immersive on KitKat and above devices controls whether to hide the virtual keys when entering the game.
UPL for Android
In UE, when adding third-party modules or modifying configuration files for mobile, AdditionalPropertiesForReceipt is often used, where ReceiptProperty is created with the xml file being the Unreal Plugin Language (UPL) script.
The platform names for ReceiptProperty are fixed for iOS and Android, namely IOSPlugin and AndroidPlugin, and cannot specify other names (see code UEDeployIOS.cs#L1153 and UEDeployAndroid.cs#L4303).
This traverses through AndroidManifest.xml to remove the meta-data where android:name equals com.epicgames.ue4.GameActivity.bUseExternalFilesDir.
JNI Call to Receive ActivityResult
Sometimes you need to create an Intent using startActivityForResult to perform operations such as opening the camera or selecting an image from the album.
However, Android does not block the current function during these operations, so data cannot be directly received in the calling function. The result from actions executed via startActivityForResult will be called in the Activity’s onActivityResult.
1 2 3
// GameActivity.java @Override protectedvoidonActivityResult(int requestCode, int resultCode, Intent data){}
UE provides a way to append Java code to OnActivityResult in UPL:
1 2
<!-- optional additions to GameActivity onActivityResult in GameActivity.java --> <gameActivityOnActivityResultAdditions></gameActivityOnActivityResultAdditions>
Using this approach, added Java code will be appended to the end of the OnActivityResult function. However, there is a problem: after executing the custom appended code, the received result still needs to be handled and passed back to UE, which can be cumbersome.
After reviewing the code, I found that UE offers a multicast delegate event for the Java side’s OnActivityResult, allowing you to listen for OnActivityResult events in UE using C++ to handle it.
// The delegate is defined in `FJavaWrapper` // Delegate that can be registered to that is called when an activity is finished static FOnActivityResult OnActivityResultDelegate;
In UE, you can bind this multicast delegate to listen for calls to Java’s OnActivityResult, allowing you to handle them accordingly.
It is called from the Java side in AndroidJNI.cpp through the Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult function, and the calling mechanism is documented in the previous notes.
Java Calls C++
Some needs and implementations require calling from Java to C++. You can do so as follows: First, declare a native Java function in GameActivity (no need to define it):
1
publicnativevoidnativeOnActivityResult(GameActivity activity, int requestCode, int resultCode, Intent data);
Then, define a corresponding C++ function as follows:
You can see the rules for the function name are as follows:
The function must be prefixed with JNI_METHOD, which is a macro __attribute__ ((visibility ("default"))) extern "C".
The function name must start with Java_ followed by com_epicgames_ue4_GameActivity_, indicating it is defined in GameActivity.
The following part must match the Java function name.
The rules for accepting parameters are:
The first parameter is the Java Env.
The second parameter is the Java this.
The subsequent parameters correspond to the parameters passed from Java.
Full-Screen Adaptation for Android P
When packaging in UE4, the project will generate a GameActivity.java file, which contains the OnCreate code for full-screen adaptation:
1 2 3 4 5 6 7 8 9 10 11 12 13
@Override publicvoidonCreate(Bundle savedInstanceState) { // ... if (UseDisplayCutout) { // will not be true if not Android Pie or later WindowManager.LayoutParamsparams= getWindow().getAttributes(); params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().setAttributes(params); } // ... }
Android P and later systems do not support this, so you will need to write JNI calls to forcibly enable compatibility for P and later system versions. In UE4.23 and later engine versions, you can use UPL to add code to the OnCreate function, allowing you to directly insert the code into GameActivity.java:
1 2 3 4 5 6 7 8 9 10
<gameActivityOnCreateFinalAdditions> <insert> // Allow the use of cutouts on P versions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParamslp=this.getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; this.getWindow().setAttributes(lp); } </insert> </gameActivityOnCreateFinalAdditions>
In UE4.23 and prior versions, you cannot add code to OnCreate; instead, you must write a JNI call:
The Enable Gradle instead of Ant option is no longer present in 4.24+, and the logs indicate that UE failed to download Grade from the internet. You can enable global proxy during packaging. Alternatively, you can use my packaged Gradle version: gradle-5.4.1.7z and extract it to the following path:
If you encounter the following error log during packaging:
1 2 3
execution failed for task ':app:packagedebug'. > a failure occurred while executing com.android.build.gradle.internal.tasks.workers$actionfacade > out of range: 3185947123
Solution: Delete Intermediate/Android and Intermediate/Build/Android from your project.