Android外部存储挂载正常但mkdirs失败:原因与排查全指南
2026/9/16 3:17:59 网站建设 项目流程

先说结论:在 Android 开发里,Environment.getExternalStorageState()返回MEDIA_MOUNTED,只代表系统认为外部存储设备已经挂载好、具备读写条件;它不保证你接下来对某个具体路径的mkdirs()就一定能成功。这个“状态正常但操作失败”的窗口期,我见过太多人踩过坑,尤其是做文件下载、相册备份、日志导出这类功能时,线上反馈“无法保存”的工单一上来,第一眼判断是MEDIA_MOUNTED,再一查mkdirs()返回 false,人就直接懵了。

我用几年的时间在多个项目里反复遇到这个问题,也帮团队排查过不少类似 Case。这篇文章就好好掰扯一下:为什么挂载状态对了,目录还是创建失败?这里面有哪些容易忽略的原因,以及遇到这种情况该怎么一步步排查。内容适合 Android 应用开发、SDK 开发、以及所有需要在外部存储上创建文件的技术同学,尤其建议被这类问题困扰过的人仔细看一遍。

1. 先搞清楚:MOUNTED 和 mkdirs 各自做了什么

1.1 MOUNTED 只是“存储卡状态正常”的信号

Android 里说的外部存储,是一套由 vold 守护进程管理的存储体系。无论是内置的 emulated 存储,还是插入的物理 SD 卡,系统都会在启动、插拔、格式化等时机执行挂载操作。挂载完成后,Environment.getExternalStorageState()会返回一个字符串状态,其中MEDIA_MOUNTED表示“设备已挂载且可以对文件进行读写”。

值得强调的是,这个状态是“全局存储状态”,不是“某个目录的专属授权”。它反映的是块设备层面的挂载结果,比如分区是否成功挂到某个挂载点、文件系统是否可读写。至于你这个 App 对某个路径有没有权限、路径本身合不合法、目录层级在文件系统层面能不能创建,MEDIA_MOUNTED完全管不着。

我看过很多同事写的代码,几乎都是一个固定模板:

if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { File dir = new File(Environment.getExternalStorageDirectory(), "myApp/cache"); boolean result = dir.mkdirs(); if (!result) { // 走到这里就开始一头雾水 } }

问题就出在这个假设上:认为MEDIA_MOUNTED等于“我可以在任何位置创建目录”。实际上,从系统返回挂载成功到你真正执行文件操作之间,隔着好几层检查,任何一层出问题,操作都会失败。

1.2 mkdirs 的失败,并不是只有“磁盘只读”一种原因

File.mkdirs()是 Java 层提供的文件操作 API,它的语义是“创建此抽象路径名指定的目录,包括所有必需但不存在的父目录”。注意这里有个细节:它不只创建目标目录,还会递归补全父目录。这个方法返回 boolean,true 表示创建成功或者目录已经存在,false 表示创建失败,而且它不像FileOutputStream那样会抛 IOException,失败原因非常不透明。

很多人以为 mkdirs 返回 false 就是“磁盘只读了”,其实原因可以非常多样:

  • 目标路径的某个父级组件是一个已存在的文件,而不是目录。
  • 路径中包含文件系统不允许的字符,或者路径长度超出限制。
  • 应用没有对应路径的写权限。
  • 文件系统层面报错,比如 inode 耗尽、只读错误、FUSE 守护进程异常。
  • 路径处于某个特殊挂载点下,而该挂载点对当前进程不可写。

还有一个非常隐蔽的问题:mkdirs()在某些情况下会“部分成功又整体失败”。比如它已经创建了a/b,但在创建a/b/c时失败,这个时候已经创建出来的目录不会被回滚。你下一次再调mkdirs(),如果因为某种原因它认为父路径已经存在就只尝试创建最后一级,结果可能又不一样。这就导致同样的代码,第一次失败,第二次竟然成功了,排查时非常迷惑。

1.3 “状态 + 操作”之间还有权限和策略的夹层

再往深一层说,MEDIA_MOUNTEDmkdirs()之间还存在一个看不见的夹层:应用沙箱、权限策略、SELinux、存储重定向。Android 从 6.0 开始有了运行时权限,从 10 开始有了分区存储,从 11 又进一步加强了包可见性和存储限制。这些策略的介入,让“存储状态可写”和“特定应用可写”完全变成两回事。

打个比方:一个仓库大门的“营业中”灯亮着,不代表你进入仓库后想在哪面墙上钉钉子都可以,仓库管理员、货架位置、消防通道,都是约束。MEDIA_MOUNTED就是那盏灯,mkdirs()就是在墙上钉钉子,灯亮着但钉子钉不进去,太常见了。

2. 导致目录创建失败的几类典型原因

2.1 路径本身就不对:File 对象指向的不是合法目录

我排查过很多“mkdirs 失败”的问题,第一类原因其实是最蠢的:路径写错了,或者路径里混入了不该有的符号。

常见的路径问题有这么几种:

  • 目标路径的父级是文件:比如你已经有一个文件/sdcard/test,然后又想创建/sdcard/test/subdir。系统无法在“文件”下面再创建目录,mkdirs()直接返回 false。这种情况最容易发生在下载文件名和目录名冲突的场景。
  • 路径末尾带了空格或特殊字符:FAT32 和 exFAT 的限制会比 ext4 严格,某些字符比如*?<>|在 Windows 风格的文件系统里是非法字符,在 Android 的 FUSE 层经过透传后,也可能会创建失败。
  • 路径长度超限:虽然 Android 的底层 Linux 支持很长的路径,但单层目录名通常被限制在 255 字节,整个路径在部分文件系统上也不能超过 4096 字节。一些 App 喜欢把时间戳、会话ID、用户ID拼到目录名里,拼着拼着就超了。
  • 根路径用错:有些代码写死/sdcard/xxx,在某些定制 ROM 上/sdcard指向的可能是/data/media/0,但实际的挂载路径是/storage/emulated/0。直接写死路径,很容易在厂商定制机上踩到挂载点不一致的问题。

这些路径问题,MEDIA_MOUNTED是看不出来的。状态只是告诉你存储设备可用,不会告诉你目标路径的层级结构是否合法。

2.2 权限问题:静态权限和运行时权限都没到位

权限问题是“挂载正常但创建失败”最常见的原因,而且分为好几个层次。

如果你做的是 Android 6.0 以下的老项目,只要在 Manifest 里声明了WRITE_EXTERNAL_STORAGE就行。但现在的应用基本都要适配到 API 23 以上,这时候WRITE_EXTERNAL_STORAGE属于危险权限,必须在运行时动态申请。要是只在 Manifest 里声明,而没有在代码里请求权限,那么在 Android 6.0+ 设备上,写外部存储的路径时mkdirs()就会静默失败。

更麻烦的情况是“权限申请了,但实际被拒绝”。现在很多手机系统(尤其是国产 ROM)对权限管理做了深度定制。用户可能在系统设置里关闭了“存储权限”,或者选择了“仅在使用中允许”,又或者 App 退到后台后权限被系统自动回收。这些情况下,checkSelfPermission返回的可能是 granted,但真正执行写操作时依然会被底层拒绝,因为系统在 FUSE 层做了更细的管控。

还有一种情况是“权限被授予,但是授予给了错的东西”。比如申请的是READ_EXTERNAL_STORAGE,写目录时却需要WRITE_EXTERNAL_STORAGE;又比如因为 targetSdkVersion 太高,系统已经默认开启分区存储,直接写公共目录本来就不该用WRITE_EXTERNAL_STORAGE,而是应该走 MediaStore。在这个前提下,即便你有权限,用File直接创建公共目录也可能被限制。

2.3 存储空间或文件系统问题:容量、inode、只读挂载

设备本身也可能有问题,这类问题属于“环境故障”,不是代码逻辑问题,但会让你误以为是代码问题。

  • 存储空间不足:当分区剩余空间为 0,或者可用 inode 数量为 0 时,创建目录会失败。inode 是什么?简单理解就是文件系统用来记录文件和目录元数据的“索引卡”,目录本身也要占用一个 inode。如果一个分区里文件数量极多,哪怕剩余空间看着还有几百 MB,也可能因为 inode 耗尽而无法创建新的目录。这种情况在一些低端机上特别常见,App 频繁写小文件,最终把 inode 消耗殆尽。
  • 文件系统只读:虽然系统状态显示为MEDIA_MOUNTED,但底层挂载参数可能是ro(read-only)。有时候是 vold 挂载时检测到文件系统异常,自动降级为只读;有时候是物理 SD 卡存在坏块,内核重新挂载成了只读。这种状态下,mkdirs()必然返回 false。
  • 文件系统损坏:常见于物理 SD 卡。FAT32/exFAT 的分区表、文件分配表出现问题后,VFS 层可能仍然能完成挂载,读操作正常,但一写就报 I/O 错误。这种情况下你在 Java 层拿到的只有 false,不会收到明确的异常信息。
  • 加密存储未解锁:一些设备支持文件级加密(FBE,File-Based Encryption)。开机后,如果用户还没解锁屏幕,某些目录(比如/data/media的某些子目录)在锁屏状态下是不可读写的,此时即使外部存储状态是 MOUNTED,你直接创建目录也很有可能会失败。

2.4 分区存储(Scoped Storage)带来的新约束

Android 10 开始,系统强制开启分区存储,Android 11 进一步收紧。分区存储的核心思想是:App 不能随意在公共目录(比如/storage/emulated/0/Pictures)里直接用 File 路径创建文件,必须通过 MediaStore API 插入媒体文件,或者只能在自己的应用专属目录(getExternalFilesDir)里自由读写。

这里有一个非常经典的坑:你的 targetSdkVersion 是 29 或 30,代码里还在用Environment.getExternalStorageDirectory() + "/MyApp"的方式创建目录。在 Android 10 以上设备上,你可能确实能看到目录创建成功(因为系统做了兼容性适配,应用专属目录会被隐式映射),但在某些厂商定制 ROM 上,这种行为就是直接失败。

更隐蔽的是,很多 SDK 会在自己的 Native 层调用mkdir系统调用,而不是 Java 层mkdirs()。分区存储的策略在 Java 层有一些兼容逻辑,但在 Native 层直接调用,可能直接收到EACCES(权限拒绝)或者EPERM(操作不允许),最终把 false 层层返回到你的回调里。

所以我一直建议:如果你的业务目标目录是公共目录,在 Android 10+ 上就老老实实用 MediaStore;如果你的业务目标是应用专属目录,用context.getExternalFilesDir()获取路径,不要在路径里硬编码Android/data/包名。这个目录在系统升级、应用卸载重装、清除数据后都可能变化,硬编码同样是 mkdirs 失败的隐患。

2.5 其他隐蔽的原因:FUSE 延迟、CDM 合法性、路径过长

除了上面几类,还有一些不那么容易想到的原因,属于“高级玩家才会踩到”的坑。

FUSE 延迟导致的状态不一致。Android 的 emulated 存储其实是通过 FUSE(Filesystem in Userspace)实现的。vold 挂载好之后,FUSE daemon 才接管请求。如果你的 App 在开机后很短的时间内就发起目录创建请求,FUSE 可能还没有完全就绪,导致mkdir请求返回ENOTCONN或者EAGAIN。我在某款车机设备上遇到过这种情况,冷启动后立刻执行文件初始化,大概率失败,等两三秒再执行就正常了。

CDM(Current Device Mount)映射问题。系统里存储状态实际上是通过 StorageManager 维护的,每个 Volume 都有自己的状态。多用户场景下,不同用户看到的/storage/emulated其实指向不同的目录,但getExternalStorageState()不会区分当前是哪个用户。如果你的 App 在“访客模式”或“多开空间”里运行,会发现自己拿到的路径和实际权限不匹配,mkdirs 也可能莫名其妙失败。

路径过长。之前提过,但还是要单独拿出来说。我遇到过日志模块拼接路径,把整个会话信息全部塞进文件名,最终总长度超过 255 字节,底层 ext4 拒绝创建。在 Java 层报的是 false,日志里什么都看不出来。

OEM 定制 ROM 的 bug。某些国产 ROM 对 FUSE 做了魔改,或者对 MediaProvider 有额外的权限控制。同样一段代码,在原生系统上能创建成功,在定制 ROM 上就失败。有一个典型的案例是某品牌手机在低电量模式下禁用文件写入,导致所有目录创建都失败,直到你打开“允许后台写入”开关才恢复。

3. 实战:如何一步步排查和解决

3.1 排查流程:从环境状态到具体操作

遇到mkdirs()失败,我的习惯是不要急着改代码,先按下面的顺序做一轮“现场取证”。

第一步:确认当前用户和权限状态

在 App 代码里打日志,把用户 ID、包名、targetSdkVersion、运行时权限结果全部打出来:

Log.e("StorageCheck", "uid=" + Process.myUid()); Log.e("StorageCheck", "targetSdk=" + getApplicationInfo().targetSdkVersion); Log.e("StorageCheck", "read=" + checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)); Log.e("StorageCheck", "write=" + checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE));

如果是在 Android 10+ 且 targetSdk 29+,那么WRITE_EXTERNAL_STORAGE可能已经没用了,别只看权限返回值。

第二步:检查存储状态的分层信息

不要只看Environment.getExternalStorageState(),还应该用StorageManager获取更详细的卷信息:

StorageManager sm = getSystemService(StorageManager.class); StorageVolume volume = sm.getStorageVolume(new File(path)); Log.e("StorageCheck", "state=" + volume.getState());

volume.getState()可能返回mountedmounted_rounmounted等状态。如果这里返回mounted_ro,那不管外层状态是什么,mkdirs()都注定失败。

第三步:检查目标路径的父级结构

用 shell 命令确认一下路径的实际类型:

adb shell ls -ld /storage/emulated/0/MyApp

如果ls显示这是一个文件而不是目录,那么mkdirs()失败的原因不言自明。还可以用stat命令查看权限位和 SELinux 标签:

adb shell stat /storage/emulated/0

SELinux 标签如果是u:object_r:media_rw_file:s0,说明是存储相关上下文,一般可写;如果标签异常,可能就是 SELinux 拦截了写入。

第四步:用 adb 直接复现写操作

在命令行里直接尝试向目标路径写入一个测试文件或目录:

adb shell mkdir /storage/emulated/0/TestDir adb shell touch /storage/emulated/0/TestFile

如果命令行里也失败,说明问题出在系统层或文件系统层,不是 App 层;如果命令行成功,但 App 里失败,那就是权限或策略问题,重点检查targetSdkVersion和分区存储适配情况。

第五步:抓取底层日志

logcat里可能没有直接报错的细节,但dmesglogcat -b system里经常有 vold、fuse、keystore 的错误信息。比如当你看到类似:

E vold: Failed to create dir /storage/emulated/0/xxx: Read-only file system

那就说明是只读挂载,不是代码问题。看到:

E FuseDaemon: Permission denied

说明是权限被底层拒绝。日志虽然难啃,但很多问题的真正原因都在里面。

3.2 一个更稳的目录创建工具类

排查再多,最终还是要落到代码层面。下面我给一个兼容性更好、日志更完整的目录创建工具,核心思路是:先做多级检查,再执行创建,失败后自动展开定位是哪一层出问题。

public static boolean ensureExternalDir(Context context, File dir) { if (dir == null) { Log.e("StorageUtil", "dir is null"); return false; } // 1. 检查外部存储状态 String state = Environment.getExternalStorageState(); if (!Environment.MEDIA_MOUNTED.equals(state)) { Log.e("StorageUtil", "external storage not mounted, state=" + state); return false; } // 2. 检查运行时权限(仅针对需要权限的场景) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { Log.e("StorageUtil", "no write permission"); return false; } } // 3. 目录已存在且是目录,直接成功 if (dir.exists()) { if (dir.isDirectory()) { return true; } Log.e("StorageUtil", "target exists but is a file: " + dir.getAbsolutePath()); return false; } // 4. 检查父目录是否存在,如果父目录是文件则无法创建 File parent = dir.getParentFile(); if (parent != null && parent.exists() && !parent.isDirectory()) { Log.e("StorageUtil", "parent exists but is not dir: " + parent.getAbsolutePath()); return false; } // 5. 尝试创建目录 boolean result = dir.mkdirs(); Log.e("StorageUtil", "mkdirs result=" + result + " path=" + dir.getAbsolutePath()); if (!result) { // 尝试创建失败后,检测到底哪一级失败了 File cur = dir; while (cur != null && !cur.exists()) { cur = cur.getParentFile(); } Log.e("StorageUtil", "first existing ancestor: " + (cur == null ? "null" : cur.getAbsolutePath())); } return result; }

这个工具类好在哪?第一,它把“存储状态”“权限”“父级冲突”“层级检查”全部分开,失败时能快速定位。第二,它在失败后会回溯找出第一个存在的祖先目录,这个信息在定位问题时非常有用。比如你发现第一个存在的祖先目录是/storage/emulated/0/Pictures,而你希望创建的目录是/storage/emulated/0/Pictures/foo/bar,现在foo创建失败,那大概率是权限或策略因素,而不是路径写错。

多说一句:不要在业务代码里到处直接调mkdirs()就把结果当最终结论。加个 try-catch 不现实,因为这个方法不抛受检异常,但你可以把失败信息记到日志系统里。线上问题排查时,一句“mkdirs failed at path=xxx with firstExistingAncestor=yyy”比什么都管用。

3.3 案例复盘:某相册 App 无法保存图片

说一个真实项目里的案例,更有代入感。

有一款相册类 App,用户反馈在 Android 11 手机上“保存图片到相册”一直失败。我们第一轮排查时,代码里确实是先判断了MEDIA_MOUNTED,然后mkdirs()准备创建/storage/emulated/0/Pictures/MyApp/,然后通过 FileOutputStream 写文件。线上日志显示mkdirs()返回 true,但 write 阶段抛了FileNotFoundException (Permission denied)。这说明问题不在创建目录,而在写文件。但也有很多用户反馈mkdirs()直接返回 false,这就说明了两种情况的原因不同。

第一种情况,mkdirs()返回 true 但写文件失败,是因为 targetSdkVersion 是 29,系统默认开启了分区存储,公共目录用 File 直接写会被 MediaProvider 拦截,虽然目录能建出来,但往里面写文件没有权限。

第二种情况,mkdirs()返回 false,是因为部分厂商 ROM 对Pictures这个公共目录做了额外限制,App 在分区存储模式下尝试直接创建子目录被拒绝。最终我们的解决方案是:Android 10 以上统一改用 MediaStore 的INSERT接口保存图片,不再自己创建目录。对于非媒体类型的文件(比如导出日志、备份包),改用getExternalFilesDir()目录,彻底绕开公共目录权限问题。

这个案例说明一个道理:同一个问题表象,可能是两种完全不同的原因。不搞清楚底层机制,只盯着mkdirs()的返回值,很难真正解决问题。

4. 常见问题速查表与避坑建议

4.1 快速对照表

我把实际开发中最容易遇到的情况整理成一张表,方便你以后直接对照。

现象可能原因排查方向
MEDIA_MOUNTEDmkdirs()返回 false权限未授予(Android 6.0+)检查运行时权限,动态申请
MEDIA_MOUNTED后创建公共目录失败分区存储限制(Android 10+)改用 MediaStore 或getExternalFilesDir
物理 SD 卡上创建目录失败SD 卡损坏、只读挂载、加密锁StorageVolume.getState()查状态
目录名看起来正常但仍失败路径中隐藏空格、非法字符、长度超限打印路径字节数,逐字符检查
有时候成功有时候失败FUSE 未就绪、空间不足、inode 耗尽加重试、检查StatFs可用块数
重启后第一次运行失败FBE 加密目录未解锁等用户解锁后再执行存储相关操作
某款国产机型上必现失败OEM ROM 定制存储策略抓取logcatdmesg,针对性适配

4.2 几个我踩过的坑和心得

第一个心得:不要相信“第一次mkdirs()失败后可以马上再试一次”。我见过一些代码在mkdirs()返回 false 后,不处理失败逻辑,而是直接继续执行写文件,结果自然也是失败。更糟的是有人会在失败后硬再试一次,但这种做法偶尔会“成功”,原因在于第一次调用虽然最终返回 false,但过程中已经把父目录建好了。这种不确定行为很容易掩盖真正的问题,正确的做法是失败后立即展开诊断。

第二个心得:在日志里一定要记录路径,而且是绝对路径。我排查过太多线上问题,日志只写了“mkdir failed”,完全看不到是在哪个路径上失败。加上绝对路径后,至少能判断是公共目录还是应用专属目录,是不是写死了/sdcard这种路径。如果路径本身就有问题,日志一眼就能看出来。

第三个心得:测试机不要只用模拟器或者某一种品牌的手机。存储相关的问题特别容易在不同 ROM 上出现差异,模拟器基本测不出问题。尽量准备至少一台原生 Android 系统的设备(比如 Pixel)和一台国产主流机型,两组测试结果对比着看,很多问题立刻就清楚了。

第四个心得:Android 10 以上的公共目录操作,能用 MediaStore 就别用 File。我知道很多老项目迁移成本高,但这是大势所趋。如果你暂时不能完全迁移,也建议至少对mkdirs()失败的情况做降级处理:比如提示用户“存储权限不可用”,或者跳转系统设置里的应用存储权限页面。把失败解释清楚,比用户收到一个“保存失败”然后什么也做不了要强得多。

4.3 最后再分享一个调试小技巧

mkdirs()返回 false 的时候,除了看日志,我还特别喜欢在命令行走一遍底层逻辑。你可以用adb shell am start -a android.intent.action.VIEW -d file:///storage/emulated/0/打开系统的文件管理器,肉眼确认一下目标路径的实际情况。这个方法听起来原始,但在很多诡异问题上真的能救急。

还有一点,针对某些很隐蔽的只读问题,可以试试:

adb shell dumpsys mount | grep -A 5 "emulated"

这条命令能看到 emulated 卷的实际挂载状态。如果里面出现了read-only的关键词,那就别再折腾代码了,问题在系统层和数据层,让用户检查存储设备更实际。

写在最后,关于 MOUNTED 和 mkdirs 的关系,我的理解始终是:状态判断只是第一步,不是免死金牌。你在写任何存储逻辑的时候,心里都要有一条线——MEDIA_MOUNTED是“系统认为存储可用”,mkdirs()才是“我的应用真正获得了路径创建能力”。这两者之间隔着权限、策略、文件系统状态、设备差异,每一层都可能中断操作。所以,不要只问“为什么挂载了还会失败”,而是要问“挂载状态正常之后,我的路径、权限、时机、设备环境是不是都没问题”。把这些都排查清楚,存储相关的问题基本都能解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询