You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android跨应用写入外部存储问题:如何向其他应用数据目录写入二进制文件?

兄弟,这个问题我太熟了!本质是Android从API 29(Android 10)开始推行的**分区存储(Scoped Storage)**机制搞的鬼,加上部分厂商(比如三星、一加)提前适配了类似限制,才导致你的旧代码在新设备上失效。我给你拆解下原因,再给你对应不同版本的靠谱解决方案:

一、为啥旧方法不管用了?
  • Android 10及以后,系统把每个应用的Android/data/<<包名>>目录划为私有外部存储区,哪怕你申请了WRITE_EXTERNAL_STORAGE权限,默认也不允许跨应用直接读写这个区域——这是为了提升用户数据安全性。
  • 你的小米Pocophone F1是Android 9(API 28),还没强制分区存储,所以旧代码能正常跑;但三星S9、一加5T这类设备要么升级到了Android 10+,要么厂商提前锁了这个权限,就会抛出Permission denied的异常。
二、分版本适配的正确写法

针对不同Android版本,得用不同的方案来实现写入:

1. Android 11+(API 30及以上)

这时候分区存储是强制开启的,而且requestLegacyExternalStorage属性也失效了,最合规的方式是用**存储访问框架(SAF)**让用户手动选择目标目录,然后通过ContentResolver写入:

步骤1:引导用户选择目标目录

先启动SAF的目录选择界面:

private static final int REQUEST_CODE_PICK_DIR = 1001;

// 触发目录选择
private void pickTargetDirectory() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    // 可以设置提示语,引导用户找到目标应用的Android/data/<<包名>>/files目录
    intent.putExtra(Intent.EXTRA_TITLE, "请选择目标应用的files目录");
    startActivityForResult(intent, REQUEST_CODE_PICK_DIR);
}

步骤2:处理选择结果并写入文件

onActivityResult(或者用Jetpack的Activity Result API)中获取目录Uri,然后写入数据:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_PICK_DIR && resultCode == RESULT_OK) {
        Uri treeUri = data.getData();
        // 持久化权限,下次启动不用再让用户选
        getContentResolver().takePersistableUriPermission(treeUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        
        // 创建要写入的文件
        String targetFileName = "your_binary_file.bin";
        DocumentFile parentDir = DocumentFile.fromTreeUri(this, treeUri);
        DocumentFile targetFile = parentDir.createFile("application/octet-stream", targetFileName);
        
        // 写入二进制数据
        try (OutputStream os = getContentResolver().openOutputStream(targetFile.getUri())) {
            os.write(myData);
            os.flush();
            // 这里可以加个成功提示
        } catch (IOException e) {
            e.printStackTrace();
            // 处理写入失败的情况
        }
    }
}

这种方式虽然需要用户手动选一次目录,但权限可以持久化,后续操作就不用重复选择了,而且完全符合Google的存储规范,不会被Play商店拒审。

2. Android 10(API 29)

这个版本可以用过渡方案,在AndroidManifest.xmlapplication标签里加个属性,暂时兼容旧的存储逻辑:

<application
    ...
    android:requestLegacyExternalStorage="true">
    ...
</application>

加上这个之后,你的旧代码(FileOutputStream那套)就能在Android 10上正常工作,但这只是临时方案,Android 12之后这个属性就失效了,所以还是建议尽快迁移到SAF方案。

3. Android 9及以下(API 28及以下)

你的旧代码没问题,但要记得动态申请存储权限(Android 6之后危险权限需要动态申请,光在清单里加没用):

private static final int REQUEST_CODE_STORAGE_PERM = 1002;

// 检查并申请存储权限
private void checkStoragePermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                REQUEST_CODE_STORAGE_PERM);
    } else {
        // 权限已授予,执行旧的写入逻辑
        writeFileWithOldMethod();
    }
}

// 你的旧写入方法
private void writeFileWithOldMethod() {
    BufferedOutputStream bos;
    try {
        bos = new BufferedOutputStream(new FileOutputStream(filePath + fileName));
        bos.write(myData);
        bos.flush();
        bos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
三、避坑提醒
  • 别碰MANAGE_EXTERNAL_STORAGE权限:这个权限是给系统级文件管理器用的,普通应用申请了也过不了Google Play的审核,完全没必要碰。
  • 先检查目标目录是否存在:如果目标应用已经被卸载,它的Android/data/<<包名>>目录会被系统自动删除,写入前要先判断,避免报错。
  • 多设备测试:一定要在Android 10、11、12+的设备上都测一遍,确保兼容性。

内容的提问来源于stack exchange,提问作者ffonz

火山引擎 最新活动