【问题标题】:File.exists() returns false for file (directory) that actually existsFile.exists() 为实际存在的文件(目录)返回 false
【发布时间】:2019-12-30 11:29:06
【问题描述】:

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%。
  • 使用上面的代码,我试图创建一个指向“文档”文件夹的文件。在我的应用程序中,我实际上使用的是String myPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getPath();
    在发生错误的特定设备中,此路径恰好是“/storage/emulated/0/Documents”,这就是我在给你的示例代码中硬编码它的原因
  • 如果我在设备上使用文件资源管理器应用程序(即“Astro 文件管理器”,我可以看到该文件夹​​确实存在并且有一些内容,并且还确认路径真的 是“/storage/emulated/0/Documents”。
  • 这在我本地从未发生过。只有应用程序的用户会遇到此问题,并且由于 Firebase/Crashlytics,我知道问题的存在。用户拥有与我用于开发的完全相同的平板电脑,即联想 TB-8504X。 (我在一家公司工作,我们提供软件和硬件)。

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

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

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

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

非常感谢您的帮助,因为这个问题开始让我感到不安。

期待任何有用的回复。

提前致谢。

编辑(2019 年 8 月 27 日):

我遇到了这个Java Bug Report,虽然它已经过时了。据此,在 NFS 挂载的卷上操作时,java.io.File.exists 最终会执行stat(2)。如果stat 失败(它可能由于多种原因而失败),那么File.exists(错误地)假定stat'ed 的文件不存在。难道这就是我烦恼的根源吗?

编辑(2019 年 8 月 28 日):

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

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

编辑(2019 年 8 月 30 日):

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

即使我想用其他东西替换File.exists()我也无法这样做,因为这段代码位于RealmConfiguration.java 文件中(阅读-only) 这是我在我的应用程序中使用的领域库的一部分。

为了让事情更清楚,我将提供两段代码。我在活动中使用的代码以及在 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 存在,并且驻留在 RealmConfiguration.java 中的代码被调用。

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;
        }

所以,myFile 存在于我的活动中,调用了 Realm 代码,突然 myFile 不再存在。我再次指出,这是不一致的。我注意到崩溃率为 4-5%,这意味着 myFile 大部分时间都存在于活动中以及领域代码对其进行检查时。

我希望这会有所帮助。

再次提前致谢!

【问题讨论】:

  • 我将完全删除 exists() 测试并留下 mkdirs() 通话。并忽略它是成功还是失败。然后,如果和何时失败,打开文件的实际尝试会给您一个异常并带有有意义的错误消息。在你得到它并在这里发布之前,我们都只是猜测。
  • 我投票决定将此问题作为题外话结束,因为该解决方案需要产品支持,而不是计算机编程。
  • 为了说服领域开发团队用其他东西替换exists(),我必须首先确认这是问题所在。我的问题与exists() 行为有关,这肯定是关于计算机编程,尤其是Java。如果exists() 不可靠,则应更新 Java 以解决此问题,或者至少提及此不可靠行为,以便开发人员避免使用它或谨慎使用它,包括 Realm 开发团队。但再一次,我不能 100% 确定这是问题的根源。也许我错过了其他东西。因此我提出了一个问题......
  • 首先,我不会从任何地方删除exists;该方法的存在是有原因的。其次,这是一个有效的编程问题,与“联系供应商”无关。 The Java SDK for Realm is open source,因此您实际上可以查看它并根据需要分叉它(专有的是数据库本身)。 OP 提出了合理的担忧。我会向 RealmDB 的人指出这一点(他们经常阅读 SO,并且以回答问题而闻名,或者在 2015 年使用它时已经习惯了)。祝你好运。
  • 至于为什么会出现这种行为,好吧,我不确定,但是 RealmDB 以在主线程上做很多事情而闻名,因为它很快,所以也许这是一种罕见的情况有时文件系统足够慢以至于您遇到这种奇怪行为的情况;这只是一个理论。我已经有几年没有接触过 realmdb 了;如果您查看您链接的文件,他们清楚地说:警告:此方法只是一个时间点检查。除非受到外部同步的保护,否则其他线程或进程可能在此方法返回后立即创建或删除了 Realm 文件。

标签: java android file


【解决方案1】:

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

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

但是当你这样说的时候:

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

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

这会是我烦恼的根源吗?

是的,它可以,而且不仅仅是在 NFS 案例中!见下文。 (对于Files.exist,NFS 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;
    }
}

注意它如何将任何ErrnoException 转换为false

更多挖掘表明 os.access 调用正在执行本机调用,该调用发出 access 系统调用,如果系统调用失败则抛出 ErrnoException

所以现在我们需要查看访问系统调用的记录行为。 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 可用内核内存不足。

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

我已经删除了我认为在技术上不可能或不可信的错误,但仍然很少考虑。


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

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

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


接下来要做什么?

我的建议是使用strace 来跟踪您的应用程序正在进行的系统调用,看看您是否可以得到一些线索,说明为什么某些access 系统调用会给您带来意想不到的结果;例如路径是什么,errno 是什么。见https://source.android.com/devices/tech/debug/strace

【讨论】:

    【解决方案2】:

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

    解决方法是改用java.nio.Files.exists()

    【讨论】:

    • 感谢您的输入伙伴!我可以确认有问题的设备上没有运行防病毒软件。即使它存在并且限制了我的访问,我认为它每次都会这样做。就我而言,我提供的示例代码的失败率为 4-5%。其余时间它工作。
    • 不,根据我的经验和一些内部测试(强制该行为发生),当 AVG 扫描文件夹时,它的所有祖先都被锁定。我不记得失败率,但大约是 5 纳秒的窗口,所有请求都失败了(200 个线程在循环中执行exists(),失败时输出到 System.err)。替换“修复”的问题,
    • 我明白了。然而,有问题的设备没有安装防病毒软件,所以我无法想象这有什么关系。在这一点上,我想再说一次,我不是在寻找 File.exists() 的替代品。此方法由我使用的一些Realm 代码调用,我无法更改它。我想了解File.exists() 返回错误响应的条件。所以你的答案,研究杀毒软件对File.exists() 执行的可能影响,肯定是在正确的轨道上,但遗憾的是与我的问题无关。
    猜你喜欢
    • 2017-08-16
    • 2011-07-23
    • 2010-10-29
    • 2012-10-22
    • 1970-01-01
    • 2015-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多