You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:17:45