修改游戏默认的数据存储路径

默认情况下,使用 UE 打包出游戏的 Apk 并在手机上安装之后,启动游戏会在 /storage/emulated/0/UE4Game/ 下创建游戏的数据目录 (也就是内部存储器的根目录下)。按照 Google 的规则,每个 APP 的数据文件最好都是放在自己的私有目录,所以我想要把 UE 打包出来的游戏的数据全放到/storage/emulated/0/Android/data/PACKAGE_NAME 目录中 (不管是 log、ini、还是 crash 信息)。
一个看似简单的需求,有几种不同的方法,涉及到了 UE4 的路径管理 /JNI/Android Manifest 以及对 UBT 的代码的分析。

默认的路径:

有两种方法,一种是改动引擎代码实现对 GFilePathBase 的修改,另一种是不改动引擎只添加项目设置中的 manifest 就可以,当然不改动引擎是最好的,不过既然是分析,我就两个都来搞一下,顺便从 UBT 代码分析一下 Project Setting-Android-Use ExternalFilesDir for UE4Game Files 选项没有作用的原因。

改动引擎代码实现

翻了一下引擎代码,发现路径的这部分代码是写在这里的:AndroidPlatformFile.cpp#L946,它是在 GFilePathBase 然后组合 UE4Game+PROJECT_NAME 的路径。

在 UE4.22 及之前的引擎版本中是在 AndroidFile.cpp 文件中的,4.23+ 是在 AndroidPlatformFile.cpp 中的。
基础路径 GFilePathBase 的初始化是在 Launch\Private\Android\AndroidJNI.cpp 中:

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
// Launch\Private\Android\AndroidJNI.cpp
JNIEXPORT jint JNI_OnLoad(JavaVM* InJavaVM, void* InReserved)
{
FPlatformMisc::LowLevelOutputDebugString(TEXT("In the JNI_OnLoad function"));

JNIEnv* Env = NULL;
InJavaVM->GetEnv((void **)&Env, JNI_CURRENT_VERSION);

// if you have problems with stuff being missing especially in distribution builds then it could be because proguard is stripping things from java
// check proguard-project.txt and see if your stuff is included in the exceptions
GJavaVM = InJavaVM;
FAndroidApplication::InitializeJavaEnv(GJavaVM, JNI_CURRENT_VERSION, FJavaWrapper::GameActivityThis);

FJavaWrapper::FindClassesAndMethods(Env);

// hook signals
if (!FPlatformMisc::IsDebuggerPresent() || GAlwaysReportCrash)
{
// disable crash handler.. getting better stack traces from system for now
//FPlatformMisc::SetCrashHandler(EngineCrashHandler);
}

// Cache path to external storage
jclass EnvClass = Env->FindClass("android/os/Environment");
jmethodID getExternalStorageDir = Env->GetStaticMethodID(EnvClass, "getExternalStorageDirectory", "()Ljava/io/File;");
jobject externalStoragePath = Env->CallStaticObjectMethod(EnvClass, getExternalStorageDir, nullptr);
jmethodID getFilePath = Env->GetMethodID(Env->FindClass("java/io/File"), "getPath", "()Ljava/lang/String;");
jstring pathString = (jstring)Env->CallObjectMethod(externalStoragePath, getFilePath, nullptr);
const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
// Copy that somewhere safe
GFilePathBase = FString(nativePathString);
GOBBFilePathBase = GFilePathBase;

// then release...
Env->ReleaseStringUTFChars(pathString, nativePathString);
Env->DeleteLocalRef(pathString);
Env->DeleteLocalRef(externalStoragePath);
Env->DeleteLocalRef(EnvClass);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Path found as '%s'\n"), *GFilePathBase);

// Get the system font directory
jstring fontPath = (jstring)Env->CallStaticObjectMethod(FJavaWrapper::GameActivityClassID, FJavaWrapper::AndroidThunkJava_GetFontDirectory);
const char * nativeFontPathString = Env->GetStringUTFChars(fontPath, 0);
GFontPathBase = FString(nativeFontPathString);
Env->ReleaseStringUTFChars(fontPath, nativeFontPathString);
Env->DeleteLocalRef(fontPath);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Font Path found as '%s'\n"), *GFontPathBase);

// Wire up to core delegates, so core code can call out to Java
DECLARE_DELEGATE_OneParam(FAndroidLaunchURLDelegate, const FString&);
extern CORE_API FAndroidLaunchURLDelegate OnAndroidLaunchURL;
OnAndroidLaunchURL = FAndroidLaunchURLDelegate::CreateStatic(&AndroidThunkCpp_LaunchURL);

FPlatformMisc::LowLevelOutputDebugString(TEXT("In the JNI_OnLoad function 5"));

char mainThreadName[] = "MainThread-UE4";
AndroidThunkCpp_SetThreadName(mainThreadName);

return JNI_CURRENT_VERSION;
}

我们的目的就是要改动 GFilePathBase 的值,因为默认引擎里是通过调用 getExternalStorageDirectory 得到的,其是外部存储的目录即 /storage/emulated/0/,再拼接上UE4Game 就是默认平时我们看到的路径。

因为 getExternalStorageDirectory 这些都是 Environment 的静态成员,没有我们想要获取的路径的方法,但是 Context 中有,UE 的代码中并没有获取到,所以我们要像一个办法得到 App 的 Context。

可以通过下列方法从 JNI 获取 Context,:

1
2
3
4
5
6
7
8
9
10
// 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);
}

之后可以使用 Context 下的函数 getExternalFilesDir 获取到我们想要的路径:

注意 getExternalFilesDir 的原型是:File getExternalFilesDir(String),在使用 JNI 获取 jmehodID 时一定注意签名要传对,不然会 Crash,其签名是(Ljava/lang/String;)Ljava/io/File;

1
2
3
4
5
6
7
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);

得到的 nativePathString 的值为:

1
/storage/emulated/0/Android/data/com.imzlp.GWorld/files

其中的 com.imzlp.GWorld 是你的 App 的包名。

然后将其赋值给 GFilePathBase 即可,打开编辑器重新打包 Apk,安装上之后该 APP 所有的数据就会在 /storage/emulated/0/Android/data/PACKAGE_NAME/files 下了。

在 UE 中调用和操作 JNI 以及 Android 存储路径相关的链接:

使用 Manifest 控制

OK,关于分析引擎中修改GFilePathBase 的大致写完了,其实有个不改动引擎的办法,就是在项目设置中添加minifest

其实原理也在 AndoidJNI.cpp 里了,AndroidJNI.cpp中有以下代码:

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
//This function is declared in the Java-defined class, GameActivity.java: "public native void nativeSetGlobalActivity();"
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeSetGlobalActivity(JNIEnv* jenv, jobject thiz, jboolean bUseExternalFilesDir, jstring internalFilePath, jstring externalFilePath, jboolean bOBBinAPK, jstring APKFilename /*, jobject googleServices*/)
{
if (!FJavaWrapper::GameActivityThis)
{
GGameActivityThis = FJavaWrapper::GameActivityThis = jenv->NewGlobalRef(thiz);
if (!FJavaWrapper::GameActivityThis)
{
FPlatformMisc::LowLevelOutputDebugString(TEXT("Error setting the global GameActivity activity"));
check(false);
}

// This call is only to set the correct GameActivityThis
FAndroidApplication::InitializeJavaEnv(GJavaVM, JNI_CURRENT_VERSION, FJavaWrapper::GameActivityThis);

// @todo split GooglePlay, this needs to be passed in to this function
FJavaWrapper::GoogleServicesThis = FJavaWrapper::GameActivityThis;
// FJavaWrapper::GoogleServicesThis = jenv->NewGlobalRef(googleServices);

// Next we check to see if the OBB file is in the APK
//jmethodID isOBBInAPKMethod = jenv->GetStaticMethodID(FJavaWrapper::GameActivityClassID, "isOBBInAPK", "()Z");
//GOBBinAPK = (bool)jenv->CallStaticBooleanMethod(FJavaWrapper::GameActivityClassID, isOBBInAPKMethod, nullptr);
GOBBinAPK = bOBBinAPK;

const char *nativeAPKFilenameString = jenv->GetStringUTFChars(APKFilename, 0);
GAPKFilename = FString(nativeAPKFilenameString);
jenv->ReleaseStringUTFChars(APKFilename, nativeAPKFilenameString);

const char *nativeInternalPath = jenv->GetStringUTFChars(internalFilePath, 0);
GInternalFilePath = FString(nativeInternalPath);
jenv->ReleaseStringUTFChars(internalFilePath, nativeInternalPath);

const char *nativeExternalPath = jenv->GetStringUTFChars(externalFilePath, 0);
GExternalFilePath = FString(nativeExternalPath);
jenv->ReleaseStringUTFChars(externalFilePath, nativeExternalPath);

if (bUseExternalFilesDir)
{
#if UE_BUILD_SHIPPING
GFilePathBase = GInternalFilePath;
#else
GFilePathBase = GExternalFilePath;
#endif
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("GFilePathBase Path override to'%s'\n"), *GFilePathBase);
}

FPlatformMisc::LowLevelOutputDebugStringf(TEXT("InternalFilePath found as '%s'\n"), *GInternalFilePath);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ExternalFilePath found as '%s'\n"), *GExternalFilePath);
}
}

在引擎启动的时候会从 JNI 调过来,其中有一个参数 bUseExternalFilesDir 用来控制修改 GFilePathBase 的值,如果它为 ture,在 Shipping 打包的模式下就会把 GFilePathBase 设置为 GInternalFilePath 的值,也就是下列路径:

1
/data/user/PACKAGE_NAME/files

在非 Shipping 打包模式下会设置为 GExternalFilePath 的值:

1
/storage/emulated/0/Android/data/PACKAGE_NAME/files

但是,问题的关键是 bUseExternalFilesDir 这个从 JNI 调过来的参数我们又如何控制呢?

问题的答案是添加 manifest 信息!本来以为是 ProjectSettings-Android-UseExternalFilesDirForUE4GameFiles 这个选项,但是选中没有任何效果,原因后面会分析。

在详细解释怎么通过 manifest 控制 bUseExternalFilesDir 这个变量之前,需要先知道,UE4 打包出来的 APK 的 Manifest 中默认有什么。

下列是我解包出来的 APK 中的 Manifest 文件:

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
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="internalOnly" package="com.imzlp.TEST" platformBuildVersionCode="29" platformBuildVersionName="10">
<application android:debuggable="true" android:hardwareAccelerated="true" android:hasCode="true" android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:debuggable="true" android:label="@string/app_name" android:launchMode="singleTask" android:name="com.epicgames.ue4.SplashActivity" android:screenOrientation="landscape" android:theme="@style/UE4SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:configChanges="density|keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|uiMode" android:debuggable="true" android:label="@string/app_name" android:launchMode="singleTask" android:name="com.epicgames.ue4.GameActivity" android:screenOrientation="landscape" android:theme="@style/UE4SplashTheme">
<meta-data android:name="android.app.lib_name" android:value="UE4"/>
</activity>
<activity android:configChanges="density|keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|uiMode" android:name=".DownloaderActivity" android:screenOrientation="landscape" android:theme="@style/UE4SplashTheme"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.EngineVersion" android:value="4.22.3"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.EngineBranch" android:value="++UE4+Release-4.22"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.ProjectVersion" android:value="1.0.0.0"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.DepthBufferPreference" android:value="0"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bPackageDataInsideApk" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bVerifyOBBOnStartUp" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bShouldHideUI" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.ProjectName" android:value="Mobile422"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.AppType" android:value=""/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bHasOBBFiles" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.BuildConfiguration" android:value="Development"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.CookedFlavors" android:value="ETC2"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bValidateTextureFormats" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseDisplayCutout" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bAllowIMU" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bSupportsVulkan" android:value="false"/>
<meta-data android:name="com.google.android.gms.games.APP_ID" android:value="@string/app_id"/>
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>
<activity android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:name="com.google.android.gms.ads.AdActivity"/>
<service android:name="OBBDownloaderService"/>
<receiver android:name="AlarmReceiver"/>
<receiver android:name="com.epicgames.ue4.LocalNotificationReceiver"/>
<receiver android:exported="true" android:name="com.epicgames.ue4.MulticastBroadcastReceiver">
<intent-filter>
<action android:name="com.android.vending.INSTALL_REFERRER"/>
</intent-filter>
</receiver>
<meta-data android:name="android.max_aspect" android:value="2.1"/>
</application>
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.VIBRATE"/>
</manifest>

该文件在 UnrealBuildTool\Platform\Android\UEDeployAdnroid.cs 中的 GenerateManifest 函数中生成。

其中控制了 APK 安装后的权限要求、属性配置等等,可以看到其中有一条:

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

bUseExternalFilesDir的值为 false!,那么怎么把它设置为 true 呢?

需要打开 Project Settings-Android-Advanced APK Packaging,找到Extra Tags for<application> node,因为<meta-data /> 是在 Application 下的,所以需要在这个选项下添加。

添加内容为:

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

没错!直接把 meta-data 这一行直接粘贴过来改一下值就可以了,UE 打包时会自动把这里的内容追加到 ManifestApplication项尾部,这样就覆盖了默认的 false 的值。

然后再打包就可以看到 bUseExternalFilesDir 这个选项起作用了。

UPL 控制 bUseExternalFilesDir

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

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
12
13
14
15
<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>

<addElements tag="application">
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="true" />
</addElements>
</androidManifestUpdates>

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

项目设置 bUseExternalFilesDir 选项无效分析

下面来分析一下 Project Settings-Android-Use ExternalFilesDir for UE4Game Files 这个选项不生效。
其实这个选项确实是控制 manifest 中的 bUseExternalFilesDir 的值的,在 UBT 中操作的,上面已经提到 manifest 文件就是在 UBT 中生成的。
但是 ,虽然 UE 提供了这个参数,但是目前的引擎中(4.22.3) 这个选项是没有作用的,因为它被默认禁用了。
首先,UBT 的构建调用栈为:

  1. AndroidPlatform(UEBuildAndroid.cs)的Deploy
  2. UEDeployAndroid(UEDeployAndroid.cs)中的PrepTargetForDeployment
  3. UEDeployAndroid(UEDeployAndroid.cs)中的MakeApk(最关键的函数)

MakeApk这个函数接收了一个特殊的控制参数bDisallowExternalFilesDir:

1
2
// UEDeployAndroid.cs
private void MakeApk(AndroidToolChain ToolChain, string ProjectName, TargetType InTargetType, string ProjectDirectory, string OutputPath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bMakeSeparateApks, bool bIncrementalPackage, bool bDisallowPackagingDataInApk, bool bDisallowExternalFilesDir);

它用来控制是否启用项目设置中的 Use ExternalFilesDir for UE4Game Files 选项。

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
// UEDeployAndroid.cs
private void MakeApk(AndroidToolChain ToolChain, string ProjectName, TargetType InTargetType, string ProjectDirectory, string OutputPath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bMakeSeparateApks, bool bIncrementalPackage, bool bDisallowPackagingDataInApk, bool bDisallowExternalFilesDir)
{
// ...
bool bUseExternalFilesDir = UseExternalFilesDir(bDisallowExternalFilesDir);
// ...
}

// func UseExternalFilesDir
public bool UseExternalFilesDir(bool bDisallowExternalFilesDir, ConfigHierarchy Ini = null)
{
if (bDisallowExternalFilesDir)
{
return false;
}

// make a new one if one wasn't passed in
if (Ini == null)
{
Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
}

// we check this a lot, so make it easy
bool bUseExternalFilesDir;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bUseExternalFilesDir", out bUseExternalFilesDir);

return bUseExternalFilesDir;
}

可以看到,如果 bDisallowExternalFilesDir 为 true 的话,就完全不会去读项目设置里的配置。

而关键的地方就在于,在 PrepTargetForDeployment 中调用 MakeApk 的时候,给了默认参数 true:

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
// UEDeployAndroid.cs
public override bool PrepTargetForDeployment(UEBuildDeployTarget InTarget)
{
//Log.TraceInformation("$$$$$$$$$$$$$$ PrepTargetForDeployment $$$$$$$$$$$$$$$$$ {0}", InTarget.TargetName);
AndroidToolChain ToolChain = new AndroidToolChain(InTarget.ProjectFile, false, InTarget.AndroidArchitectures, InTarget.AndroidGPUArchitectures);

// we need to strip architecture from any of the output paths
string BaseSoName = ToolChain.RemoveArchName(InTarget.OutputPaths[0].FullName);

// get the receipt
UnrealTargetPlatform Platform = InTarget.Platform;
UnrealTargetConfiguration Configuration = InTarget.Configuration;
string ProjectBaseName = Path.GetFileName(BaseSoName).Replace("-" + Platform, "").Replace("-" + Configuration, "").Replace(".so", "");
FileReference ReceiptFilename = TargetReceipt.GetDefaultPath(InTarget.ProjectDirectory, ProjectBaseName, Platform, Configuration, "");
Log.TraceInformation("Receipt Filename: {0}", ReceiptFilename);
SetAndroidPluginData(ToolChain.GetAllArchitectures(), CollectPluginDataPaths(TargetReceipt.Read(ReceiptFilename, UnrealBuildTool.EngineDirectory, InTarget.ProjectDirectory)));

// make an apk at the end of compiling, so that we can run without packaging (debugger, cook on the fly, etc)
string RelativeEnginePath = UnrealBuildTool.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory());
MakeApk(ToolChain, InTarget.TargetName, InTarget.ProjectDirectory.FullName, BaseSoName, RelativeEnginePath, bForDistribution: false, CookFlavor: "",
bMakeSeparateApks: ShouldMakeSeparateApks(), bIncrementalPackage: true, bDisallowPackagingDataInApk: false, bDisallowExternalFilesDir: true);

// if we made any non-standard .apk files, the generated debugger settings may be wrong
if (ShouldMakeSeparateApks() && (InTarget.OutputPaths.Count > 1 || !InTarget.OutputPaths[0].FullName.Contains("-armv7-es2")))
{
Console.WriteLine("================================================================================================================================");
Console.WriteLine("Non-default apk(s) have been made: If you are debugging, you will need to manually select one to run in the debugger properties!");
Console.WriteLine("================================================================================================================================");
}
return true;
}

这真是好坑的一个点…我看 UE4.18 UBT 的源码中是一样的,都是默认关闭的。明明有这个选项,却默认给关闭了,但是还没有任何的提示,这真是比较蛋疼的事情。

总结

其实改动引擎代码和使用 manifest 各有好处:

  • 改动代码的好处是可以任意指定路径(当然不一定合理),但缺点是需要源码版引擎;
  • 使用 Manifest 的好处是不需要源码版引擎,但是只能使用 InternalFilesDir(Shipping) 或者ExternalFilesDir(not-shipping);

顺道吐槽一下 UE,一个选项没作用,还把它在设置里暴露出来干嘛…