UE 开发 Android 的工程实践

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控制的是进入游戏时是否隐藏虚拟按键。