File.exists() 为实际存在的文件(目录)返回 false

2022-09-02 13:49:07

TLDR:File.exists()是错误的,我想知道为什么!


我在我的Android应用程序中遇到了一个奇怪的问题(经常发生)。我将尽量简短。

首先,我将向您展示代码,然后提供一些其他信息。这不是完整的代码。只是问题的核心。

示例代码:

String myPath = "/storage/emulated/0/Documents";
File directory= new File(myPath);
if (!directory.exists() && !directory.mkdirs()) {
   throw new IllegalArgumentException("Could not create the specified directory: " + directory.getAbsolutePath() + ".");
}

大多数情况下,这工作正常。但是,有几次会引发异常,这意味着该目录不存在无法创建。在每100次运行中,它在95-96次上工作正常,失败4-5次。

  • 我已经在清单中声明了存储/读取外部存储/写入外部存储的权限,并在运行时请求权限。问题不在那里。(如果有的话,我现在有太多的权限:D)。毕竟,如果这是一个权限问题,它每次都会失败,但在我的情况下,它以4%或5%的速率失败。
  • 使用上面的代码,我正在尝试创建一个指向“文档”文件夹的文件。在我的应用程序中,我实际上正在使用
    在发生错误的特定设备中,此路径恰好是“/存储/模拟/0/文档”,这就是为什么我在我给你的示例代码中硬编码它的原因。String myPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getPath();
  • 如果我在设备上使用文件浏览器应用程序(即“Astro文件管理器”),我可以看到该文件夹确实存在并具有一些内容,并且还确认路径确实是“/storage/emulated/0/Documents”。
  • 这在当地从未发生在我身上。只有该应用程序的用户遇到此问题,并且我知道由于Firebase / Crashlytics,该问题仍然存在。用户拥有与我用于开发的平板电脑完全相同的平板电脑,即联想TB-8504X。(我为一家公司工作,我们提供软件和硬件)。

那么,您对为什么会出现此问题有任何想法吗?

有没有人经历过类似的事情?

“文档”文件夹的路径有时是“/存储/模拟/0/文档”,有时成为同一物理设备上的其他内容吗?

我是一个有经验的Android开发人员,但我在Android架构和Android文件系统方面是相当新手。可能是在启动时(当设备打开电源或重新启动后),文件系统尚未“挂载”“磁盘”,而我的代码会检查目录是否存在?在这里,我尽可能松散地使用术语“装载”和“磁盘”。此外,我的应用程序实际上是一个启动器/家长控制应用程序,因此它是设备启动时触发的第一件事。我几乎认为这根本没有意义,但在这一点上,我试图看到更大的图景,并探索超越典型Android开发的解决方案。

我真的非常感谢你的帮助,因为这个问题开始让我紧张。

期待任何有用的回复。

提前致谢。

编辑 (27/08/2019) :

我遇到了这个Java错误报告,尽管它已经过时了。据此,在 NFS 装载的卷上运行时,最终会执行 .如果失败(它可能由于多种原因而失败),则(错误地)假定文件不存在。这可能是我烦恼的根源吗?java.io.File.existsstat(2)statFile.existsstat'ed

编辑 (28/08/2019) :

今天,我能够为这个问题添加一个赏金,以试图引起更多的关注。我鼓励你仔细阅读这个问题,仔细阅读评论,忽略那些声称这与Realm的客户支持有关的评论。领域代码确实是使用不可靠方法的代码,但我想知道的是为什么该方法不可靠。Realm 是否能够解决这个问题,而是使用其他代码,这超出了问题的范围。我只是想知道是否可以安全地使用,如果没有,为什么File.exists()

再次,提前感谢大家。对我来说,得到一个答案真的很重要,即使它过于技术性,并且涉及对NFS文件系统,Java,Android,Linux或其他任何东西的更深入理解!

编辑 (30/08/2019) :

因为有些用户建议用其他方法替换,所以我想声明,在这一点上,我感兴趣的是低估了该方法失败的原因,而不是可以使用什么作为解决方法。File.exists()

即使我想用其他东西替换,我也无法做到这一点,因为这段代码驻留在文件(只读)中,这是我在应用程序中使用的 Realm 库的一部分。File.exists()RealmConfiguration.java

为了使事情更加清晰,我将提供两段代码。我在活动中使用的代码以及因此被调用的方法:RealmConfiguration.java

我在活动中使用的代码:

File myfile = new File("/storage/emulated/0/Documents");
if(myFile.exists()){        //<---- Notice that myFile exists at this point.        
   Realm.init(this);

   config = new RealmConfiguration.Builder()
   .name(".TheDatabaseName")
   .directory(myFile)       //<---- Notice this line of code.
   .schemaVersion(7)
   .migration(new MyMigration())
   .build();

   Realm.setDefaultConfiguration(config);
   realm = Realm.getDefaultInstance();        
}

此时,myFile 存在,并且调用了驻留在 get 中的代码。RealmConfiguration.java

领域配置.java方法崩溃:

    /**
         * Specifies the directory where the Realm file will be saved. The default value is {@code context.getFilesDir()}.
         * If the directory does not exist, it will be created.
         *
         * @param directory the directory to save the Realm file in. Directory must be writable.
         * @throws IllegalArgumentException if {@code directory} is null, not writable or a file.
         */
        public Builder directory(File directory) {
            //noinspection ConstantConditions
            if (directory == null) {
                throw new IllegalArgumentException("Non-null 'dir' required.");
            }
            if (directory.isFile()) {
                throw new IllegalArgumentException("'dir' is a file, not a directory: " + directory.getAbsolutePath() + ".");
            }
------>     if (!directory.exists() && !directory.mkdirs()) {   //<---- Here is the problem
                throw new IllegalArgumentException("Could not create the specified directory: " + directory.getAbsolutePath() + ".");
            }
            if (!directory.canWrite()) {
                throw new IllegalArgumentException("Realm directory is not writable: " + directory.getAbsolutePath() + ".");
            }
            this.directory = directory;
            return this;
        }

因此,myFile存在于我的活动中,Realm代码被调用,突然myFile不再存在。我想再次指出,这是不一致的。我注意到崩溃的速度为4-5%,这意味着大多数时候myFile都存在于活动中,并且当领域代码进行检查时。

我希望这将是有帮助的。

再次提前致谢!


答案 1

首先,如果您使用的是Android,则Java Bugs数据库中的错误报告不相关。Android不使用Sun / Oracle代码库。Android最初是作为Java类库的洁净室重新实现而开始的。

因此,如果Android上存在错误,则错误将位于Android代码库中,并且任何报告都将位于Android问题跟踪器中。File.exists()

但是当你说这句话时:

据此,当在 NFS 挂载的卷上运行时, java.io.File.exist 最终会执行一个 stat(2)。如果统计信息失败(它可能由于多种原因而失败),则(错误地)假定正在统计的文件不存在。File.exists

  1. 除非您使用的是 NFS,否则该错误报告并不直接相关。
  2. 这不是一个错误/错误。这是一个限制。
  3. 在文件系统级别,Linux支持许多不同类型的文件系统,并且其中许多文件系统以意想不到的方式运行,这是生活中的一个事实......与“普通”文件系统相比。JVM 不可能在 Java API 级别隐藏所有奇怪的文件系统特定边缘情况。
  4. 在 API 级别,无法报告任何错误。签名不允许它引发 ,并且引发未经检查的异常将是一个重大更改。它只能说是 或 .File.existsIOExceptiontruefalse
  5. 如果要区分 的各种原因,则应改用较新的方法。falseFiles.exists(Path, LinkOptions...)

这可能是我烦恼的根源吗?

是的,它可以,而不仅仅是在NFS的情况下!见下文。(使用 ,NFS 故障很可能是 一个 ,这将引发 一个 而不是返回 。Files.existstatEIOIOExceptionfalse


Android 代码库(android-4.2.2_r1 版本)中的 File.java 代码是:

public boolean exists() {
    return doAccess(F_OK);
}

private boolean doAccess(int mode) {
    try {
        return Libcore.os.access(path, mode);
    } catch (ErrnoException errnoException) {
        return false;
    }
}

请注意它如何将 any 转换为 .ErrnoExceptionfalse

进一步的挖掘显示 os.access 调用正在执行本机调用,该调用会发出系统调用,如果系统调用失败,则会抛出。accessErrnoException

因此,现在我们需要查看访问系统调用的记录行为。这是怎么说的:man 2 access

  1. F_OK文件是否存在的测试。
  2. 如果出现错误(在模式下至少有一个位要求的权限被拒绝,或者模式F_OK但文件不存在,或者发生了其他一些错误),则返回 -1,并相应地设置了 errno。
  3. 如果出现以下情况,则 access() 将失败:

    • EACCES 对文件的访问将被拒绝,或者对路径名的路径前缀中的某个目录的搜索任务被拒绝。(另见path_resolution(7))。

    • ELOOP 在解析路径名时遇到太多的符号链接。

    • ENAMETOOLONG 路径名太长。

    • ENOENT 路径名的组件不存在,或者是悬空的符号链接。

    • ENOTDIR 在路径名中用作目录的组件实际上不是目录。

    • 为只读文件系统上的文件请求了 EROFS 写入权限。

  4. 如果出现以下情况,access() 可能会失败:

    • EFAULT 路径名指向可访问地址空间之外。

    • 未正确指定 EINVAL 模式。

    • EIO 发生 I/O 错误。

    • ENOMEM 内核内存不足可用。

    • 已请求ETXTBSY对正在执行的可执行文件进行写入访问。

我已经消除了我认为在技术上不可能或不可信的错误,但仍然有相当多的错误需要考虑。


另一种可能性是某些东西(例如,应用程序的其他部分)正在删除或重命名文件或(假设的)符号链接,或更改文件权限......在你的背后。

但我不认为这是损坏的1,或者主机操作系统已损坏。这在理论上是可能的,但你需要一些明确的证据来支持这个理论。File.exist()

1 - 它不会被破坏,因为它的行为与方法的已知行为没有不同。你可以争论,直到奶牛回家,行为是否“正确”,但自Java 1.0以来,情况一直如此,如果不破坏过去20多年来编写的数千个现有应用程序,就无法在OpenJDK或Android中更改它。它不会发生。


下一步做什么?

好吧,我的建议是用于跟踪您的应用程序正在制作的系统调用,看看您是否可以获得一些线索,说明为什么某些系统调用会给您带来意想不到的结果;例如,路径是什么,路径是什么。请参阅 https://source.android.com/devices/tech/debug/stracestraceaccesserrno


答案 2

我遇到了类似的问题,但故障率更高,其中防病毒软件锁定 ,因此使任何请求失败(几乎立即)FileSystem

变通办法是改用。java.nio.Files.exists()