Android Q设备下如何利用Content Uri将文件发送至PHP MySQL服务器
我之前在Android Q设备上也踩过这个坑!因为从Android Q(API 29)开始,Scoped Storage机制限制了直接访问外部存储的文件路径,你拿到的Content Uri没法像以前那样直接转成文件路径来上传。别慌,给你几个实用的解决方案:
方案1:将Content Uri对应的文件复制到应用私有目录后上传
应用对自己的私有目录(比如getCacheDir()或getFilesDir())拥有完全读写权限,我们可以先把Content Uri指向的文件复制到这个目录,再上传这个私有文件。
代码示例:
private File copyContentUriToPrivateDir(ContentResolver resolver, Uri contentUri) throws IOException { // 获取文件的MIME类型,用来命名临时文件(可选) String mimeType = resolver.getType(contentUri); String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); String fileName = "upload_temp." + (fileExtension != null ? fileExtension : "tmp"); // 创建应用缓存目录下的临时文件 File tempFile = new File(getCacheDir(), fileName); // 复制文件流 try (InputStream inputStream = resolver.openInputStream(contentUri); OutputStream outputStream = new FileOutputStream(tempFile)) { byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,提升复制效率 int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } return tempFile; }
使用方式:
在获取到Content Uri后(比如通过onActivityResult或AndroidX的ActivityResultContracts.GetContent),调用这个方法得到临时文件,然后用你熟悉的方式(比如HttpURLConnection、OkHttp)上传这个文件到PHP服务器即可。上传完成后记得删除临时文件,避免占用存储空间。
方案2:直接上传Content Uri的输入流(推荐)
这个方案更高效,不需要生成临时文件,直接把Content Uri对应的输入流传给HTTP请求体,完美适配Scoped Storage规则,适合大文件上传。
用OkHttp实现的代码示例:
// 1. 构建包含文件流的请求体 RequestBody multipartBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", "uploaded_file", // 这里的"file"对应PHP接口接收的参数名 RequestBody.create( MediaType.parse(getContentResolver().getType(contentUri)), new InputStreamRequestBody(getContentResolver().openInputStream(contentUri)) )) .build(); // 2. 发起上传请求 OkHttpClient client = new OkHttpClient(); Request uploadRequest = new Request.Builder() .url("你的PHP服务器上传接口地址") .post(multipartBody) .build(); client.newCall(uploadRequest).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 处理上传失败逻辑,比如提示用户 runOnUiThread(() -> Toast.makeText(getApplicationContext(), "上传失败", Toast.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { runOnUiThread(() -> Toast.makeText(getApplicationContext(), "上传成功", Toast.LENGTH_SHORT).show()); } else { runOnUiThread(() -> Toast.makeText(getApplicationContext(), "上传失败:服务器错误", Toast.LENGTH_SHORT).show()); } } }); // 自定义RequestBody,用来包装输入流 static class InputStreamRequestBody extends RequestBody { private final InputStream inputStream; public InputStreamRequestBody(InputStream inputStream) { this.inputStream = inputStream; } @Override public MediaType contentType() { return null; // 这里可以根据实际MIME类型设置,或者留空让OkHttp自动处理 } @Override public void writeTo(BufferedSink sink) throws IOException { // 用Okio的工具类把输入流写入到请求体 sink.writeAll(Okio.source(inputStream)); } }
说明:
PHP服务器端可以像处理普通文件上传一样,通过$_FILES['file']获取上传的文件,不需要做额外修改。这个方案省去了文件复制的步骤,节省存储空间和时间。
方案3:获取媒体文件的真实路径(仅适用于媒体文件)
如果你的应用只处理图片、视频、音频这类媒体文件,可以尝试通过MediaStore获取真实路径,但注意这个方法在Android Q+下对非媒体文件可能返回null,而且即使拿到路径,你也不一定有读写权限,所以不推荐作为首选方案。
代码示例:
public String getMediaFilePath(ContentResolver resolver, Uri uri) { String[] projection = {MediaStore.MediaColumns.DATA}; Cursor cursor = null; try { cursor = resolver.query(uri, projection, null, null, null); if (cursor != null && cursor.moveToFirst()) { int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); return cursor.getString(columnIndex); } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) { cursor.close(); } } return null; }
额外注意事项
- Android Q及以上,通过系统文件选择器(
ACTION_GET_CONTENT)获取的Content Uri,不需要申请READ_EXTERNAL_STORAGE权限,系统会自动授予临时访问权限。 - 如果你使用Retrofit,可以参考方案2的思路,自定义
RequestBody来包装输入流,实现类似的上传逻辑。
内容的提问来源于stack exchange,提问作者Ankit Verma

