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

Sot*_*nas 16 java android file

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() + ".");
}
Run Code Online (Sandbox Code Playgroud)

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

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

那么,您对为什么发生此问题有任何想法吗?

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

有时“文档”文件夹的路径是“ / storage / emulated / 0 / Documents”,有时又变成同一物理设备上的其他文件吗?

我是一位经验丰富的Android开发人员,但在Android体系结构和Android文件系统方面还是一个新手。可能是在启动时(设备开机或重新启动后)文件系统尚未在我的代码检查目录是否存在时“挂载”“磁盘”吗?在这里,我尽可能宽松地使用术语“安装”和“磁盘”。另外,我的应用程序实际上是启动器/家长控制应用程序,因此这是设备启动时首先触发的内容。我几乎确信这根本没有道理,但在这一点上,我试图查看更广阔的前景,并探索超越典型Android开发的解决方案。

在这个问题开始困扰我的时候,我将非常感谢您的帮助。

期待任何有用的回应。

提前致谢。

编辑(27/08/2019):

我遇到了这个Java Bug报告,尽管它已经过时了。据此,在NFS挂接的卷java.io.File.exists上进行操作时,最终会执行stat(2)。如果stat失败(可能由于多种原因而导致失败),则File.exists(错误地)认为该文件stat'ed不存在。这可能是我麻烦的根源吗?

编辑(28/08/2019):

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

再次感谢大家。对于我来说,即使答案过于技术性并且涉及对NFS文件系统,Java,Android,Linux或任何其他内容的更深入了解,对我来说也非常重要!

编辑(30/08/2019):

因为有些用户建议用File.exists()其他方法代替,所以我想指出,我对此感兴趣的是低估了该方法失败的原因而不是一种可以替代的解决方法。

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

为了使事情更加清楚,我将提供两段代码。因此,我在活动中使用的代码和调用的方法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();        
}
Run Code Online (Sandbox Code Playgroud)

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

RealmConfiguration.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;
        }
Run Code Online (Sandbox Code Playgroud)

因此,myFile存在于我的活动中,调用了Realm代码,突然myFile不再存在。.我再次指出这是不一致的。我注意到崩溃的发生率为4-5%,这意味着myFile在大多数情况下都存在于活动中,并且在领域代码进行检查时都存在。

我希望这会有所帮助。

再次感谢!

Ste*_*n C 9

首先,如果您使用的是Android,则Java Bugs数据库中的错误报告不相关。Android不使用Sun / Oracle代码库。Android最初是对Java类库的无尘室重新实现。

因此,如果File.exists()Android中存在错误,则这些错误将存在于Android代码库中,而所有报告都将存在于Android问题跟踪器中。

但是当你这样说:

据此,在对NFS挂载的卷进行操作时,java.io.File.exists最终将执行stat(2)。如果统计信息失败(可能由于多种原因而导致),那么File.exists(错误地)认为正在统计的文件不存在。

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

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

是的,它可以,而不仅限于NFS!见下文。(使用Files.existNFS stat失败很可能是EIO,并且将引发IOException而不是返回false。)


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;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,它是如何将其ErrnoException变成的false

进一步的挖掘表明os.access调用正在执行本机调用,该access调用将进行syscall,并ErrnoException在syscall失败时引发。

因此,现在我们需要查看访问系统调用的已记录行为。内容如下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已请求对正在执行的可执行文件进行写访问。

我已经剔除了我认为在技术上不可能或难以置信的错误,但是仍然有很少的问题需要考虑。


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

但是我不认为这File.exist()坏了1,或者说主机操作系统坏了。从理论上讲这是可能的,但是您需要一些明确的证据来支持该理论。

1-就其在行为上与该方法的已知行为没有不同的意义而言,它没有被破坏。您可能会争论不休,直到人们意识到行为是否“正确”为止,但是从Java 1.0开始就一直如此,并且不能在OpenJDK或Android中更改它,而不会破坏过去20多年来编写的数千个现有应用程序年份。不会发生的


接下来做什么?

好吧,我的建议是用来strace跟踪您的应用程序正在执行的系统调用,并查看您是否可以从中获得一些线索,以了解为什么某些access系统调用会给您带来意想不到的结果。例如,路径是什么,路径是什么errno。请参阅https://source.android.com/devices/tech/debug/strace