Modify the default data storage path for Android games
UE源码分析:修改游戏默认的数据存储路径
Posted onInUnrealEngine
,
AndroidViews: Symbols count in article: 7kReading time ≈18 mins.
By default, after packaging a game with UE and installing the Apk on a phone, launching the game will create the game’s data directory under /storage/emulated/0/UE4Game/ (which is at the root of the internal storage). According to Google’s rules, it is best for each app’s data files to be placed in their own private directory. Therefore, I want to put all the game’s data packaged by UE into the directory /storage/emulated/0/Android/data/PACKAGE_NAME (regardless of whether it’s log, ini, or crash information). This seemingly simple requirement has several different approaches involving UE4’s path management, JNI, Android Manifest, and analysis of UBT code.
Default path:
There are two methods: one is to modify the engine code to implement a change to GFilePathBase, and the other is to simply add manifest in the project settings without changing the engine. Of course, not changing the engine is the best approach. However, since this is an analysis, I’ll explore both methods and also analyze the Project Setting - Android - Use ExternalFilesDir for UE4Game Files option to see why it has no effect.
Modifying Engine Code
After looking through the engine code, I found that this part of the path is written here: AndroidPlatformFile.cpp#L946. It takes GFilePathBase and combines it with UE4Game + PROJECT_NAME to form the path.
In versions of the engine up to UE4.22, this was in the AndroidFile.cpp file, but from 4.23 onwards, it’s in AndroidPlatformFile.cpp. The initialization of the base path GFilePathBase happens in Launch\Private\Android\AndroidJNI.cpp:
// 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); }
// 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); constchar * 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; }
Our goal is to change the value of GFilePathBase, because by default it gets its value by calling getExternalStorageDirectory, which is the directory of external storage: /storage/emulated/0/. When combined with UE4Game, this gives the default path we usually see.
Since getExternalStorageDirectory and the like are static members of Environment and do not provide the path we want, but Context does, and the UE code does not retrieve it, we need a way to get the App’s Context.
We can get the Context from JNI using the following method:
Then we can use the function getExternalFilesDir under Context to obtain our desired path:
Note: the prototype of getExternalFilesDir is: File getExternalFilesDir(String), so when using JNI to get the jmehodID, it’s important to match the signature exactly, or it will crash; its signature is (Ljava/lang/String;)Ljava/io/File;.
Here, com.imzlp.GWorld is your App’s package name.
Then simply assign this value to GFilePathBase, and after repackaging the Apk in the editor and reinstalling it, all the app’s data will be under /storage/emulated/0/Android/data/PACKAGE_NAME/files.
Links for invoking and operating JNI and Android storage paths in UE:
OK, I’ve generally written about analyzing the modification of GFilePathBase in the engine. There is actually a non-engine modification approach, which is to add manifest in the project settings.
The principle is also in AndoidJNI.cpp, where there is the following code:
//This function is declared in the Java-defined class, GameActivity.java: "public native void nativeSetGlobalActivity();" JNI_METHOD voidJava_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;
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("InternalFilePath found as '%s'\n"), *GInternalFilePath); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ExternalFilePath found as '%s'\n"), *GExternalFilePath); } }
During engine startup, it will be called from JNI, where one of the parameters bUseExternalFilesDir controls whether to modify the value of GFilePathBase. If it’s true, in Shipping packaging mode, GFilePathBase will be set to the value of GInternalFilePath, which is the following path:
1
/data/user/PACKAGE_NAME/files
This is the app’s private data path on Android, which cannot be accessed without ROOT permissions. This also means that files cannot be manually placed in this directory; I believe the design intent is to prevent users from accessing data generated during the release version.
However, UE also provides a configuration to output logs to non-internal storage during Shipping mode. This is done by adding bPublicLogFiles=True under [/Script/AndroidRuntimeSettings.AndroidRuntimeSettings] in DefaultEngine.ini.
In non-Shipping packaging mode, it will be set to the value of GExternalFilePath:
This is the external storage sandbox path for Android apps, which will be automatically cleaned up when the app is uninstalled.
However, the key issue is how to control the bUseExternalFilesDir parameter that comes from JNI.
The answer to this question is to add manifest information! I initially thought it was the ProjectSettings - Android - UseExternalFilesDirForUE4GameFiles option, but selecting it has no effect, as I will analyze later.
Before explaining how to control the bUseExternalFilesDir variable through the manifest, it’s important to know what the Manifest of an APK packaged by UE4 contains by default.
Below is the Manifest file I unpacked from the APK:
The value of bUseExternalFilesDir is false! How can we set it to true?
You need to open Project Settings - Android - Advanced APK Packaging and find the Extra Tags for <application> node. Since <meta-data /> is under Application, you need to add content here.
That’s right! Just paste this line with meta-data and modify the value, and UE will automatically append this content to the Manifest in the Application section during packing, overriding the default false value.
Then, after repackaging, you will see that the bUseExternalFilesDir option takes effect.
Controlling bUseExternalFilesDir via UPL
Since UE automatically adds com.epicgames.ue4.GameActivity.bUseExternalFilesDir to AndroidManifest.xml, if we want to manually control it, directly adding it will cause errors, prompting that it already exists:
This code iterates through the AndroidManifest.xml, locating and deleting the meta-data entry with the android:name as com.epicgames.ue4.GameActivity.bUseExternalFilesDir.
Analysis of the Ineffectiveness of the bUseExternalFilesDir Option in Project Settings
Now let’s analyze why the Project Settings - Android - Use ExternalFilesDir for UE4Game Files option is ineffective. In fact, this option does control the value of bUseExternalFilesDir in the manifest, manipulated within UBT, as previously mentioned, the manifest file is generated within UBT. However, although UE provides this parameter, in the current engine (4.22.3) this option has no effect because it is disabled by default.
First, the UBT build call stack is as follows:
Deploy in AndroidPlatform (UEBuildAndroid.cs)
PrepTargetForDeployment in UEDeployAndroid (UEDeployAndroid.cs)
MakeApk in UEDeployAndroid (UEDeployAndroid.cs) (the most critical function)
The MakeApk function receives a special control parameter bDisallowExternalFilesDir:
// func UseExternalFilesDir publicboolUseExternalFilesDir(bool bDisallowExternalFilesDir, ConfigHierarchy Ini = null) { if (bDisallowExternalFilesDir) { returnfalse; }
// 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; }
As can be seen, if bDisallowExternalFilesDir is true, it will not read the configuration in project settings at all.
The critical point is that, when calling MakeApk in PrepTargetForDeployment, the default parameter passed is true:
// 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("================================================================================================================================"); } returntrue; }
This is really a tricky point… I see that the UBT source code for UE4.18 is the same, it is still disabled by default. Clearly, there is this option, yet it is closed by default without any prompt, which is quite frustrating.
Conclusion
In fact, modifying the engine code and using the manifest both have their advantages:
The advantage of modifying the code is that you can specify any path (not necessarily reasonable), but the downside is that you need the source version of the engine.
The advantage of using the Manifest is that you don’t need the source version of the engine, but can only use InternalFilesDir (Shipping) or ExternalFilesDir (not-shipping).
By the way, let’s complain a bit about UE; why expose an option that doesn’t work in the settings?
The article is finished. If you have any questions, please comment and communicate.