UE4:4.27升级安卓SDK到30后的文件保存问题

问题:

4.27将targetSDK版本改为30之后,打包之后的dev版本无法在设备写入文件

SDK升级相关修改

1
private string GetSdkApiLevel(AndroidToolChain ToolChain)

UEDeployAndroid.cs文件中,这个函数会根据TargetSDKVersion来返回对应的SDK版本,如果你的本地环境有多个UE版本或者有通过Android Studio安装过SDK的话,可能会出现返回错误的情况,可以在这里手动指定,比如直接ruturn你需要的版本号

同理,也有可能会出错的地方是build-tools的版本,可以在GetBuildToolsVersion函数中手动指定版本

1
private string GetBuildToolsVersion()

这里我能够成功通过4.27打包TargetSDK 30版本所用的环境如下:

  • build-tools: 30.0.3
  • NDK: 21.4.7075529
  • jdk: 1.8.0_271

存储空间权限相关

首先捋一下一些概念

根据安卓文档的说法,数据和文件存储主要分为四类,我们主要关注App-specific storageShared storage

其中getFilesDir() or getCacheDir() 返回内部存储空间的路径
getExternalFilesDir()getExternalCacheDir()返回外部存储空间

以上“内部存储路径”和“外部存储路径”都是App-specific storage,在API 19以上都不需要任何权限,其他应用无法访问,卸载时会被删除

举例,创建了一个API 30的原生安卓应用,包名com.example.test,然后打印一下这几个路径:

尝试在这四个路径下创建test.test文件:

另外两个internal目录是无法看到的

看一下UE相关的log:

可以定位到AndroidJNI文件中,首先是在JNI_OnLoad
这个函数里

可以看到这里是通过Android的EnvironmentgetExternalStorageDirectory函数获取到了External存储空间对应的路径

然后是在Java_com_epicgames_ue4_GameActivity_nativeSetGlobalActivity 这个函数里面

根据不同的情况重新修改了GFilePathBase
这个函数会在GameActivity的onCreate阶段调用

为了开发调试方便起见,可以考虑修改这两处的位置,将GFilePathBase指向一个可以更加容易访问的位置

由于安卓的限制,在TargetSDK版本在Android 10及以上已经不能在根目录下创建文件夹,但是可以在Documents或者Download文件夹下进行读写,这里选择直接将GFilePathBase改为Documents下面,其他不做改变

但是这样会有一个新问题,如果存在了同名文件,是没有权限覆盖文件的,需要应用声明MANAGE_EXTERNAL_STORAGE这个权限,并在启动应用时主动申请,由用户手动授权

如果没有在AndroidManifest文件中声明需要这个权限的话,在系统的权限授权界面是不会看到该应用的,极其在启动的时候申请也无法授权

如果只是需要将Project下的某些文件获取到,比如Log文件或者PSO缓存文件等,也可以考虑在dev和shipping模式下都将base目录指向应用的External目录,在Java侧添加接口来将文件复制到Documents目录,只需要注意检查Documents目录下的重名文件即可。安卓30以上经过测试,release版本的应用也不需要权限

参考实现

java侧的一些方法:

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
137
138
139
140
// 封装一个复制文件的方法
public void CopyFile(File file, File destFile)
{
if (!destFile.exists())
{
try
{
destFile.createNewFile();
}
catch (IOException e)
{
e.printStackTrace();
}
}
FileInputStream fis = null;
FileOutputStream fos = null;
try
{
fis = new FileInputStream(file);
fos = new FileOutputStream(destFile);
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1)
{
fos.write(buffer, 0, len);
}
}
catch (IOException e)
{
e.printStackTrace();
} finally
{
if (fis != null)
{
try
{
fis.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
if (fos != null)
{
try
{
fos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}

// 封装一个复制文件夹的方法
public void CopyFiles(String SourceDirPath, String DestDirPath)
{
Log.debug("CopyFiles: SourceDirPath: " + SourceDirPath + " DestDirPath: " + DestDirPath);
File sourceDir = new File(SourceDirPath);
File destDir = new File(DestDirPath);
if (!destDir.exists())
{
destDir.mkdirs();
}
if (sourceDir.exists() && sourceDir.isDirectory())
{
File[] files = sourceDir.listFiles();
for (File file : files)
{
if (file.isFile())
{
File destFile = new File(destDir, file.getName());
CopyFile(file, destFile);
}
else if (file.isDirectory())
{
String destPath = destDir.getAbsolutePath() + "/" + file.getName();
CopyFiles(file.getAbsolutePath(), destPath);
}
}
}
else
{
if(!sourceDir.exists())
{
Log.debug("CopyFiles: SourceDirPath does not exist: " + SourceDirPath);
}
else if(!sourceDir.isDirectory())
{
Log.debug("CopyFiles: SourceDirPath is not a directory: " + SourceDirPath);
}
}
}

// 获取项目名的方法,如果在AndroidManifest中声明了ProjectName的话,就直接返回,否则返回包名的最后一段
public String GetProjectName()
{
String projectName;
if (_bundle.containsKey("com.epicgames.ue4.GameActivity.ProjectName"))
{
projectName = _bundle.getString("com.epicgames.ue4.GameActivity.ProjectName");
Log.debug("Found ProjectName = " + projectName);
}
else
{
String packageName = getPackageName();
String[] packageSegments = packageName.split("\\.");
String lastSegment = packageSegments[packageSegments.length - 1];
projectName = lastSegment;
}
return projectName;
}

// 获取项目的Saved目录
public String GetProjectSavedDir()
{
String projectName = GetProjectName();
String path = getExternalFilesDir(null).getAbsolutePath();
return path + "/UE4Game/" + projectName + "/" + projectName + "/Saved";
}

// 获取Documents目录的绝对路径
public String GetDocumentsProjectDirPath()
{
String projectName = GetProjectName();
String path = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOCUMENTS).getAbsolutePath();
return path + "/" + projectName;
}

// 将Saved目录下的文件复制到Documents目录下,避免权限问题,加上时间戳防止出现重名文件夹导致复制失败
public void AndroidThunkJava_CopySavedFilesToDocuments()
{
String savedDirPath = GetProjectSavedDir();
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String destDirPath = GetDocumentsProjectDirPath() + "/CopiedSaved_" + timeStamp;
CopyFiles(savedDirPath, destDirPath);
}

在C++侧的AndroidJNI中添加对应接口的声明,就可以在安卓版本中使用了

参考文档

https://developer.android.google.cn/training/data-storage?hl=zh-cn

https://developer.android.google.cn/training/data-storage/shared/documents-files?hl=zh-cn


UE4:4.27升级安卓SDK到30后的文件保存问题
http://muchenhen.com/posts/26609/
作者
木尘痕
发布于
2023年11月10日
许可协议