在 Android 上写入外部 SD 卡的通用方法

新手上路,请多包涵

在我的应用程序中,我需要在设备存储中存储大量图像。此类文件往往会满足设备存储,我希望允许用户能够选择外部 SD 卡作为目标文件夹。

我到处都读到 Android 不允许用户写入外部 SD 卡,我所说的 SD 卡是指外部和可安装的 SD 卡,而 不是外部存储,但文件管理器应用程序设法在所有 Android 版本上写入外部 SD。

在不同的 API 级别(Pre-KitKat、KitKat、Lollipop+)授予对外部 SD 卡的读/写访问权限的更好方法是什么?

更新 1

我尝试了 Doomknight 的回答中的方法 1,但无济于事:如您所见,我在尝试写入 SD 之前在运行时检查权限:

 HashSet<String> extDirs = getStorageDirectories();
for(String dir: extDirs) {
    Log.e("SD",dir);
    File f = new File(new File(dir),"TEST.TXT");
    try {
        if(ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)==PackageManager.PERMISSION_GRANTED) {
            f.createNewFile();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

但是我遇到访问错误,在两种不同的设备上尝试过:HTC10 和 Shield K1。

 10-22 14:52:57.329 30280-30280/? E/SD: /mnt/media_rw/F38E-14F8
10-22 14:52:57.329 30280-30280/? W/System.err: java.io.IOException: open failed: EACCES (Permission denied)
10-22 14:52:57.329 30280-30280/? W/System.err:     at java.io.File.createNewFile(File.java:939)
10-22 14:52:57.329 30280-30280/? W/System.err:     at com.myapp.activities.TestActivity.onResume(TestActivity.java:167)
10-22 14:52:57.329 30280-30280/? W/System.err:     at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1326)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.Activity.performResume(Activity.java:6338)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3336)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3384)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2574)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.access$900(ActivityThread.java:150)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1399)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:102)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.os.Looper.loop(Looper.java:168)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:5885)
10-22 14:52:57.330 30280-30280/? W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
10-22 14:52:57.330 30280-30280/? W/System.err:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:819)
10-22 14:52:57.330 30280-30280/? W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:709)
10-22 14:52:57.330 30280-30280/? W/System.err: Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
10-22 14:52:57.330 30280-30280/? W/System.err:     at libcore.io.Posix.open(Native Method)
10-22 14:52:57.330 30280-30280/? W/System.err:     at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
10-22 14:52:57.330 30280-30280/? W/System.err:     at java.io.File.createNewFile(File.java:932)
10-22 14:52:57.330 30280-30280/? W/System.err:  ... 14 more

原文由 Vektor88 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.5k
2 个回答

概括

您可以在 不同的 api 级别运行时的 API23+授予对外部 SD 卡的读/写访问权限

自 KitKat 以来, 如果您使用特定于应用程序的目录,则 不需要 权限,否则 需要

通用方式:

历史 表明没有 通用的方法可以写入外部 SD 卡 ,但仍在继续……

这些设备的外部存储配置示例 证明了 这一事实

基于API的方式:

KitKat 之前尝试使用 Doomsknight 方法 1,否则使用方法 2。

在清单(Api < 23) 和 运行时(Api >= 23) 中请求权限。

推荐方式:

ContextCompat.getExternalFilesDirs 解决不需要共享文件时 的访问错误

安全的共享方式是使用 内容提供商新的存储访问框架

隐私感知方式:

从 Android Q Beta 4 开始,默认情况下,针对 Android 9(API 级别 28)或更低版本的应用没有变化。

默认情况下(或选择加入)以 Android Q 为目标平台的应用会获得外部存储的 过滤视图


  1. 初步回答。

在 Android 上写入外部 SD 卡的通用方法

由于 不断变化,在 Android 上没有 通用写入外部 SD 卡的 方法:

  • Pre-KitKat:Android 官方平台除例外情况外,完全不支持 SD 卡。

  • KitKat:引入了 API,让应用程序可以访问 SD 卡上应用程序特定目录中的文件。

  • Lollipop:添加了 API 以允许应用程序请求访问其他提供商拥有的文件夹。

  • Nougat:提供了一个简化的 API 来访问常见的外部存储目录。

  • Android Q 隐私更改:应用范围和媒体范围的存储

在不同的 API 级别上授予对外部 SD 卡的读/写访问权限的更好方法是什么

基于 Doomsknight 的回答 的,以及 Dave Smith 和 Mark Murphy博客文章: 1、2、3


  1. 更新的答案。

更新 1 。我尝试了 Doomknight 的回答中的方法 1,但无济于事:

如您所见,我在尝试写入 SD 之前在运行时检查权限…

我会使用特定于应用程序的目录 来避免更新问题的 问题,并且 ContextCompat.getExternalFilesDirs() 使用 getExternalFilesDir 文档 作为参考。

改进 启发式方法,根据不同的 API 级别确定代表可移动媒体的内容,例如 android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT

… 但是我遇到访问错误,在两种不同的设备上尝试过:HTC10 和 Shield K1。

请记住, Android 6.0 支持便携式存储设备,第三方应用程序必须通过 存储访问框架。您的设备 HTC10Shield K1 可能是 API 23。

您的日志显示 权限被拒绝的异常 访问 /mnt/media_rw ,例如 API 19+ 的 修复

 <permission name="android.permission.WRITE_EXTERNAL_STORAGE" >
<group gid="sdcard_r" />
<group gid="sdcard_rw" />
<group gid="media_rw" /> // this line is added via root in the link to fix it.
</permission>

我从来没有尝试过,所以我不能共享代码,但我会避免 for 尝试写入所有返回的目录,并 根据剩余空间寻找最佳可用存储目录写入

也许 Gizm0 替代 您的 getStorageDirectories() 方法是一个很好的起点。

ContextCompat.getExternalFilesDirs 如果您不需要 访问其他文件夹, 则可以解决 此问题


  1. Android 1.0 .. Pre-KitKat。

在 KitKat 之前尝试使用 Doomsknight 方法 1 或阅读 Gnathonic 的 回复

 public static HashSet<String> getExternalMounts() {
    final HashSet<String> out = new HashSet<String>();
    String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
    String s = "";
    try {
        final Process process = new ProcessBuilder().command("mount")
                .redirectErrorStream(true).start();
        process.waitFor();
        final InputStream is = process.getInputStream();
        final byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            s = s + new String(buffer);
        }
        is.close();
    } catch (final Exception e) {
        e.printStackTrace();
    }

    // parse output
    final String[] lines = s.split("\n");
    for (String line : lines) {
        if (!line.toLowerCase(Locale.US).contains("asec")) {
            if (line.matches(reg)) {
                String[] parts = line.split(" ");
                for (String part : parts) {
                    if (part.startsWith("/"))
                        if (!part.toLowerCase(Locale.US).contains("vold"))
                            out.add(part);
                }
            }
        }
    }
    return out;
}

将下一个代码添加到您的 AndroidManifest.xml 并阅读 获取外部存储的访问权限

对外部存储的访问受到各种 Android 权限的保护。

从 Android 1.0 开始,写访问受到 WRITE_EXTERNAL_STORAGE 权限 的保护。

从 Android 4.1 开始,读访问受到 READ_EXTERNAL_STORAGE 权限的保护。

为了…在外部存储上写入文件,您的应用程序必须获得…系统权限:

 <manifest ...>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

如果你需要两者……,你只需要请求 WRITE_EXTERNAL_STORAGE 权限。

阅读 Mark Murphy 的解释推荐Dianne HackbornDave Smith 的帖子

  • 直到 Android 4.4,Android 才正式支持可移动媒体,从 KitKat 开始,FMW API 中出现了“primary”和“secondary”外部存储的概念。
  • 以前的应用程序只是依赖于 MediaStore 索引,随硬件一起提供或检查挂载点并应用一些启发式方法来确定什么代表可移动媒体。

  1. Android 4.4 KitKat 引入 了存储访问框架 (SAF)

由于错误请忽略下一条注释,但请尝试使用 ContextCompat.getExternalFilesDirs()

  • 自 Android 4.2 以来,Google 一直要求设备制造商为了安全(多用户支持)而锁定可移动媒体,并且在 4.4 中添加了新的测试。
  • 由于添加了 KitKat getExternalFilesDirs() 和其他方法以返回所有可用存储卷上的可用路径(返回的第一项是主卷)。
  • 下表显示了开发人员可能会尝试做什么以及 KitKat 将如何响应: 在此处输入图像描述

注意: 从 Android 4.4 开始,如果您只读取或写入您的应用程序私有的文件,则不需要这些权限。有关详细信息…,请参阅 保存应用程序私有的文件

 <manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
</manifest>

另请阅读 Paolo Rovelli 的解释 并尝试使用 Jeff Sharkey 的自 KitKat 以来的解决方案

在 KitKat 中,现在有一个公共 API 用于与这些辅助共享存储设备进行交互。

新的 Context.getExternalFilesDirs()Context.getExternalCacheDirs() 方法可以返回多个路径,包括主要和次要设备。

然后您可以遍历它们并检查 Environment.getStorageState()File.getFreeSpace() 以确定存储文件的最佳位置。

这些方法也可在 support-v4 库中的 ContextCompat 上找到。

从Android 4.4开始,外部存储设备上的文件的所有者、组和模式现在根据目录结构进行合成。这使应用程序能够在外部存储上管理其特定于包的目录,而无需它们持有广泛的 WRITE_EXTERNAL_STORAGE 权限。例如,包名为 com.example.foo 的应用程序现在可以在没有权限的情况下自由访问外部存储设备上的 Android/data/com.example.foo/ 。这些综合权限是通过将原始存储设备包装在 FUSE 守护进程中来实现的。

使用 KitKat,您无需 root 即可获得“完整解决方案”的机会几乎为零:

Android 项目肯定在这里搞砸了。没有应用程序可以完全访问外部 SD 卡:

  • 文件管理器: 您不能使用它们来管理您的外部 SD 卡。在大多数地区,他们只能读不能写。
  • 媒体应用程序: 您不能再重新标记/重新组织您的媒体收藏,因为这些应用程序无法写入。
  • 办公应用程序: 几乎相同

唯一允许第三方应用程序在您的外部卡上写入的地方是“他们自己的目录”(即 --- /sdcard/Android/data/<package_name_of_the_app> )。

真正修复的唯一方法需要制造商(他们中的一些修复了它,例如华为用他们的 P6 Kitkat 更新)或 root …… (Izzy 的解释继续在这里)


  1. Android 5.0 引入了更改和 DocumentFile 帮助程序类。

getStorageState 在API 19中添加,在API 21中弃用, 使用 getExternalStorageState(File)

这是一个很棒的教程,用于与 KitKat 中的存储访问框架进行交互。

在 Lollipop 中与新 API 交互非常相似(Jeff Sharkey 的解释)


  1. Android 6.0 Marshmallow 引入了新的 运行时权限 模型。

如果 API 级别 23+,则在运行时请求 权限 并阅读 在运行时请求权限

从 Android 6.0(API 级别 23)开始,用户在应用程序运行时向应用程序授予权限,而不是在安装应用程序时……或更新应用程序……用户可以撤销权限。

 // Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.WRITE_EXTERNAL_STORAGE);

Android 6.0 引入了一种新的 运行时权限 模型,其中应用程序在运行时需要时请求功能。由于新模型包含 READ/WRITE_EXTERNAL_STORAGE 权限,平台需要动态授予存储访问权限,而无需终止或重启已运行的应用程序。它通过维护所有已安装存储设备的三个不同视图来实现此目的:

  • /mnt/runtime/default 显示给没有特殊存储权限的应用程序…
  • /mnt/runtime/read 显示给具有 READ_EXTERNAL_STORAGE 的应用程序
  • /mnt/runtime/write 显示给具有 WRITE_EXTERNAL_STORAGE 的应用程序

  1. Android 7.0 提供了一个简化的 API 来访问外部存储目录。

范围目录访问 在 Android 7.0 中,应用程序可以使用新的 API 来请求访问特定的 外部存储 目录,包括可移动媒体上的目录,例如 SD 卡…

有关详细信息,请参阅 范围目录访问培训

阅读 Mark Murphy 的帖子: 谨慎使用范围目录访问它在 Android Q 中被弃用

请注意,7.0 中添加的范围目录访问在 Android Q 中已弃用。

具体而言,不推荐使用 StorageVolume 上的 createAccessIntent() 方法。

他们添加了一个 createOpenDocumentTreeIntent() 可以用作替代方案。


  1. Android 8.0 Oreo .. Android Q Beta 更改。

从 Android O 开始,存储访问框架允许 自定义文档提供程序 为驻留在远程数据源中的文件创建可搜索的文件描述符…

权限在 Android O 之前,如果应用程序在运行时请求权限并且权限被授予,系统还会错误地向应用程序授予属于同一权限组的其余权限,这些权限已在清单中注册。

对于面向 Android O 的应用,此行为已得到纠正。该应用仅被授予其明确请求的权限。但是,一旦用户授予应用程序权限,该权限组中的所有后续权限请求都会自动授予。

例如, READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE

更新: Android Q 早期测试版暂时用更细粒度的媒体特定权限取代了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限。

注意: 谷歌在 Beta 1 中引入了 角色,并在 Beta 2 之前从文档中删除了它们……

注意: 在早期测试版中引入的特定于媒体收藏的权限 READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEO 现已过时 更多信息:

Mark Murphy 的 Q Beta 4(最终 API)评论: 外部存储的死亡:传奇的终结(?)

“死亡比生命更普遍。每个人都死了,但不是每个人都活着。” —— 安德鲁·萨克斯


  1. 相关问题和推荐答案。

如何获取 Android 4.0+ 的外部 SD 卡路径?

mkdir() 在内部闪存中工作,但不是 SD 卡?

getExternalFilesDir 和 getExternalStorageDirectory() 之间的区别

为什么 getExternalFilesDirs() 在某些设备上不起作用?

如何使用为 Android 5.0 (Lollipop) 提供的新 SD 卡访问 API

Android 5.0及以上系统写入外置SD卡

使用 SAF(存储访问框架)的 Android SD 卡写权限

SAFFAQ:存储访问框架常见问题解答


  1. 相关错误和问题。

错误:在 Android 6 上,当使用 getExternalFilesDirs 时,它不会让您在其结果中创建新文件

在没有写入权限的情况下写入 Lollipop 上 getExternalCacheDir() 返回的目录失败

原文由 albodelu 发布,翻译遵循 CC BY-SA 4.0 许可协议

我相信有两种方法可以实现这一目标:

方法 1:( 由于权限更改, 不适 用于 6.0 及更高版本)

我多年来一直在许多设备版本上使用这种方法,没有任何问题。 感谢原始来源,因为它不是我写的。

它将在字符串目录位置列表中返回 所有安装的媒体(包括 Real SD 卡) 。使用列表,您可以询问用户保存位置等。

您可以使用以下方式调用它:

  HashSet<String> extDirs = getStorageDirectories();

方法:

 /**
 * Returns all the possible SDCard directories
 */
public static HashSet<String> getStorageDirectories() {
    final HashSet<String> out = new HashSet<String>();
    String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
    String s = "";
    try {
        final Process process = new ProcessBuilder().command("mount")
                .redirectErrorStream(true).start();
        process.waitFor();
        final InputStream is = process.getInputStream();
        final byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            s = s + new String(buffer);
        }
        is.close();
    } catch (final Exception e) {
        e.printStackTrace();
    }

    // parse output
    final String[] lines = s.split("\n");
    for (String line : lines) {
        if (!line.toLowerCase().contains("asec")) {
            if (line.matches(reg)) {
                String[] parts = line.split(" ");
                for (String part : parts) {
                    if (part.startsWith("/"))
                        if (!part.toLowerCase().contains("vold"))
                            out.add(part);
                }
            }
        }
    }
    return out;
}

方法二:

使用 v4 支持库

import android.support.v4.content.ContextCompat;

只需调用以下命令即可获取 File 存储位置列表。

  File[] list = ContextCompat.getExternalFilesDirs(myContext, null);

但是,这些位置在用法上有所不同。

返回所有外部存储设备上特定于应用程序的目录的绝对路径,应用程序可以在其中放置它拥有的持久文件。这些文件在应用程序内部,通常不会作为媒体对用户可见。

此处返回的外部存储设备被视为设备的永久部分,包括模拟外部存储和物理媒体插槽,例如电池仓中的 SD 卡。返回的路径不包括临时设备,例如 USB 闪存驱动器。

应用程序可以在任何或所有返回的设备上存储数据。例如,应用程序可能会选择在可用空间最大的设备上存储大文件

有关 ContextCompat 的更多信息

它们就像特定于应用程序的文件。对其他应用程序隐藏。

原文由 IAmGroot 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题