如何通过ContentProvider最优地提供虚拟文件?
自定义ContentProvider无磁盘副本提供Zip归档的跨SDK方案优化
我实现了一个自定义ContentProvider,可通过内容URI按需生成并提供Zip归档文件——文件在请求前不存在,由多个文件合并而成,类似导出功能。
最初考虑将文件写入磁盘,再通过ParcelFileDescriptor.open(File)返回给请求方,但这种方式会留下冗余磁盘副本,副本易过期,且没有原生机制在文件使用完成后自动删除,需要自行实现复杂的清理逻辑。因此希望直接在内存中处理,避免写入磁盘。
现有方案分析
1. 管道传输方案(ParcelFileDescriptor.createPipe())
尝试通过管道在内存中直接传输Zip内容,多数应用可正常工作,但Gmail会提前关闭输入流,导致管道破裂异常,同时提示文件为空。实现代码如下:
@Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { long id = Long.parseLong(Objects.requireNonNull(uri.getPathSegments().get(0))); try { ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); ParcelFileDescriptor output = pipe[1]; new Thread(() -> { try (ParcelFileDescriptor.AutoCloseOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(output)) { serializeZipFile(out, id); } catch (IOException e) { Log.e(TAG, "Error during zip serialization.", e); } }).start(); return pipe[0]; } catch (IOException e) { Log.e(TAG, "Error while trying to create pipe", e); throw new FileNotFoundException("Failed to create pipe"); } }
2. ProxyFileDescriptor方案(SDK≥26)
查阅文档后使用StorageManager#openProxyFileDescriptor,完美解决了Gmail的问题,但该方法仅在API 26及以上可用,无法覆盖我的最低SDK(24)需求。实现代码如下:
StorageManager storageManager = Objects.requireNonNull(getContext()).getSystemService(StorageManager.class); ByteArrayOutputStream out = new ByteArrayOutputStream(); serializeZipFile(out, id); return storageManager.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY, new ProxyFileDescriptorCallback() { private final byte[] bytes = out.toByteArray(); @Override public long onGetSize() { return bytes.length; } @Override public int onRead(long offset, int size, byte[] data) { if (offset >= bytes.length) { return 0; } int intOffset = Math.toIntExact(offset); int realSize = Math.min(Math.min(size, bytes.length - intOffset), data.length); System.arraycopy(bytes, intOffset, data, 0, realSize); return realSize; } @Override public void onRelease() {} }, new Handler(Looper.getMainLooper()));
SDK<26的优化实现
针对API 24-25的场景,可以通过以下方式优化,无需放弃Gmail支持:
1. 针对Gmail的临时文件+自动清理
- 检查调用方包名:通过
getContentResolver().getCallingPackage()判断是否为Gmail(包名com.google.android.gm)。 - 生成临时Zip文件:将Zip写入应用内部存储的临时目录,通过
FileProvider提供访问URI。 - 自动清理:使用
BroadcastReceiver监听Intent.ACTION_PACKAGE_REMOVED避免Gmail被卸载时残留文件,同时用JobScheduler设置延迟任务(比如1小时后)删除临时文件;另外在ContentProvider的shutdown()方法中清理过期文件。
2. 优化管道方案的兼容性
- 提前返回文件大小:在ContentProvider的
query()方法中,针对OpenableColumns.SIZE字段返回Zip的预估大小,让Gmail等应用提前知晓文件长度,减少提前关闭流的概率。 - 优雅处理管道破裂:在写入线程中捕获
IOException(管道破裂时会抛出),终止写入逻辑,避免无用日志和崩溃:new Thread(() -> { try (ParcelFileDescriptor.AutoCloseOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(output)) { serializeZipFile(out, id); } catch (IOException e) { // 忽略管道破裂异常,因为调用方已关闭流 if (!(e.getMessage() != null && e.getMessage().contains("Broken pipe"))) { Log.e(TAG, "Error during zip serialization.", e); } } }).start();
3. 内存文件(MemoryFile)适配
对于较小的Zip文件,可使用MemoryFile存储内容,再通过管道将内容传输给调用方:
// 先将Zip写入MemoryFile MemoryFile memoryFile = new MemoryFile("temp-zip", zipSize); OutputStream mfOut = memoryFile.getOutputStream(); serializeZipFile(mfOut, id); mfOut.close(); // 通过管道传输 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); new Thread(() -> { try (InputStream mfIn = memoryFile.getInputStream(); ParcelFileDescriptor.AutoCloseOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])) { byte[] buffer = new byte[4096]; int read; while ((read = mfIn.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { Log.e(TAG, "Error transferring memory file.", e); } finally { try { memoryFile.close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); return pipe[0];
注意:MemoryFile的大小受限于应用内存,不适合大文件。
内容的提问来源于stack exchange,提问作者RoiEX
相关产品推荐
相关产品推荐

