在安卓系统中通用的在外置SD卡上写入文件的方法

83
在我的应用程序中,我需要将大量图片存储在设备存储器中。这些文件往往会占用设备存储空间,我希望允许用户选择外部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

4
@PavneetSingh,这并不正确,所有的文件管理器应用程序都可以访问外部SD卡,即使没有root权限。 - Vektor88
1
他们使用我告诉你的方法,测试只需使用KitKat操作系统(未root)并安装ES,尝试从中删除文件,您将收到警告(这可能会使您的手机变砖),要求自行承担风险应用根过程。 - Pavneet_Singh
1
我强烈建议你永远不要依赖这段代码,就像我说的,你的应用程序单独无法完成它,但媒体提供程序是一个系统应用程序,因此你可以利用它的功能来做你想做的事情。 - Pavneet_Singh
forum.xda-developers.com: "如果一切都失败了,此代码应仅用于在Android 4.4+设备上写入辅助存储。"由于该应用程序应(a)可用于非root手机,并且(b)必须修改sdcard上的文件,因此是否有任何替代方法可以在不root手机和不更改android标准权限的情况下完成? - k3b
根据类似问题 android-sd-card-write-permission-using-saf-storage-access-framework 的回答,应该可以通过 SAF API 实现,而不是直接通过文件系统实现。然而,我尚未在我的 Android 4.4 上成功运行此方法。 - k3b
显示剩余6条评论
7个回答

133

摘要

您可以在不同的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 SmithMark Murphy的博客文章:123


  1. 更新的答案。

更新1。我尝试了Doomknight答案中的方法1,但没有成功:

如你所见,在尝试写SD卡之前,我在运行时检查权限...

我建议使用应用程序特定目录避免问题,参考getExternalFilesDir文档使用ContextCompat.getExternalFilesDirs()

根据不同API级别(如android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT),改进启发式算法以确定可移动媒体的表示方式。

引用:

...但我在两个不同的设备上尝试都出现访问错误:HTC10和Shield K1。

请记住 Android 6.0 支持可移动存储设备,第三方应用程序必须通过 Storage Access Framework 进行访问。您的设备 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. 安卓 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中,并阅读获取外部存储访问权限

Access to external storage is protected by various Android permissions.

Starting in Android 1.0, write access is protected with the WRITE_EXTERNAL_STORAGE permission.

Starting in Android 4.1, read access is protected with the READ_EXTERNAL_STORAGE permission.

In order to ... write files on the external storage, your app must acquire ... system permissions:

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

If you need to both..., you need to request only the WRITE_EXTERNAL_STORAGE permission.

阅读Mark Murphy的解释以及推荐Dianne HackbornDave Smith的文章

  • 在Android 4.4之前,Android中没有官方支持可移动媒体的功能。从KitKat开始,在FMW API中引入了“主”和“次”外部存储的概念。
  • 早期应用程序只是依赖于MediaStore索引、随硬件一起提供或检查挂载点,并应用一些启发式算法来确定什么代表可移动媒体。

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

由于存在错误,请忽略下一个提示,但请尝试使用ContextCompat.getExternalFilesDirs()

  • 自Android 4.2以来,Google要求设备制造商为了安全性(多用户支持)锁定可移动媒体,并在4.4中添加了新测试。
  • 自KitKat以来,添加了getExternalFilesDirs()和其他方法,以返回所有可用存储卷上可用的路径(返回的第一个项目是主卷)。
  • 下表显示了开发人员可能尝试执行的操作以及KitKat的响应方式: enter image description here

Note: Beginning with Android 4.4, these permissions are not required if you're reading or writing only files that are private to your app. For more info..., see saving files that are app-private.

<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()来确定存储文件的最佳位置。

这些方法也可在支持v4库中的ContextCompat上使用。

从Android 4.4开始,外部存储设备上文件的所有者、组和模式现在是基于目录结构综合的。这使得应用程序可以管理它们在外部存储上的特定于包的目录,而无需持有广泛的“WRITE_EXTERNAL_STORAGE”权限。例如,名为“com.example.foo”的应用程序现在可以自由访问外部存储设备上的“Android/data/com.example.foo/”而不需要任何权限。这些综合权限是通过在FUSE守护程序中包装原始存储设备来实现的。
在KitKat版本中,如果没有进行root操作,你很难获得一个“完整的解决方案”。
安卓项目在这里明显出了问题。没有应用程序可以完全访问外部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的解释)


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

如果 API 级别为 23 或更高,请在运行时请求权限并阅读运行时请求权限

Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app ... or update the app ... user can revoke the permissions.

// 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的应用程序

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

作用域目录访问 在Android 7.0中,应用程序可以使用新的API请求访问特定的外部存储目录,包括可移动介质上的目录,如SD卡...

更多信息,请参见Scoped Directory Access training

阅读Mark Murphy的文章:小心使用Scoped Directory Access它在Android Q中已被弃用

请注意,Android Q中已经弃用了7.0中添加的作用域目录访问功能。具体来说,StorageVolume上的createAccessIntent()方法已被弃用。他们添加了一个createOpenDocumentTreeIntent()作为替代方案。
  1. Android 8.0 Oreo .. Android Q Beta 变化。

自 Android O 开始,存储访问框架允许自定义文档提供程序为驻留在远程数据源中的文件创建可寻址的文件描述符... 在 Android O 之前,如果应用在运行时请求权限并且被授予了该权限,则系统还会错误地授予应用属于同一权限组并在清单中注册的其余权限。对于针对 Android O 的应用程序,已经纠正了这种行为。只授予应用程序明确请求的权限。但是,一旦用户向应用授予权限,该权限组中所有后续权限请求都将自动授予。例如,READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE... 权限 更新: Android Q早期的beta版本暂时替换了READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE权限,使用更精细化、媒体相关的权限。
注意: Google在Beta 1中引入了角色,但在Beta 2之前从文档中删除了它们...
注意: 早期beta版本引入的与媒体集合相关的权限——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(棒棒糖)提供的新SD卡访问API?

在Android 5.0及以上版本中写入外部SD卡的方法?

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

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


  1. 相关的错误和问题。

在安卓6上使用getExternalFilesDirs时,结果中无法创建新文件

在Lollipop上写入getExternalCacheDir()返回的目录时,没有写入权限会失败


2
非常深入的分析。+1。但是我必须指出,我并没有编写那个方法。它来自另一个来源,大约几年前 :) 希望它能帮到他。 - IAmGroot
如果需要在应用程序目录之外编写文件,似乎使用官方API是正确的方法。看起来这里解决了类似的问题,我会在另一天检查它。 - albodelu
ES文件管理器在Android 4.4.2手机上如何创建文件夹?@albodelu,您能否解释一下? - karanatwal.github.io
我非常感激这样的见解,但是我的天啊,谷歌创造了一个多么混乱的局面。 - Andrew S
显示剩余6条评论

11

我认为有两种方法可以实现这个目标:

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

我已经在许多设备版本上使用了这种方法多年,没有遇到任何问题。鸣谢原始来源,因为不是我写的。

它将返回包括真实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;
}

方法2:

使用v4支持库

import android.support.v4.content.ContextCompat;

只需调用以下代码即可获取存储位置的File列表。

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

然而它们的使用位置有所不同。

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

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

应用程序可以将数据存储在其中任何一个返回的设备上,例如,应用程序可以选择将大文件存储在可用空间最多的设备上。

有关ContextCompat的更多信息

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


1
请分享一下你在方法1中获取代码的链接。感谢原作者的贡献。谢谢。 - Eugen Pechanec
2
@EugenPechanec 我完全同意。不过这段代码是我多年前从一个很棒的来源获取的。我会去找一下它。 - IAmGroot
1
我不清楚我应该如何处理这些路径:如果设备没有root权限,通过指定其绝对路径,我将无法获得对sdcard的写访问权限。我错了吗?也许我可以将一些数据存储在/external_sd/data/data/com.myapp...中的专用应用程序文件夹中,但我将无法写入/external_sd/MyApp/Images。 - Vektor88
1
@vektor88,你可以在没有root的情况下正常访问它。我和我的客户从来不使用已root的设备。你遇到了什么错误?你是否添加了权限?如下所述,你是否针对最新版本进行了目标设置,并在运行时请求了权限?将目标更改为20,看看是否有效,然后再解决运行时权限请求。否则,请提供错误信息。 - IAmGroot
1
@Vektor88 这个概念纯粹是为了测试,以查看您现在是否有权限问题。 如果您的目标是api 23,则会引出一个问题,您是否在运行时请求了权限。 尽管这与希望获取SDCard位置以保存(最初提问)的愿望是不同的问题。 https://developer.android.com/training/permissions/requesting.html - IAmGroot
显示剩余16条评论

3

这只是另一个答案。我只显示5.0+,因为我相信Doomknight在此发布的答案是Android 4.4及以下版本最好的方法。

这最初是由我在此处发布的(Is there a way to get SD Card size in Android?),以获取Android 5.0+上外部SD卡的大小

要将外部SD卡作为File获取:

public File getExternalSdCard() {
    File externalStorage = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        File storage = new File("/storage");

        if(storage.exists()) {
            File[] files = storage.listFiles();

            for (File file : files) {
                if (file.exists()) {
                    try {
                        if (Environment.isExternalStorageRemovable(file)) {
                            externalStorage = file;
                            break;
                        }
                    } catch (Exception e) {
                        Log.e("TAG", e.toString());
                    }
                }
            }
        }
    } else {
        // do one of many old methods
        // I believe Doomsknight's method is the best option here
    }

    return externalStorage;
}

注意:我只能获取“第一个”外部SD卡,但您可以修改它并返回ArrayList<File>而不是File,让循环继续而不是在找到第一个后调用break


感谢 @th3pat3l,参考 isExternalStorageRemovable https://developer.android.com/reference/android/os/Environment.html#isExternalStorageRemovable(java.io.File)。 - albodelu
1
这仅适用于基于您的应用程序访问外部SD卡子目录的情况:例如,在我的LG手机上,它给了我:/storage/0403-0201/Android/data/com.your.application/files--如果您想要将文件添加到DCIM目录 - 即使您在清单中具有权限并在运行时请求它们,并且您进入应用程序->权限下的设置并打开存储,如果您尝试在那里创建和打开文件,仍会收到ENOENT(没有此类文件或目录)错误。至少在我的LG Aristo SDK 23上是这样的。我看到唯一有效的方法是使用SAF。 - MarkJoel60
isExternalStorageRemovable 只适用于 >= 21 版本,是否有可能在 <= 19 API 设备上确定存储是否可移动(microsd 卡),并以某种方式实现类似的方法? - user924

3
除了其他精彩的回答,我可以为这个问题添加一些内容,以便为读者提供更广泛的覆盖范围。在这里,我将使用两个可计数的资源来介绍外部存储。
第一个资源是来自Android编程,《The Big Nerd Ranch Guide》第二版,第16章,第294页。
该书描述了基本的和外部文件和目录方法。我将尝试概括与您的问题相关的内容。
以下是该书的一部分: 外部存储 您的照片需要不仅需要屏幕上的位置。全尺寸图片太大了,无法放入SQLite数据库,更不能放在Intent中。它们需要一个地方存放在设备的文件系统中。通常情况下,您会将它们放在私有存储中。请回想一下,您用私有存储保存SQLite数据库。使用像Context.getFileStreamPath(String)Context.getFilesDir()这样的方法,您也可以对常规文件执行相同的操作(这些文件将位于与存储SQLite数据库的databases子文件夹相邻的子文件夹中)。
Context中的基本文件和目录方法。
| Method                                                                                |
|---------------------------------------------------------------------------------------|
|File getFilesDir()                                                                      |
| - Returns a handle to the directory for private application files.                    |
|                                                                                       |
|FileInputStream openFileInput(String name)                                             |
| - Opens an existing file for input (relative to the files directory).                 |
|                                                                                       |
|FileOutputStream openFileOutput(String name, int mode)                                 |
| - Opens a file for output, possibly creating it (relative to the files directory).    |
|                                                                                       |
|File getDir(String name, int mode)                                                     |
| - Gets (and possibly creates) a subdirectory within the files directory.              |
|                                                                                       |
|String[] fileList()                                                                    |
| - Gets a list of file names in the main files directory, such as for use with         |
|   openFileInput(String).                                                              |
|                                                                                       |
|File getCacheDir()                                                                     |
| - Returns a handle to a directory you can use specifically for storing cache files.   |
|   You should take care to keep this directory tidy and use as little space as possible|

如果您正在存储只有当前应用程序需要使用的文件,那么这些方法正是您所需的。
另一方面,如果您需要另一个应用程序写入这些文件,那么您就没有办法了:虽然有一个Context.MODE_WORLD_READABLE标志可以传递给openFileOutput(String, int),但它已经被弃用,而在新设备上对其影响并不完全可靠。 如果您希望存储要与其他应用程序共享或从其他应用程序接收的文件(例如存储的图片等文件),则需要将它们存储在外部存储器中。
外部存储器有两种类型:主存储器和其他所有存储器。 所有Android设备都至少有一个外部存储器位置:主位置,位于由Environment.getExternalStorageDirectory()返回的文件夹中。 这可能是SD卡,但如今更常见的是集成到设备本身中。 一些设备可能会有额外的外部存储器。 那将属于“其他所有存储器”。
Context还提供了许多用于获取外部存储器的方法。 这些方法提供了轻松访问主要外部存储器的方法,并且有点容易访问其他所有存储器的方法。 所有这些方法也都将文件存储在公开可用的地方,因此请小心使用。
Context中的外部文件和目录方法
| Method                                                                                |
| --------------------------------------------------------------------------------------|
|File getExternalCacheDir()                                                             |
| - Returns a handle to a cache folder in primary external storage. Treat it like you do|
|   getCacheDir(), except a little more carefully. Android is even less likely to clean |
|   up this folder than the private storage one.                                        |
|                                                                                       |
|File[] getExternalCacheDirs()                                                          |
| - Returns cache folders for multiple external storage locations.                      |
|                                                                                       |
|File getExternalFilesDir(String)                                                       |
| - Returns a handle to a folder on primary external storage in which to store regular  |
|   files. If you pass in a type String, you can access a specific subfolder dedicated  |
|   to a particular type of content. Type constants are defined in Environment, where   |
|   they are prefixed with DIRECTORY_.                                                  |
|   For example, pictures go in Environment.DIRECTORY_PICTURES.                         |
|                                                                                       |
|File[] getExternalFilesDirs(String)                                                    |
| - Same as getExternalFilesDir(String), but returns all possible file folders for the  |
|   given type.                                                                         |
|                                                                                       |
|File[] getExternalMediaDirs()                                                          |
| - Returns handles to all the external folders Android makes available for storing     |
|   media – pictures, movies, and music. What makes this different from calling         |
|   getExternalFilesDir(Environment.DIRECTORY_PICTURES) is that the media scanner       |
|   automatically scans this folder. The media scanner makes files available to         |
|   applications that play music, or browse movies and photos, so anything that you     |
|   put in a folder returned by getExternalMediaDirs() will automatically appear in     |
|   those apps.                                                                         |

从技术上讲,上面提供的外部文件夹可能不可用,因为一些设备使用可移动SD卡作为外部存储。实际上,这很少是一个问题,因为几乎所有现代设备都有非可拆卸的内部存储器用于其“外部”存储。因此,没有必要极力解决它。但我们建议包含简单的代码以防止可能性,您马上就会做到。

外部存储权限

通常情况下,您需要获得一个权限才能读写外部存储。权限是您在清单中使用<uses-permission>标签放置的众所周知的字符串值。它们告诉Android您想要做某些事情,而Android希望您请求权限。

在这里,Android希望您请求权限,因为它想执行一些问责制。您告诉Android您需要访问外部存储,然后当用户尝试安装应用程序时,Android会告诉用户这是您的应用程序要做的事情之一。这样,当您开始将东西保存到他们的SD卡上时,没有人会感到惊讶。

在Android 4.4 KitKat中,他们放松了这个限制。由于Context.getExternalFilesDir(String)返回一个特定于您的应用程序的文件夹,因此您希望能够读写位于那里的文件。因此,在Android 4.4(API 19)及以上版本中,您无需为此文件夹获得此权限。(但您仍然需要其他类型的外部存储权限。)

在清单中添加一行请求读取外部存储权限的代码,但仅适用于API 16.5之前。(AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.bignerdranch.android.criminalintent" >
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
         android:maxSdkVersion="18" />

maxSdkVersion属性可以使您的应用仅在Android版本早于API 19(Android KitKat)上请求此权限。请注意,您只需要请求读取外部存储的权限。还有一个WRITE_EXTERNAL_STORAGE权限,但您不需要它。您不会向外部存储写入任何内容:相机应用程序将为您完成。

第二个资源是这个链接 ,您可以阅读其中的全部内容,但也可以跳转到使用外部存储一节。

参考文献:

更多阅读材料:

免责声明:此信息是从Android Programming: The Big Nerd Ranch Guide中获得的,经过作者许可。有关此书的更多信息或购买,请访问bignerdranch.com。


2

这个话题有点老了,但我在寻找一种解决方案,在一些研究后,我得到了下面的代码来检索可用的“外部”挂载点列表,根据我的知识,它适用于许多不同的设备。

基本上,它读取可用的挂载点,过滤掉无效的挂载点,测试剩余的挂载点是否可访问,并在所有条件都满足时添加它们。

当然,在调用代码之前必须授予所需的权限。

// Notice: FileSystemDevice is just my own wrapper class. Feel free to replace it with your own. 

private List<FileSystemDevice> getDevices() {

    List<FileSystemDevice> devices = new ArrayList<>();

    // Add default external storage if available.
    File sdCardFromSystem = null;
    switch(Environment.getExternalStorageState()) {
        case Environment.MEDIA_MOUNTED:
        case Environment.MEDIA_MOUNTED_READ_ONLY:
        case Environment.MEDIA_SHARED:
            sdCardFromSystem = Environment.getExternalStorageDirectory();
            break;
    }

    if (sdCardFromSystem != null) {
        devices.add(new FileSystemDevice(sdCardFromSystem));
    }

    // Read /proc/mounts and add all mount points that are available
    // and are not "special". Also, check if the default external storage
    // is not contained inside the mount point. 
    try {
        FileInputStream fs = new FileInputStream("/proc/mounts");
        String mounts = IOUtils.toString(fs, "UTF-8");
        for(String line : mounts.split("\n")) {
            String[] parts = line.split(" ");

            // parts[0] - mount type
            // parts[1] - mount point
            if (parts.length > 1) {
                try {

                    // Skip "special" mount points and mount points that can be accessed
                    // directly by Android's functions. 
                    if (parts[0].equals("proc")) { continue; }
                    if (parts[0].equals("rootfs")) { continue; }
                    if (parts[0].equals("devpts")) { continue; }
                    if (parts[0].equals("none")) { continue; }
                    if (parts[0].equals("sysfs")) { continue; }
                    if (parts[0].equals("selinuxfs")) { continue; }
                    if (parts[0].equals("debugfs")) { continue; }
                    if (parts[0].equals("tmpfs")) { continue; }
                    if (parts[1].equals(Environment.getRootDirectory().getAbsolutePath())) { continue; }
                    if (parts[1].equals(Environment.getDataDirectory().getAbsolutePath())) { continue; }
                    if (parts[1].equals(Environment.getExternalStorageDirectory().getAbsolutePath())) { continue; }

                    // Verify that the mount point is accessible by listing its content. 
                    File file = new File(parts[1]);
                    if (file.listFiles() != null) {
                        try {

                            // Get canonical path for case it's just symlink to another mount point.
                            String devPath = file.getCanonicalPath();

                            for(FileSystemDevice device : devices) {

                                if (!devices.contains(devPath)) {                        
                                    devices.add(new FileSystemDevice(new File(devPath)));
                                }

                            }
                        } catch (Exception e) {
                            // Silently skip the exception as it can only occur if the mount point is not valid. 
                            e.printStackTrace();
                        }
                    }
                } catch (Exception e) {
                    // Silently skip the exception as it can only occur if the mount point is not valid. 
                    e.printStackTrace();
                }
            }
        }

        fs.close();
    } catch (FileNotFoundException e) {
        // Silently skip the exception as it can only occur if the /proc/mounts file is unavailable. 
        // Possibly, another detection method can be called here.
        e.printStackTrace();
    } catch (IOException e) {
        // Silently skip the exception as it can only occur if the /proc/mounts file is unavailable.
        // Possibly, another detection method can be called here.
        e.printStackTrace();            
    }

    return devices;
}

-1

这里有一种在外部存储器中创建新文件的方法(如果设备上存在SD卡,则为SD卡,否则为设备外部存储器)。只需将“foldername”替换为您所需的目标文件夹的名称,“filename”替换为您要保存的文件的名称。当然,在这里您可以看到如何保存通用文件,现在您可以搜索如何将图像也许在这里或其他任何内容保存到文件中。

try {
            File dir =  new File(Environment.getExternalStorageDirectory() + "/foldername/");
            if (!dir.exists()){
                dir.mkdirs();
            }
            File sdCardFile = new File(Environment.getExternalStorageDirectory() + "/foldername/" + fileName );
            int num = 1;
            String fileNameAux = fileName;
            while (sdCardFile.exists()){
                fileNameAux = fileName+"_"+num;
                sdCardFile = new File(Environment.getExternalStorageDirectory() + "/foldername/" + fileNameAux);
                num++;
            }

这也控制文件是否存在,并在新文件的名称末尾添加一个数字以保存它。
希望对你有所帮助!
编辑:抱歉,我忘记了你必须在清单中要求<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />(或者如果你更喜欢从Marshmallow开始,可以通过编程方式实现)。

-3

对于低于Marshmallow版本的设备,您可以直接在清单中授予权限。

但是对于Marshmallow及以上版本的设备,您需要在运行时授予权限。

通过使用

Environment.getExternalStorageDirectory();

您可以直接访问外部SD卡(已挂载的)。 希望这能帮到您。


Environment.getExternalStorageDirectory();会给你设备内部SD卡的路径。 - Vektor88
现在从Android Lollipop开始,您有两个选项,可以将外部SD卡用作内部存储或像典型的SD卡一样使用,这也取决于该选择。 - Geet Choubey
请尝试这个链接: http://stackoverflow.com/questions/15744064/cant-check-if-file-on-sdcard-exists/15744282#comment22381234_15744282 - Geet Choubey
2
那段代码给我提供了外部SD卡的路径。然后呢?我还是不能往上面写入数据。 - Vektor88
当您尝试写入外部SD卡时,您遇到了什么错误? - Geet Choubey
Vektor88,尽管以上所有贡献,但它们实际上都没有解决您在Android 6或更高版本中写入外部可移动SD卡的问题。您是否成功解决了这个问题? - Theo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接