UE开发笔记:Android篇

在之前的一篇文章中,介绍了Mac/IOS的开发笔记和一些工程实践:UE4 开发笔记:Mac/iOS 篇。本篇文章作为姊妹篇,记录我在使用UE在开发Android时所用的标准化环境、调试工具、工程实践以及分析相关的引擎代码等内容,记录了一些在项目中遇到的坑,主要从我之前的笔记notes/ue中整理而来,后续Android相关的内容也都会更新到这篇文章里。

博客文章

博客中与Android有关的文章:

编译环境

Android的开发环境需要JDK/NDK/SDK/Ant/Gradle等组合而成,UE的文档中介绍需要安装NVDIA的CodeWorks,但是由于国内网络问题难以下载,并且有些组件并不需要,所以我打包了一份Android的开发环境,可以快速部署。

组件版本:

  • JDK 18077
  • NDK r14b
  • SDK android19-26
  • Ant 1.8.2
  • Gradle 4.1

下载地址:AndroidSDK_1R7u1_20190923.7z,解压之后有AddToPath.bat脚本,一键添加所需的环境变量。

1
2
3
4
5
6
7
8
9
10
11
12
@echo off
set "current_dir_name=%cd%"
setx JAVA_HOME "%current_dir_name%\jdk18077"
setx ANDROID_HOME "%current_dir_name%\android-sdk-windows"
setx ANDROID_NDK_ROOT "%current_dir_name%\android-ndk-r14b"
setx ANT_HOME "%current_dir_name%\apache-ant-1.8.2"
setx GRADLE_HOME "%current_dir_name%\gradle-4.1"
setx NDK_ROOT "%current_dir_name%\android-ndk-r14b"
setx NDKROOT "%current_dir_name%\android-ndk-r14b"
setx NVPACK_NDK_TOOL_VERSION "4.9"
setx NVPACK_NDK_VERSION "android-ndk-r14b"
setx NVPACK_ROOT "%current_dir_name%"

如果需要更新版本的NDK和SDK支持的版本有些老,可以自行在下载所需的版本:

下载之后放到对应的目录下即可,并且需要修改环境变量中的值。

添加完环境变量之后之后无需再从UE中设置SDK与NDK的路径,保持默认即可打包。

引擎依赖与支持

引擎对Android版本的支持

在之前的笔记里:Android SDK版本与Android的版本列出了Android系统版本和API Leve版本之间的对照表。

但是UE不同的引擎版本对Android的系统支持也是不一样的,在Project Setting-Android中的Minimum SDK Version中可以设置最小的SDK版本,也就是UE打包Android所支持的最低系统版本。

在UE4.25中,最低可以设置Level为19,即Android4.4,在4.25之前的引擎版本最低支持Level 9,也就是Android 2.3。

这部分的代码可以在Runtime/Android/AndroidRuntimeSettings/Classes/AndroidRuntimeSettings.h中查看,并对比不同引擎版本的区别。

SDK版本与Android的版本对照表

可以在Google的开发者站点看到:Android SDK Platform
Build.VERSION_CODES的含义:Build.VERSION_CODES

AndroidVersion SDK Version Build.VERSION_CODES
Android 11 (API level 30) R
Android 10 (API level 29) Q
Android 9 (API level 28) P
Android 8.1 (API level27) O_MR1
Android 8.0 (API level 26) O
Android 7.1 (API level 25) N_MR1
Android 7.0 (API level 24) N
Android 6.0 (API level 23) M
Android 5.1 (API level 22) LOLLIPOP_MR1
Android 5.0 (API level 21) LOLLIPOP
Android 4.4W (API level 20) KITKAT_WATCH
Android 4.4 (API level 19) KITKAT
Android 4.3 (API level 18) JELLY_BEAN_MR2
Android 4.2 (API level 17) JELLY_BEAN_MR1
Android 4.1 (API level 16) JELLY_BEAN
Android 4.0.3 (API level15) ICE_CREAM_SANDWICH_MR1
Android 4.0 (API level 14) ICE_CREAM_SANDWICH
Android 3.2 (API level 13) HONEYCOMB_MR2
Android 3.1 (API level 12) HONEYCOMB_MR1
Android 3.0 (API level 11) HONEYCOMB
Android 2.3.3 (API level 10) GINGERBREAD_MR1
Android 2.3 (API level 9) GINGERBREAD

UE4对Android的最低支持是SDK9,也就是Android2.3。

引擎对AndroidNDK的要求

UE在打包Android的时候会要求系统中具有NDK环境,但是不同的引擎版本对NDK的版本要求也不一样。

当使用不支持的NDK版本时,打包会有如下错误:

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)

提示当前系统中的NDK版本不支持,并会显示支持的版本。

UE打包时对NDK版本的检测是在UBT中执行的,具体文件为UnrealBuildTool/Platform/Android/AndroidToolChain.cs

其中定义了当前引擎版本支持的NDK的最低和最高版本:

1
2
3
4
// in ue 4.25
readonly int MinimumNDKToolchain = 210100;
readonly int MaximumNDKToolchain = 230100;
readonly int RecommendedNDKToolchain = 210200;

可以在Github上比较方便地查看不同引擎版本要求的NDK版本:UE_425_AndroidToolChain.cs

不同的引擎版本对NDK的要求UE文档中也有介绍:Setting Up Android SDK and NDK for Unreal

Unreal Engine NDK Version
4.25+ NDK r21b, NDK r20b
4.21 - 4.24 NDK r14b
4.19 - 4.20 NDK r12b

NDK的编译器版本

UE4支持r14b-r18b的Android NDK,但是我在UE4.22.3中设置r18b被引擎识别为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

之所以要换NDK的版本是因为不同的NDK版本所包含的编译器对C++11标准支持度不同。

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

工具

Adb

adb是Android的调试工具,非常强大,熟悉一些adb的命令能够让效率加倍。首先先要下载Adb

安装Apk

1
$ adb install APK_FILE_NAME.apk

启动App

安装的renderdoccmd是没有桌面图标的,想要自己启动的话只能使用下列adb命令:

1
adb shell am start org.renderdoc.renderdoccmd.arm64/.Loader -e renderdoccmd "remoteserver"

adb启动App的shell命令模板:

1
adb shell am start PACKAGE_NAME/.ActivityName

这个方法需要知道App的包名和Activity名,包名很容易知道,但是Activity如果不知道可以通过下列操作获取:

首先使用一个反编译工具将apk解包(可以使用之前的apktools):

1
apktool.bat d -o ./renderdoccmd_arm64 org.renderdoc.renderdoccmd.arm64.apk

然后打开org.renderdoc.renderdoccmd.arm64目录下的AndroidManifest.xml文件,找到其中的Application项:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.renderdoc.renderdoccmd.arm64" platformBuildVersionCode="26" platformBuildVersionName="8.0.0">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<application android:debuggable="true" android:hasCode="true" android:icon="@drawable/icon" android:label="RenderDocCmd">
<activity android:configChanges="keyboardHidden|orientation" android:exported="true" android:label="RenderDoc" android:name=".Loader" android:screenOrientation="landscape">
<meta-data android:name="android.app.lib_name" android:value="renderdoccmd"/>
</activity>
</application>
</manifest>

其中有所有注册的Activity,没有有界面的apk只有一个Activity,所以上面的renderdoccmd的主Activity就是.Loader

如果说有界面的app,则会有多个,则可以从AndroidManifest.xml查找Category或者根据命名(名字带main的Activity)来判断哪个是主Activity。一般都是从lanucher开始,到main,或者有的进登陆界面。

PS:使用UE打包出游戏的主Activity是com.epicgames.ue4.SplashActivity,可以通过下列命令启动。

1
adb shell am start com.imzlp.GWorld/com.epicgames.ue4.SplashActivity

传输文件

使用adb往手机传文件:

1
2
# adb push 1.0_Android_ETC2_P.pak /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks
$ adb push FILE_NAME REMOATE_PATH

从手机传递到电脑:

1
2
# adb pull /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks/1.0_Android_ETC2_P.pak A.Pak
$ adb pull REMOATE_FILE_PATH LOCAL_PATH

Logcat

使用logcast可以看到Android的设备Log信息。

1
$ adb logcat

会打印出当前设备的所有信息,但是我们调试App时不需要看到这么多,可以使用find进行筛选(注意大小写严格区分):

1
2
# adb logcat | find "GWorld"
$ adb logcat | find "KEY_WORD"

查看UE打包的APP所有的log可以筛选:

1
$ adb logcat | find "UE4"

如果运行的次数过多积累了大量的Log,可以使用清理:

1
adb logcat -c

从设备中提取已安装的APK

注意:执行下列命令时需要检查手机是否开放开发者权限,手机上提示的验证指纹信息要允许。

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
# 查看链接设备
$ adb devices
List of devices attached
b2fcxxxx unauthorized
# 列出手机中安装的所有app
$ adb shell pm list package
# 如果提示下问题,则需要执行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.
# 正常情况下会列出一堆这样的列表
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

# 找到指定app的的apk位置
$ adb shell pm path com.github.shadowsocks
package:/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
# 然后将该文件拉取到本地来即可
$ 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)

刷入Recovery

下载Adb,然后根据具体情况使用下列命令(如果当前已经在bootloader就不需要执行第一条了)。

1
2
3
4
5
6
adb reboot bootloader
# 写入img到设备
fastboot flash recovery recovery.img
fastboot flash boot boot.img
# 引导img
fastboot boot recovery.img

端口转发

可以通过adb命令来指定:

1
2
3
4
# PC to Device
adb reverse tcp:1985 tcp:1985
# Device to PC
adb forward tcp:1985 tcp:1985

根据包名查看apk位置

可以使用以下adb命令:

1
2
$ adb shell pm list package -f com.tencent.tmgp.fm
package:/data/app/com.tencent.tmgp.fm-a_cOsX8G3VClXwiI-RD9wQ==/base.apk=com.tencent.tmgp.fm

最后一个参数是包名,输出的则是apk的路径。

查看当前窗口的app的包名

使用以下adb命令:

1
2
3
4
5
6
7
8
$ adb shell dumpsys window w | findstr \/ | findstr name=
mSurface=Surface(name=SideSlideGestureBar-Bottom)/@0xa618588
mSurface=Surface(name=SideSlideGestureBar-Right)/@0x619b646
mSurface=Surface(name=SideSlideGestureBar-Left)/@0xea02007
mSurface=Surface(name=StatusBar)/@0x7e4962d
mAnimationIsEntrance=true mSurface=Surface(name=com.tencent.tmgp.fm/com.epicgames.ue4.GameActivity)/@0x43b30a0
mSurface=Surface(name=com.tencent.tmgp.fm/com.epicgames.ue4.GameActivity)/@0xa3481e
mAnimationIsEntrance=true mSurface=Surface(name=com.vivo.livewallpaper.monster.bmw.MonsterWallpaperService)/@0x53e44ae

其中的mAnimationIsEntrance=true mSurface=Surface(name=之后,到/之前的字符串就是我们的app包名。

SessionFrontEnd实时分析

有几个条件:

  1. 需要USB连接PC和手机
  2. 需要安装adb

首先需要映射端口,因为SessionFrontEnd是通过监听端口的方式来与游戏内通信的,手机和PC并不在同一个网段,所以需要以adb的形式把PC的监听端口转发给手机的端口。

SessionFrontEnd的监听端口可以通过对UE4Editor.exe的端口分析获取:

1
2
3
4
5
6
7
8
9
C:\Users\lipengzha>netstat -ano | findstr "231096"  
TCP 0.0.0.0:1985 0.0.0.0:0 LISTENING 231096
TCP 0.0.0.0:3961 0.0.0.0:0 LISTENING 231096
TCP 0.0.0.0:3963 0.0.0.0:0 LISTENING 231096
TCP 127.0.0.1:4014 127.0.0.1:12639 ESTABLISHED 231096
TCP 127.0.0.1:4199 127.0.0.1:12639 ESTABLISHED 231096
UDP 0.0.0.0:6666 *:* 231096
UDP 0.0.0.0:24024 *:* 231096
UDP 0.0.0.0:58101 *:* 231096

需要把PC的1985端口映射到Android的1985端口,这样手机上APP启动时,连接0.0.0.01985端口就可以连接到PC上的端口。

通过adb命令来执行:

1
adb reverse tcp:1985 tcp:1985 

然后需要给手机上App指定启动参数:

1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device"  

把这些文本保存为UE4Commandline.txt文件,放到项目的数据目录下即可,具体路径为:

1
/sdcard/UE4Game/PROJECT_NAME/ 

之后直接启动App,在PC上的SessionFrontEnd中就可以看到设备的数据了。

Unreal Insights实时分析

上一节提到了使用SessionFrontEnd实时分析Android的方法,在实际的测试当中发现不太稳定,会造成游戏的Crash,UE在新的引擎版本中也提供了新的性能分析工具Unreal Insights,可以更方便和直观地进行Profile。
文档:

同样也需要端口映射,需要把PC的1980端口映射到设备上:

1
adb reverse tcp:1980 tcp:1980 

然后需要给Android设备添加启动命令:

1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device" -iterative -tracehost=127.0.0.1 -Trace=CPU 

在PC上开启Unreal Insights,在手机上启动游戏,即可实时捕获:

Unreal Insights也可以实时捕获PIE的数据,需要在Editor启动时添加-trace参数:

1
UE4Editor.exe PROJECT_NAME.uproject -trace=counters,cpu,frame,bookmark,gpu

在启动游戏后在Unreal Insights里通过New Connection监听127.0.0.1即可。

工程实践

同时支持armv7和arm64的链接库

在开发Android的时候,有需求需要同时支持arm64和armv7,需要在Build.cs中同时把armv7和arm64的链接库都添加到PublicAdditionalLibraries中:

1
2
3
4
5
PublicAdditionalLibraries.AddRange(new string[]
{
Path.Combine(ThirdPartyFolder, "Android_armeabi-v7a", AkConfigurationDir),
Path.Combine(ThirdPartyFolder, "Android_arm64-v8a", AkConfigurationDir),
});

但是,在UE的ModuleRules里没有能判断当前编译的架构的方法(ModuleRules的构造在编译时只会执行一次),导致编译arm64的时候找到了armv7的链接库,导致链接错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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'

所以,需要找到一种方法,能够在使用PublicAdditionalLibraries同时添加了arm64和armv7链接库的情况下让编译器能够自动地匹配到应该去什么路径来执行链接。

翻了一下UBT的代码,发现UE中对在Android上对链接库的路径做了模式匹配:

Engine\Source\Programs\UnrealBuildTool\Platform\Android\AndroidToolChain.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
static private Dictionary<string, string[]> AllArchNames = new Dictionary<string, string[]> {
{ "-armv7", new string[] { "armv7", "armeabi-v7a", } },
{ "-arm64", new string[] { "arm64", "arm64-v8a", } },
{ "-x86", new string[] { "x86", } },
{ "-x64", new string[] { "x64", "x86_64", } },
};

static bool IsDirectoryForArch(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))
{
return false;
}
}
}
}

// if nothing was found, we are okay
return true;
}

public override 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);
}
}
}

// ...
}

最关键的就是这这一行:

1
if (Regex.IsMatch(Dir, "/" + ArchName + "$") || Regex.IsMatch(Dir, "/" + ArchName + "/") || Regex.IsMatch(Dir, "/" + ArchName + "_API[0-9]+_NDK[0-9]+", RegexOptions.IgnoreCase))

根据上面正则的规则,可以把链接库的路径改为:

1
2
XXXX/armeabi-v7a/
XXXX/armeabi-v8a/

只要我们为Android添加的链接库路径匹配这个规则,使用UE编译时就会自动使用对应架构的链接库(.a和.so都是可以使用这个规则的)。

UE这也太坑了,这个要求文档没写,完全是一个潜规则。

Android运行时请求权限

ActivityCompact 里面有个 requestPermission方法,可以用来处理这种情况。

Android获取包名

通过UPL在Java里添加以下代码:

1
2
3
4
5
public String AndroidThunkJava_GetPackageName()
{
Context context = getApplicationContext();
return context.getPackageName();
}

在C++里通过JNI调用即可:

1
2
3
4
5
6
7
8
9
10
11
12
FString UFlibAppHelper::GetAppPackageName()
{
FString result;
#if PLATFORM_ANDROID
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetPackageName", "()Ljava/lang/String;", false);
result = FJavaHelper::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetInstalledPakPathMethodID));
}
#endif
return result;
}

Android获取外部存储路径

获取App的沙盒路径:

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
// /storage/emulated/0/Android/data/com.xxxx.yyyy.zzzz/files
FString FAndroidGCloudPlatformMisc::getExternalStorageDirectory()
{
FString result;
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
// get context
jobject JniEnvContext;
{
jclass activityThreadClass = Env->FindClass("android/app/ActivityThread");
jmethodID currentActivityThread = FJavaWrapper::FindStaticMethod(Env, activityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;", false);
jobject at = Env->CallStaticObjectMethod(activityThreadClass, currentActivityThread);
jmethodID getApplication = FJavaWrapper::FindMethod(Env, activityThreadClass, "getApplication", "()Landroid/app/Application;", false);

JniEnvContext = FJavaWrapper::CallObjectMethod(Env, at, getApplication);
}
jmethodID getExternalFilesDir = Env->GetMethodID(Env->GetObjectClass(JniEnvContext), "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;");
// get File
jobject ExternalFileDir = Env->CallObjectMethod(JniEnvContext, getExternalFilesDir,nullptr);
// getPath method in File class
jmethodID getFilePath = Env->GetMethodID(Env->FindClass("java/io/File"), "getPath", "()Ljava/lang/String;");
jstring pathString = (jstring)Env->CallObjectMethod(ExternalFileDir, getFilePath, nullptr);
const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
result = ANSI_TO_TCHAR(nativePathString);
}
return result;
}

Android获取已安装App的Apk路径

有个需求,需要在运行时获取到,App的Apk路径,查了一下UE里没有现成的接口,只能用JNI调用从Java那边想办法了。
通过在Android Developer上查找,发现ApplicationInfo中具有sourceDir属性,记录着APK的路径。
而可以通过PackageManager调用getApplicationInfo可以获取指定包名的ApplicationInfo。

那么就好说了,UPL里java代码走起:

1
2
3
4
5
6
7
8
9
10
11
12
public String AndroidThunkJava_GetInstalledApkPath()
{
Context context = getApplicationContext();
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo;
try{
appInfo = packageManager.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA);
return appInfo.sourceDir;
}catch (PackageManager.NameNotFoundException e){
return "invalid";
}
}

然后在UE里使用JNI调用:

1
2
3
4
5
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetInstalledApkPath", "()Ljava/lang/String;", false);
FString ResultApkPath = FJavaHelperEx::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis,GetInstalledPakPathMethodID));
}

其中FJavaHelperEx::FStringFromLocalRef是我封装的从jstring到FString的转换函数:

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
namespace FJavaHelperEx
{
FString FStringFromParam(JNIEnv* Env, jstring JavaString)
{
if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL))
{
return {};
}

const auto chars = Env->GetStringUTFChars(JavaString, 0);
FString ReturnString(UTF8_TO_TCHAR(chars));
Env->ReleaseStringUTFChars(JavaString, chars);
return ReturnString;
}

FString FStringFromLocalRef(JNIEnv* Env, jstring JavaString)
{
FString ReturnString = FStringFromParam(Env, JavaString);

if (Env && JavaString)
{
Env->DeleteLocalRef(JavaString);
}

return ReturnString;

}
}

获取的结果:

升级至AndroidX资料

看操作方式也是使用UPL来介入打包过程,先记录下。

为APK添加外部存储读写权限

Project Settings-Platform-Android-Advanced APK Packaging-Extra Permissions下添加:

1
2
android.permission.WRITE EXTERNAL STORAGE
android.permission.READ_EXTERNAL_STORAGE

AndroidP HTTP请求错误

在Android P上使用HTTP请求上传数据会有以下错误提示:

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

这需要我们在打包时把指定的域名给配置成白名单。

方法如下:

res/xml下创建network_security_config.xml文件

填入以下内容(网址自行修改):

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">android.bugly.qq.com</domain>
</domain-config>
</network-security-config>

然后在AndroifManifest.xml中引用该文件:

1
<application android:networkSecurityConfig="@xml/network_security_config"/>

重新打包即可,在运行时会有以下Log:

1
02-25 21:09:15.831 27760 27791 D NetworkSecurityConfig: Using Network Security Config from resource network_security_config debugBuild: true

Android写入文件

当调用FFileHelper::SaveArrayToFile时:

1
FFileHelper::SaveArrayToFile(TArrayView<const uint8>(data, delta), *path, &IFileManager::Get(), EFileWrite::FILEWRITE_Append));

在该函数内部会创建一个FArchive的对象来管理当前文件,其内部具有一个IFileHandle的对象Handle,在Android平台上是FFileHandleAndroid

FArchive中写入文件调用的是Serialize,它又会调用HandleWrite

1
2
3
4
bool FArchiveFileWriterGeneric::WriteLowLevel( const uint8* Src, int64 CountToWrite )
{
return Handle->Write( Src, CountToWrite );
}

Android的Write的实现为:

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
// Runtime/Core/Private/Android/AndroidFile.h
virtual bool Write(const uint8* Source, int64 BytesToWrite) override
{
CheckValid();
if (nullptr != File->Asset)
{
// Can't write to assets.
return false;
}

bool bSuccess = true;
while (BytesToWrite)
{
check(BytesToWrite >= 0);
int64 ThisSize = FMath::Min<int64>(READWRITE_SIZE, BytesToWrite);
check(Source);
if (__pwrite(File->Handle, Source, ThisSize, CurrentOffset) != ThisSize)
{
bSuccess = false;
break;
}
CurrentOffset += ThisSize;
Source += ThisSize;
BytesToWrite -= ThisSize;
}

// Update the cached file length
Length = FMath::Max(Length, CurrentOffset);

return bSuccess;
}

可以看到是每次1M往文件里存的。

Android写入文件错误码对照

根据 error code number 查找 error string.

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// bionic_errdefs.h
#ifndef __BIONIC_ERRDEF
#error "__BIONIC_ERRDEF must be defined before including this file"
#endif
__BIONIC_ERRDEF( 0 , 0, "Success" )
__BIONIC_ERRDEF( EPERM , 1, "Operation not permitted" )
__BIONIC_ERRDEF( ENOENT , 2, "No such file or directory" )
__BIONIC_ERRDEF( ESRCH , 3, "No such process" )
__BIONIC_ERRDEF( EINTR , 4, "Interrupted system call" )
__BIONIC_ERRDEF( EIO , 5, "I/O error" )
__BIONIC_ERRDEF( ENXIO , 6, "No such device or address" )
__BIONIC_ERRDEF( E2BIG , 7, "Argument list too long" )
__BIONIC_ERRDEF( ENOEXEC , 8, "Exec format error" )
__BIONIC_ERRDEF( EBADF , 9, "Bad file descriptor" )
__BIONIC_ERRDEF( ECHILD , 10, "No child processes" )
__BIONIC_ERRDEF( EAGAIN , 11, "Try again" )
__BIONIC_ERRDEF( ENOMEM , 12, "Out of memory" )
__BIONIC_ERRDEF( EACCES , 13, "Permission denied" )
__BIONIC_ERRDEF( EFAULT , 14, "Bad address" )
__BIONIC_ERRDEF( ENOTBLK , 15, "Block device required" )
__BIONIC_ERRDEF( EBUSY , 16, "Device or resource busy" )
__BIONIC_ERRDEF( EEXIST , 17, "File exists" )
__BIONIC_ERRDEF( EXDEV , 18, "Cross-device link" )
__BIONIC_ERRDEF( ENODEV , 19, "No such device" )
__BIONIC_ERRDEF( ENOTDIR , 20, "Not a directory" )
__BIONIC_ERRDEF( EISDIR , 21, "Is a directory" )
__BIONIC_ERRDEF( EINVAL , 22, "Invalid argument" )
__BIONIC_ERRDEF( ENFILE , 23, "File table overflow" )
__BIONIC_ERRDEF( EMFILE , 24, "Too many open files" )
__BIONIC_ERRDEF( ENOTTY , 25, "Not a typewriter" )
__BIONIC_ERRDEF( ETXTBSY , 26, "Text file busy" )
__BIONIC_ERRDEF( EFBIG , 27, "File too large" )
__BIONIC_ERRDEF( ENOSPC , 28, "No space left on device" )
__BIONIC_ERRDEF( ESPIPE , 29, "Illegal seek" )
__BIONIC_ERRDEF( EROFS , 30, "Read-only file system" )
__BIONIC_ERRDEF( EMLINK , 31, "Too many links" )
__BIONIC_ERRDEF( EPIPE , 32, "Broken pipe" )
__BIONIC_ERRDEF( EDOM , 33, "Math argument out of domain of func" )
__BIONIC_ERRDEF( ERANGE , 34, "Math result not representable" )
__BIONIC_ERRDEF( EDEADLK , 35, "Resource deadlock would occur" )
__BIONIC_ERRDEF( ENAMETOOLONG , 36, "File name too long" )
__BIONIC_ERRDEF( ENOLCK , 37, "No record locks available" )
__BIONIC_ERRDEF( ENOSYS , 38, "Function not implemented" )
__BIONIC_ERRDEF( ENOTEMPTY , 39, "Directory not empty" )
__BIONIC_ERRDEF( ELOOP , 40, "Too many symbolic links encountered" )
__BIONIC_ERRDEF( ENOMSG , 42, "No message of desired type" )
__BIONIC_ERRDEF( EIDRM , 43, "Identifier removed" )
__BIONIC_ERRDEF( ECHRNG , 44, "Channel number out of range" )
__BIONIC_ERRDEF( EL2NSYNC , 45, "Level 2 not synchronized" )
__BIONIC_ERRDEF( EL3HLT , 46, "Level 3 halted" )
__BIONIC_ERRDEF( EL3RST , 47, "Level 3 reset" )
__BIONIC_ERRDEF( ELNRNG , 48, "Link number out of range" )
__BIONIC_ERRDEF( EUNATCH , 49, "Protocol driver not attached" )
__BIONIC_ERRDEF( ENOCSI , 50, "No CSI structure available" )
__BIONIC_ERRDEF( EL2HLT , 51, "Level 2 halted" )
__BIONIC_ERRDEF( EBADE , 52, "Invalid exchange" )
__BIONIC_ERRDEF( EBADR , 53, "Invalid request descriptor" )
__BIONIC_ERRDEF( EXFULL , 54, "Exchange full" )
__BIONIC_ERRDEF( ENOANO , 55, "No anode" )
__BIONIC_ERRDEF( EBADRQC , 56, "Invalid request code" )
__BIONIC_ERRDEF( EBADSLT , 57, "Invalid slot" )
__BIONIC_ERRDEF( EBFONT , 59, "Bad font file format" )
__BIONIC_ERRDEF( ENOSTR , 60, "Device not a stream" )
__BIONIC_ERRDEF( ENODATA , 61, "No data available" )
__BIONIC_ERRDEF( ETIME , 62, "Timer expired" )
__BIONIC_ERRDEF( ENOSR , 63, "Out of streams resources" )
__BIONIC_ERRDEF( ENONET , 64, "Machine is not on the network" )
__BIONIC_ERRDEF( ENOPKG , 65, "Package not installed" )
__BIONIC_ERRDEF( EREMOTE , 66, "Object is remote" )
__BIONIC_ERRDEF( ENOLINK , 67, "Link has been severed" )
__BIONIC_ERRDEF( EADV , 68, "Advertise error" )
__BIONIC_ERRDEF( ESRMNT , 69, "Srmount error" )
__BIONIC_ERRDEF( ECOMM , 70, "Communication error on send" )
__BIONIC_ERRDEF( EPROTO , 71, "Protocol error" )
__BIONIC_ERRDEF( EMULTIHOP , 72, "Multihop attempted" )
__BIONIC_ERRDEF( EDOTDOT , 73, "RFS specific error" )
__BIONIC_ERRDEF( EBADMSG , 74, "Not a data message" )
__BIONIC_ERRDEF( EOVERFLOW , 75, "Value too large for defined data type" )
__BIONIC_ERRDEF( ENOTUNIQ , 76, "Name not unique on network" )
__BIONIC_ERRDEF( EBADFD , 77, "File descriptor in bad state" )
__BIONIC_ERRDEF( EREMCHG , 78, "Remote address changed" )
__BIONIC_ERRDEF( ELIBACC , 79, "Can not access a needed shared library" )
__BIONIC_ERRDEF( ELIBBAD , 80, "Accessing a corrupted shared library" )
__BIONIC_ERRDEF( ELIBSCN , 81, ".lib section in a.out corrupted" )
__BIONIC_ERRDEF( ELIBMAX , 82, "Attempting to link in too many shared libraries" )
__BIONIC_ERRDEF( ELIBEXEC , 83, "Cannot exec a shared library directly" )
__BIONIC_ERRDEF( EILSEQ , 84, "Illegal byte sequence" )
__BIONIC_ERRDEF( ERESTART , 85, "Interrupted system call should be restarted" )
__BIONIC_ERRDEF( ESTRPIPE , 86, "Streams pipe error" )
__BIONIC_ERRDEF( EUSERS , 87, "Too many users" )
__BIONIC_ERRDEF( ENOTSOCK , 88, "Socket operation on non-socket" )
__BIONIC_ERRDEF( EDESTADDRREQ , 89, "Destination address required" )
__BIONIC_ERRDEF( EMSGSIZE , 90, "Message too long" )
__BIONIC_ERRDEF( EPROTOTYPE , 91, "Protocol wrong type for socket" )
__BIONIC_ERRDEF( ENOPROTOOPT , 92, "Protocol not available" )
__BIONIC_ERRDEF( EPROTONOSUPPORT, 93, "Protocol not supported" )
__BIONIC_ERRDEF( ESOCKTNOSUPPORT, 94, "Socket type not supported" )
__BIONIC_ERRDEF( EOPNOTSUPP , 95, "Operation not supported on transport endpoint" )
__BIONIC_ERRDEF( EPFNOSUPPORT , 96, "Protocol family not supported" )
__BIONIC_ERRDEF( EAFNOSUPPORT , 97, "Address family not supported by protocol" )
__BIONIC_ERRDEF( EADDRINUSE , 98, "Address already in use" )
__BIONIC_ERRDEF( EADDRNOTAVAIL , 99, "Cannot assign requested address" )
__BIONIC_ERRDEF( ENETDOWN , 100, "Network is down" )
__BIONIC_ERRDEF( ENETUNREACH , 101, "Network is unreachable" )
__BIONIC_ERRDEF( ENETRESET , 102, "Network dropped connection because of reset" )
__BIONIC_ERRDEF( ECONNABORTED , 103, "Software caused connection abort" )
__BIONIC_ERRDEF( ECONNRESET , 104, "Connection reset by peer" )
__BIONIC_ERRDEF( ENOBUFS , 105, "No buffer space available" )
__BIONIC_ERRDEF( EISCONN , 106, "Transport endpoint is already connected" )
__BIONIC_ERRDEF( ENOTCONN , 107, "Transport endpoint is not connected" )
__BIONIC_ERRDEF( ESHUTDOWN , 108, "Cannot send after transport endpoint shutdown" )
__BIONIC_ERRDEF( ETOOMANYREFS , 109, "Too many references: cannot splice" )
__BIONIC_ERRDEF( ETIMEDOUT , 110, "Connection timed out" )
__BIONIC_ERRDEF( ECONNREFUSED , 111, "Connection refused" )
__BIONIC_ERRDEF( EHOSTDOWN , 112, "Host is down" )
__BIONIC_ERRDEF( EHOSTUNREACH , 113, "No route to host" )
__BIONIC_ERRDEF( EALREADY , 114, "Operation already in progress" )
__BIONIC_ERRDEF( EINPROGRESS , 115, "Operation now in progress" )
__BIONIC_ERRDEF( ESTALE , 116, "Stale NFS file handle" )
__BIONIC_ERRDEF( EUCLEAN , 117, "Structure needs cleaning" )
__BIONIC_ERRDEF( ENOTNAM , 118, "Not a XENIX named type file" )
__BIONIC_ERRDEF( ENAVAIL , 119, "No XENIX semaphores available" )
__BIONIC_ERRDEF( EISNAM , 120, "Is a named type file" )
__BIONIC_ERRDEF( EREMOTEIO , 121, "Remote I/O error" )
__BIONIC_ERRDEF( EDQUOT , 122, "Quota exceeded" )
__BIONIC_ERRDEF( ENOMEDIUM , 123, "No medium found" )
__BIONIC_ERRDEF( EMEDIUMTYPE , 124, "Wrong medium type" )
__BIONIC_ERRDEF( ECANCELED , 125, "Operation Canceled" )
__BIONIC_ERRDEF( ENOKEY , 126, "Required key not available" )
__BIONIC_ERRDEF( EKEYEXPIRED , 127, "Key has expired" )
__BIONIC_ERRDEF( EKEYREVOKED , 128, "Key has been revoked" )
__BIONIC_ERRDEF( EKEYREJECTED , 129, "Key was rejected by service" )
__BIONIC_ERRDEF( EOWNERDEAD , 130, "Owner died" )
__BIONIC_ERRDEF( ENOTRECOVERABLE, 131, "State not recoverable" )

#undef __BIONIC_ERRDEF

UE项目启动参数

看了一下引擎里的代码,在Launch模块下Launch\Private\Android\LaunchAndroid.cpp中有InitCommandLine函数:

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
61
62
63
64
65
66
67
68
69
70
// Launch\Private\Android\LaunchAndroid.cpp
static void InitCommandLine()
{
static const uint32 CMD_LINE_MAX = 16384u;

// initialize the command line to an empty string
FCommandLine::Set(TEXT(""));

AAssetManager* AssetMgr = AndroidThunkCpp_GetAssetManager();
AAsset* asset = AAssetManager_open(AssetMgr, TCHAR_TO_UTF8(TEXT("UE4CommandLine.txt")), AASSET_MODE_BUFFER);
if (nullptr != asset)
{
const void* FileContents = AAsset_getBuffer(asset);
int32 FileLength = AAsset_getLength(asset);

char CommandLine[CMD_LINE_MAX];
FileLength = (FileLength < CMD_LINE_MAX - 1) ? FileLength : CMD_LINE_MAX - 1;
memcpy(CommandLine, FileContents, FileLength);
CommandLine[FileLength] = '\0';

AAsset_close(asset);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("APK Commandline: %s"), FCommandLine::Get());
}

// 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");
}

if(CommandLineFile)
{
char CommandLine[CMD_LINE_MAX];
fgets(CommandLine, ARRAY_COUNT(CommandLine) - 1, CommandLineFile);

fclose(CommandLineFile);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

// initialize the command line to an empty string
FCommandLine::Set(TEXT(""));

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Override Commandline: %s"), FCommandLine::Get());
}

#if !UE_BUILD_SHIPPING
if (FString* ConfigRulesCmdLineAppend = FAndroidMisc::GetConfigRulesVariable(TEXT("cmdline")))
{
FCommandLine::Append(**ConfigRulesCmdLineAppend);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ConfigRules appended: %s"), **ConfigRulesCmdLineAppend);
}
#endif
}

简单来说就是在UE4Game/ProjectName/ue4commandline.txt中把启动参数写到里面,引擎启动的时候会从这个文件去读,然后添加到FCommandLine中。

Android设置宽高比

Project Settings-Platforms-Android-Maximum support aspect ratio的值,默认是2.1,但是在全面屏的情况下会有黑边。

它控制的值是AndroidManifest.xml中的值:

1
<meta-data android:name="android.max_aspect" android:value="2.1"/>

我目前设置的值是2.5.

注:Enable FullScreen Immersive on KitKat and above devices控制的是进入游戏时是否隐藏虚拟按键。

UPL for Android

在UE中为移动端添加第三方模块或者修改配置文件时经常会用到AdditionalPropertiesForReceipt,里面创建ReceiptProperty传入的xml文件就是UE的Unreal Plugin Language脚本。

ReceiptProperty的平台名称在IOS和Android上是固定的,分别是IOSPluginAndroidPlugin,不可以指定其他的名字(详见代码UEDeployIOS.cs#L1153UEDeployAndroid.cs#L4303)。

1
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ThirdPartyPath, "Android/PlatformUtils_UPL_Android.xml")));

Android项目中所有的UPL

可以在项目路径下Intermediate/Android/ActiveUPL.xml,里面列出了当前项目中所有的UPL文件路径:

1
2
3
4
5
6
7
Plugins\Online\Android\OnlineSubsystemGooglePlay\Source\OnlineSubsystemGooglePlay_UPL.xml
Plugins\Runtime\AndroidPermission\Source\AndroidPermission\AndroidPermission_APL.xml
Plugins\Runtime\GoogleCloudMessaging\Source\GoogleCloudMessaging\GoogleCloudMessaging_UPL.xml
Plugins\Runtime\GooglePAD\Source\GooglePAD\GooglePAD_APL.xml
Plugins\Runtime\Oculus\OculusVR\Source\OculusHMD\OculusMobile_APL.xml
Source\Runtime\Online\Voice\AndroidVoiceImpl_UPL.xml
Source\ThirdParty\GoogleGameSDK\GoogleGameSDK_APL.xml

删除AndroidManifest.xml中的项

因为UE默认会给AndroidManifest.xml添加项,如果其中的项我们想要手动控制,直接添加的话会产生错误,提示已经存在:

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
UATHelper: Packaging (Android (ASTC)):   > Task :app:processDebugManifest FAILED
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): Z:\app\src\main\AndroidManifest.xml:47:5-106 Error:
UATHelper: Packaging (Android (ASTC)): Element meta-data#com.epicgames.ue4.GameActivity.bUseExternalFilesDir at AndroidManifest.xml:47:5-106 duplicated with element declared at AndroidManifest.xml:27:5-107
UATHelper: Packaging (Android (ASTC)): Z:\app\src\main\AndroidManifest.xml Error:
UATHelper: Packaging (Android (ASTC)): Validation failed, exiting
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): FAILURE: Build failed with an exception.
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): * What went wrong:
UATHelper: Packaging (Android (ASTC)): Execution failed for task ':app:processDebugManifest'.
UATHelper: Packaging (Android (ASTC)): > Manifest merger failed with multiple errors, see logs
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): See http://g.co/androidstudio/manifest-merger for more information about the manifest merger.
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): * Try:
UATHelper: Packaging (Android (ASTC)): Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): * Get more help at https://help.gradle.org
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): BUILD FAILED in 10s
UATHelper: Packaging (Android (ASTC)): 189 actionable tasks: 1 executed, 188 up-to-date
UATHelper: Packaging (Android (ASTC)): ERROR: cmd.exe failed with args /c "C:\Users\lipengzha\Documents\Unreal Projects\GCloudExample\Intermediate\Android\armv7\gradle\rungradle.bat" :app:assembleDebug
PackagingResults: Error: cmd.exe failed with args /c "C:\Users\lipengzha\Documents\Unreal Projects\GCloudExample\Intermediate\Android\armv7\gradle\rungradle.bat" :app:assembleDebug
UATHelper: Packaging (Android (ASTC)): Took 13.3060694s to run UnrealBuildTool.exe, ExitCode=6
UATHelper: Packaging (Android (ASTC)): UnrealBuildTool failed. See log for more details. (C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.25\UBT-.txt)
UATHelper: Packaging (Android (ASTC)): AutomationTool exiting with ExitCode=6 (6)
UATHelper: Packaging (Android (ASTC)): BUILD FAILED
PackagingResults: Error: Unknown Error

如果想要修改或者删除UE默认生成的AndroidManifest.xml中的项,可以通过先删除再添加的方式。

以删除以下项为例:

1
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="false" />

在UPL的androidManifestUpdates中编写以下代码:

1
2
3
4
5
6
7
8
9
10
11
<androidManifestUpdates>
<loopElements tag="meta-data">
<setStringFromAttribute result="ApplicationSectionName" tag="$" name="android:name"/>
<setBoolIsEqual result="bUseExternalFilesDir" arg1="$S(ApplicationSectionName)" arg2="com.epicgames.ue4.GameActivity.bUseExternalFilesDir"/>
<if condition="bUseExternalFilesDir">
<true>
<removeElement tag="$"/>
</true>
</if>
</loopElements>
</androidManifestUpdates>

就是去遍历AndroidManfest.xml中已经存在meta-data中,android:namecom.epicgames.ue4.GameActivity.bUseExternalFilesDir的项给删除。

JNI调用接收ActivityResult

有时需要通过startActivityForResult来创建Intent来执行一些操作,如打开摄像头、打开相册选择图片等。

但是Android做这些操作的时候不是阻塞在当前的函数中的,所以不能直接在调用的函数里接收这些数据。而通过startActivityForResult执行的Action的结果都会调用到Activity的onActivityResult中。

1
2
3
// GameActivity.java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){}

UE在UPL中提供了往OnActivityResult追加Java代码的用法:

1
2
<!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
<gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>

使用这种方式添加的Java代码会追加到OnActivityResult函数的末尾,但是这种方式有一个问题,那就是执行了自己追加到OnActivityResult的代码之后,还要处理接收到的结果,并且传递到UE端来,有点麻烦。

经过翻阅代码,发现UE提供了Java端的OnActivityResult的多播代理事件,这样就可以直接在UE里用C++来监听OnActivityResult的事件,自己做处理。

1
2
3
4
5
6
// Launch/Puclic/Android/AndroidJNI.h
DECLARE_MULTICAST_DELEGATE_SixParams(FOnActivityResult, JNIEnv *, jobject, jobject, jint, jint, jobject);

// 该代理是定义在`FJavaWrapper`里的
// Delegate that can be registered to that is called when an activity is finished
static FOnActivityResult OnActivityResultDelegate;

在UE侧就可以通过绑定这个多播代理来监听Java端的OnActivityResult调用,可以在其中做分别的处理。

它由AndroidJNI.cpp中的Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult函数从Java那边调用过来,调用机制在上个笔记中有记录。

Java调C++

有些需求和实现需要从Java调到C++这边,可以通过下面这种方式:
首先,在GameActivity中新建一个native的java函数声明(不需要定义):

1
public native void nativeOnActivityResult(GameActivity activity, int requestCode, int resultCode, Intent data);

然后在C++端按照下面的规则定义一个C++函数:

1
2
3
4
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult(JNIEnv* jenv, jobject thiz, jobject activity, jint requestCode, jint resultCode, jobject data)
{
FJavaWrapper::OnActivityResultDelegate.Broadcast(jenv, thiz, activity, requestCode, resultCode, data);
}

可以看到函数名字的规则为:

  1. 函数前需要加JNI_METHOD修饰,它是一个宏__attribute__ ((visibility ("default"))) extern "C"
  2. 函数名需要以Java_开头,并且后面跟上com_epicgames_ue4_GameActivity_,标识是定义在GameActivity中的
  3. 然后再跟上java中的函数名

接受参数的规则:

  1. 第一个参数是Java的Env
  2. 第二个是java里的this
  3. 后面的参数以此是从java里传递参数

AndroidP的全面屏适配

在UE4打包的时候,会给项目生成GameActivity.java文件,里面的OnCreate具有适配全面屏的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onCreate(Bundle savedInstanceState)
{
// ...
if (UseDisplayCutout)
{
// will not be true if not Android Pie or later
WindowManager.LayoutParams params = getWindow().getAttributes();
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(params);
}
// ...
}

Android P和之后的系统不支持,所以就要自己写Jni调用来强制让P和之后系统版本支持。
在UE4.23+以后的引擎版本,支持了通过UPL往OnCreate函数添加代码,就可以直接把代码插入到GameActivity.java了:

1
2
3
4
5
6
7
8
9
10
<gameActivityOnCreateFinalAdditions>
<insert>
// P版本允许使用刘海
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams lp = this.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
this.getWindow().setAttributes(lp);
}
</insert>
</gameActivityOnCreateFinalAdditions>

在UE4.23之前不可以给OnCreate添加代码,只能自己写个JNI调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void AndroidThunkJava_SetFullScreenDisplayForP()
{
final GameActivity Activity = this;
runOnUiThread(new Runnable()
{
private GameActivity InActivity = Activity;
public void run()
{
WindowManager windowManager = InActivity.getWindowManager();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
{

WindowManager.LayoutParams lp = InActivity.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
InActivity.getWindow().setAttributes(lp);
Log.debug( "call AndroidThunkJava_SetFullScreenDisplayForP");
}
}
});
}

将其在UPL中通过<gameActivityClassAdditions>添加到GameActivity.java中,在游戏启动时通过JNI调用即可。

注:

layoutInDisplayCutoutMode的可选项为:

外部资料

Android重启App

当游戏更新完毕之后有时候需要重启App才可以生效,在UE中可以使用UPL写入以下java代码:

1
2
3
4
5
6
7
8
9
10
public void AndroidThunkJava_AndroidAPI_RestartApplication( ) {
Context context = getApplicationContext();
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(context.getPackageName());
int delayTime = 500;
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent restartIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + delayTime, restartIntent);
System.exit(0);
}

在需要重启的时候通过jni调用来触发:

1
2
3
4
5
6
7
8
9
void RestartApplication()
{
#if PLATFORM_ANDROID
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true))
{
FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, AndroidThunkJava_AndroidAPI_RestartApplication);
}
#endif
}

常见问题

app:assembleDebug报错

1
UATHelper: Packaging (Android…) ERROR: cmd.exe failed with args /c “[ProjectPath]\Intermediate/Android/APK/gradle/rungradle.bat” :app:assembleDebug

将下面选项取消勾选即可:

在4.24+中没有了Enable Gradle instead of Ant选项,仔细看了下log发现是因为UE要从网络上下载Grade下载失败导致的,可以在打包时开启全局代理。或者使用我打包的gradle版本:gradle-5.4.1.7z,将其解压到以下路径即可:

1
C:\Users\lipengzha\.gradle\wrapper\dists\gradle-5.4.1-all\3221gyojl5jsh0helicew7rwx\gradle-5.4.1

然后创建一个环境变量ANDROID_HOME指向该路径即可。

还有一些相关的文章,可以更新Android Tools,操作方法为:

  1. run NVPACK/android-sdk-windows/tools/android.bat
  2. click on “Deselect All”
  3. update Extras/Android Support Repository

文章:

相关链接

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

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

本文标题:UE开发笔记:Android篇
文章作者:查利鹏
发布时间:2021年04月15日 19时54分
本文字数:本文一共有9.7k字
原始链接:https://imzlp.com/posts/17996/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!