Android 存储分析/分区/挂载

270 阅读15分钟

android 设备存储信息

image.png

• **Filesystem**:文件系统的名称。

• **Size**:文件系统的总大小。

• **Used**:已经使用的空间。

• **Avail**:可用的空间。

• **Use%** :已经使用的百分比。

• **Mounted on**:文件系统挂载的挂载点。

Filesystem            Size  Used Avail Use% Mounted on

/dev/root             2.9G  1.2G  1.6G  45% /

文件系统的名称:

文件系统的名称表示每个分区或设备在系统中的标识。以下是详细解释:

/dev/root:表示根文件系统,通常存储操作系统的核心文件和目录。

文件挂载点:

文件挂载点表示文件系统在操作系统中访问的位置。以下是每个挂载点的详细解释:

/ :根目录,是整个文件系统的起点,所有其他目录都在这个目录下。

根目录 / 不是就代表所有的存储吗?

Filesystem            Size  Used Avail Use% Mounted on

/dev/root             2.9G  1.2G  1.6G  45% /

  1. 解释根目录的存储大小:

虽然根目录 (/) 是整个文件系统的起点,但它并不代表设备上所有的存储大小。根目录包含了操作系统的核心文件和目录,

但设备上的其他分区,如 /data、/cache 和 /vendor 等, 虽然在逻辑上是根目录的一部分, 但在物理存储上是独立的分区。每个分区都有自己的存储大小和用途。

  1. 详细解释各分区的关系:

根目录 (/) :包含操作系统的基本文件和目录,如 /bin、/etc、/lib 等,但并不包括其他挂载点的数据。

其他分区:/data、/cache、/vendor 等分区独立存在,并挂载在根目录下的不同路径。这些分区用于特定的目的,例如存储用户数据、缓存数据和供应商文件。

  1. 根目录与其他分区的区别:

根目录 (/) 的大小:2.9G,其中 1.2G 已使用,1.6G 可用。它只包括操作系统的基本文件和目录。

其他分区:它们的大小各不相同,例如 /data 分区有 10G,其中 3.1G 已使用,6.5G 可用。这些分区用于特定的存储目的。

结论:

根目录 (/)是文件系统的起点,它的存储大小只包括挂载点的大小,

每个分区有其独立的存储大小,并挂载在根目录的不同路径上。

设备的总存储空间是这些分区的总和,而不是根目录的大小。

为什么不同的路径进入可以到达同样的文件?怎么判断是否是同一个文件?


/storage/emulated/0
/mnt/runtime/default/emulated/0/
/sdcard/

k62v1_64_bsp:/storage/emulated/0 # df -h /storage/emulated/0/

Filesystem     Size  Used Avail Use% Mounted on

/data/media     10G  3.2G  6.5G  33% /mnt/runtime/write/emulated

k62v1_64_bsp:/storage/emulated/0 # df -h /mnt/runtime/default/emulated/0/     

Filesystem     Size  Used Avail Use% Mounted on

/data/media     10G  3.2G  6.5G  33% /mnt/runtime/write/emulated

k62v1_64_bsp:/storage/emulated/0 # df -h /sdcard/

Filesystem     Size  Used Avail Use% Mounted on

/data/media     10G  3.1G  6.5G  33% /mnt/runtime/write/emulated



方法 1:
使用 File.getCanonicalPath() 方法可以判断两个路径是否指向同一个文件或目录。

// 定义需要比较的路径
//            File file1 = new File("/storage/emulated/0");
//            File file2 = new File("/mnt/runtime/default/emulated/0");//new StatFs(path2);//出现这个错误的原因是路径 /mnt/runtime/default/emulated/0 可能需要特殊权限才能访问
//            File file3 = new File("/sdcard");
//          f1 and f2  different
//          f1 and f3 the same
//          f2 and f3 the different

            File file1 = new File("/storage/emulated/0/DCIM");
            File file2 = new File("/mnt/runtime/default/emulated/0/DCIM");
            File file3 = new File("/sdcard/DCIM");
//          f1 and f2  different
//          f1 and f3 the same
//          f2 and f3 the different

            // 获取规范路径
            String canonicalPath1 = file1.getCanonicalPath();
            String canonicalPath2 = file2.getCanonicalPath();
            String canonicalPath3 = file3.getCanonicalPath();

            // 比较规范路径
            boolean isSameFile1And2 = canonicalPath1.equals(canonicalPath2);
            boolean isSameFile1And3 = canonicalPath1.equals(canonicalPath3);
            boolean isSameFile2And3 = canonicalPath2.equals(canonicalPath3);
            

方法 2:比较文件系统信息:
使用 StatFs 类获取路径的文件系统信息,包括块大小、块总数和可用块数等。
块大小、块总数和可用块数是否相同。
不推荐。

StatFs stat2 = new StatFs(path2);//出现这个错误的原因是路径 /mnt/runtime/default/emulated/0 可能需要特殊权限才能访问
// Caused by: java.lang.IllegalArgumentException: Invalid path: /mnt/runtime/default/emulated/0
// Caused by: android.system.ErrnoException: statvfs failed: EACCES (Permission denied)
//       at libcore.io.Linux.statvfs(Native Method)
// 比较文件系统信息
    val isSameFileSystem = (stat1.blockSizeLong == stat2.blockSizeLong
            && stat1.blockSizeLong == stat3.blockSizeLong
            && stat1.blockCountLong == stat2.blockCountLong
            && stat1.blockCountLong == stat3.blockCountLong
            && stat1.availableBlocksLong == stat2.availableBlocksLong
            && stat1.availableBlocksLong == stat3.availableBlocksLong)

image.png

结论:不同位置访问同一个文件

1. 符号链接 /sdcard 可以是 /storage/emulated/0 的符号链接。

2. 挂载点的重定向

Android系统中的 /storage/emulated/0、/mnt/runtime/default/emulated/0 和 /sdcard 都指向同一个物理存储位置 /data/media/0。这通过系统的挂载点重定向机制实现,使得这些路径可以访问相同的数据。

3. 目的: 兼容性和方便性

判断方法:

推荐>>> 方法一:使用 getCanonicalPath() 方法比较规范路径 是否是同一个文件,简单,避免权限问题.

方法二:使用 StatFs 类比较文件系统信息,,,权限问题!,适用于,信息统计。

            // 定义需要比较的路径
//            File file1 = new File("/storage/emulated/0");
//            File file2 = new File("/mnt/runtime/default/emulated/0");//如果使用 StatFnew StatFs(path2);判断,会出现这个错误的原因是路径 /mnt/runtime/default/emulated/0 可能需要特殊权限才能访问
//            File file3 = new File("/sdcard");
//          f1 and f2  different
//          f1 and f3 the same
//          f2 and f3 the different

            //判断,子文件。
            File file1 = new File("/storage/emulated/0/DCIM");
            File file2 = new File("/mnt/runtime/default/emulated/0/DCIM");//特殊情况 ,权限。。。虚拟路径,用于多用户环境和权限控制
            File file3 = new File("/sdcard/DCIM");
            //  f1 and f2  different
            //  f1 and f3 the same
            //  f2 and f3 the different

            Log.i(TAG, "compareIsSamePath: path1:"+file1.getPath());// /storage/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath: path2:"+file2.getPath());// /mnt/runtime/default/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath: path3:"+file3.getPath());// /sdcard/DCIM
            // 获取规范路径
            String canonicalPath1 = file1.getCanonicalPath();
            String canonicalPath2 = file2.getCanonicalPath();
            String canonicalPath3 = file3.getCanonicalPath();
            Log.i(TAG, "compareIsSamePath1: "+canonicalPath1);//  /storage/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath2: "+canonicalPath2);//  /mnt/runtime/default/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath3: "+canonicalPath3);//  /storage/emulated/0/DCIM   <----- /sdcard/DCIMTODO 改变了。ok

            /*
             * /storage/emulated/0 -> /data/media/0
             * /sdcard -> /storage/emulated/0
             *
             * sdcardfs 文件系统: /mnt/runtime/default/emulated/0 是使用 sdcardfs 文件系统虚拟化的路径,这个路径提供了对原始路径的多用户隔离和权限控制。
             * */

            // 比较规范路径
            boolean isSameFile1And2 = canonicalPath1.equals(canonicalPath2);
            boolean isSameFile1And3 = canonicalPath1.equals(canonicalPath3);
            boolean isSameFile2And3 = canonicalPath2.equals(canonicalPath3);

怎么统计磁盘的总大小?

image.png

1个挂载点只能

如何判断 2 个分区是否,共享存储?

Filesystem            1K-blocks    Used Available Use% Mounted on

tmpfs                    945912    1420    944492   1% /dev

tmpfs                    945912       0    945912   0% /mnt

...
/dev/block/dm-2        10182948 3155920   7027028  31% /data

/data/media            10182948 3155920   7027028  31% /mnt/runtime/default/emulated

tmpfs 是一种基于内存的文件系统,通常用于存储临时文件。它的特点是数据存储在内存中,而不是物理存储设备上,因此读写速度非常快。

使用场景:

tmpfs 文件系统在 Linux 和 Android 系统中广泛用于存储临时文件,例如设备上的 /dev 和 /mnt 目录。它们用于存储设备文件和挂载点的信息。

tmpfs的存储空间和区域:2块不同。

tmpfs                    945912    1420    944492   1% /dev

tmpfs                    945912       0    945912   0% /mnt

下面这 2 个是否一样呢?

/dev/block/dm-2        10182948 3155920   7027028  31% /data

/data/media            10182948 3155920   7027028  31% /mnt/runtime/default/emulated

  • 大小相同,使用相同,怎么确定是否是同一块?

方法 1。 查看物理地址。没法查看。。。。不知道咋搞

方法 2。 分析挂载信息。

cat /proc/mounts 查看所有的挂载信息: 找到相关信息:


130|k62v1_64_bsp:/ # cat /proc/mounts | grep /data

/dev/block/dm-2 /data ext4 rw,seclabel,nosuid,nodev,noatime,noauto_da_alloc,resuid=10010,resgid=1065,errors=panic,data=ordered 0 0
...


k62v1_64_bsp:/ # cat /proc/mounts | grep /mnt/runtime/default/emulated                                                                                                        

/data/media /mnt/runtime/default/emulated sdcardfs rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid,default_normal 0 0
...


分析,字段解释:

  1. /dev/block/dm-2:设备文件,表示逻辑卷或分区。
  2. /data:挂载点目录。
  3. ext4:文件系统类型。<-------------重点。
  4. rw:读写权限,表示挂载为可读写。
  5. seclabel:启用了安全标签。
  6. nosuid:不允许设置用户ID或组ID。
  7. nodev:不解释设备文件。
  8. noatime:不更新访问时间。
  9. noauto_da_alloc:不自动分配磁盘块。
  10. resuid=10010:保留块的所有者用户ID。
  11. resgid=1065:保留块的所有者组ID。
  12. errors=panic:文件系统错误时内核会触发panic。
  13. data=ordered:文件系统元数据写入顺序,保证数据写入前元数据写入。

/mnt/runtime/default/emulated    

字段解释:

  1. /data/media:源目录。<-------------重点。
  2. /mnt/runtime/default/emulated:目标挂载点目录。
  3. sdcardfs:文件系统类型,Android上的一种虚拟文件系统,通常用于实现多用户的存储隔离。<-------------重点。
  4. rw:读写权限,表示挂载为可读写。
  5. nosuid:不允许设置用户ID或组ID。
  6. nodev:不解释设备文件。
  7. noexec:不允许执行文件。
  8. noatime:不更新访问时间。
  9. fsuid=1023:文件系统的用户ID。
  10. fsgid=1023:文件系统的组ID。
  11. gid=1015:使用的组ID。
  12. multiuser:支持多用户。
  13. mask=6:掩码,控制文件和目录的权限。
  14. derive_gid:派生组ID。
  15. default_normal:默认挂载选项。

结论

  • /mnt/runtime/default/emulated 是 共享 /data 存储的。

原因

  1. /data 存储分区,挂载在物理设备 /dev/block/dm-2 上,使用的是 ext4 文件系统

ext4 真实的存储系统。

  1. /mnt/runtime/default/emulated 基于 sdcardfs 虚拟文件系统
  2. /mnt/runtime/default/emulated 的源目录是 /data/media,是 /data 的子目录。而 /data 挂载在 /dev/block/dm-2 上 挂载出来。

提供了一个虚拟视图,通常用于多用户环境,具有特定的安全和访问控制选项。

所以: /mnt/runtime/default/emulated sdcardfs 提供虚拟化访问 /data/media,但底层数据仍由 ext4 文件系统管理。

存储空间分类

。。。后续补充。。。 /storage/emulated/0/Android/data/包名

内部存储**

Context的getFileStreamPath、getFilesDir

外部存储**

Environment类的getExternalStoragePublicDirectory::

/storage/emulated/0/

/storage/emulated/userid (如果设备支持多用户的话userid是当前的用户id,一般情况下是0)

app专有目录 /storage/emulated/userid/Android/data/packageName  (packageName是app的包名)、 /storage/emulated/userid/Android/obb/packageName

通过Context类的getExternalXXX等方法,可以把文件存放在app专有目录

其他目录

除了app专有目录外,剩下的这些目录它们都是可以被任何进程访问的(在获取对应权限后),这些目录的uid都是root,gid都是everybody。下图显示了一些目录。

代码:

public class StorageUtil {

    private static final String TAG = "StorageUtil";




    /**
     * 检查所有挂载点的存储空间并打印日志
     */
    public static void checkAllStorage() {
        // 获取所有挂载点
        Map<String, String> mountPoints = getMountPoints();
        long totalSpace = 0;
        long totalUsed = 0;

        Set<StorageInfo> setStorage = new HashSet<>();

        for (Map.Entry<String, String> entry : mountPoints.entrySet()) {
            StorageInfo storageInfo = getStorageInfo(entry.getKey());
            if (storageInfo != null) {

                if (storageInfo.totalSpace<1){// 0 的忽略
                    continue;
                }

                if (setStorage.contains(storageInfo)
                        //特殊忽略,,,有时判断,,大小会失误。
                        // Filesystem            1K-blocks    Used Available Use% Mounted on
                        // /dev/block/dm-2        10182948 3106668   7076280  31% /data
                        // /data/media            10182948 3106668   7076280  31% /mnt/runtime/default/emulated  忽略这个。防止重复计算
                        ||("/data/media".equals(entry.getValue()) && mountPoints.containsKey("/data"))){

                    Log.e(TAG, " 重复,需要忽略的:设备节点:" + entry.getValue()
                            +",挂载点: " + entry.getKey()
                            +",总存储空间: " + formatSize(storageInfo.totalSpace)
                            +",已使用存储空间: " + formatSize(storageInfo.usedSpace)
                            +",可用存储空间: " + formatSize(storageInfo.availableSpace)
                    +"已使用百分比: " + storageInfo.usedPercentage + "%");
                }else {
                    totalSpace += storageInfo.totalSpace;
                    totalUsed += storageInfo.usedSpace;
                    setStorage.add(storageInfo);
                    Log.i(TAG, "设备节点:" + entry.getValue()
                            +",挂载点: " + entry.getKey()
                            +",总存储空间: " + formatSize(storageInfo.totalSpace)
                            +",已使用存储空间: " + formatSize(storageInfo.usedSpace)
                            +",可用存储空间: " + formatSize(storageInfo.availableSpace)
                            +"已使用百分比: " + storageInfo.usedPercentage + "%");
                }
            }
        }

        // 打印总存储空间的日志
        Log.i(TAG, "总存储空间: " + formatSize(totalSpace)+"--"+totalSpace);
        Log.i(TAG, "总已使用存储空间: " + formatSize(totalUsed)+"--"+totalUsed);
        Log.i(TAG, "总已使用百分比: " + ((double) totalUsed / totalSpace * 100) + "%");
    }
    /**
     * 获取所有挂载点目录及其对应的设备节点
     * @return 挂载点目录和设备节点的映射
     * key mount 点,
     * value Filesystem
     */
    private static Map<String, String> getMountPoints() {
        Map<String, String> mountPoints = new HashMap<>();
        //不合适。修改,适配下面
//        Filesystem            Size  Used Avail Use% Mounted on
//        tmpfs                 924M  1.3M  922M   1% /dev
//        tmpfs                 924M     0  924M   0% /mnt
//这两个目录虽然都使用了 tmpfs 文件系统,并且显示的总容量相同,但它们是 独立的存储区域。这意味着它们各自有自己的内存分配,并不是共享同一块内存空间。
        try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) {
            String line;
            while ((line = br.readLine()) != null) {
                String[] fields = line.split(" ");
                if (fields.length > 1) {
                    String device = fields[0];
                    String mountPoint = fields[1];//挂载点。
                    File dir = new File(mountPoint);
                    if (dir.exists() && dir.isDirectory()) {
                        mountPoints.put(mountPoint,device);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return mountPoints;
    }
    /**
     * 获取指定挂载点的存储信息
     * @param path 挂载点路径
     * @return 存储信息对象
     */
    private static StorageInfo getStorageInfo(String path) {
        if (TextUtils.isEmpty(path))return null;
        File dir = new File(path);
        if (dir != null && dir.exists() && dir.isDirectory()) {
            StatFs stat = new StatFs(dir.getPath());

            long blockSize = stat.getBlockSizeLong();
            long totalBlocks = stat.getBlockCountLong();
            long availableBlocks = stat.getAvailableBlocksLong();

            long totalSpace = totalBlocks * blockSize;
            long availableSpace = availableBlocks * blockSize;
            long usedSpace = totalSpace - availableSpace;
            double usedPercentage = (double) usedSpace / totalSpace * 100;

            return new StorageInfo(totalSpace, availableSpace, usedSpace, usedPercentage);
        }
        return null;
    }




    /**
     * 检查存储空间占用百分比并显示Toast通知,同时打印日志
     * @param context 应用上下文
     */
    public static void checkStorage(Context context) {
        // 获取外部存储状态
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            // 获取外部存储的路径
            String path = Environment.getExternalStorageDirectory().getPath();///storage/emulated/0
            Log.i(TAG, "checkStorage: 存储路径:"+path);
            try {
                Log.i(TAG, "checkStorage: 存储路径:"+(new File(path).getCanonicalPath()));///storage/emulated/0
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            StatFs stat = new StatFs(path);

            // 获取存储块的大小(字节)
            long blockSize = stat.getBlockSizeLong();
            // 获取总的存储块数量
            long totalBlocks = stat.getBlockCountLong();
            // 获取可用的存储块数量
            long availableBlocks = stat.getAvailableBlocksLong();

            // 计算总存储空间和可用存储空间
            long totalStorage = totalBlocks * blockSize;
            long availableStorage = availableBlocks * blockSize;

            // 计算已使用的存储空间
            long usedStorage = totalStorage - availableStorage;
            double usedPercentage = (double) usedStorage / totalStorage * 100;

            // 打印日志
            Log.i(TAG, "总存储空间: " + formatSize(totalStorage));
            Log.i(TAG, "已使用存储空间: " + formatSize(usedStorage));
            Log.i(TAG, "可用存储空间: " + formatSize(availableStorage));
            Log.i(TAG, "已使用百分比: " + usedPercentage + "%");

            // 检查是否超过80%
            if (usedPercentage > 80.0) {
                // 显示Toast通知
                Toast.makeText(context, "存储空间已超过80%", Toast.LENGTH_LONG).show();
                Log.i(TAG, "checkStorage: 存储空间已超过80%");
            } else {
                Toast.makeText(context, "存储空间正常", Toast.LENGTH_LONG).show();
                Log.i(TAG, "checkStorage: 存储空间正常");
            }
        } else {
            Toast.makeText(context, "无法访问外部存储", Toast.LENGTH_LONG).show();
            Log.i(TAG, "checkStorage: 无法访问外部存储");
        }
    }
    /**
     * 获取存储空间占用百分比
     * @return 存储空间占用百分比
     */
    public static double getStorageUsagePercentage() {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String path = Environment.getExternalStorageDirectory().getPath();
            StatFs stat = new StatFs(path);

            long blockSize = stat.getBlockSizeLong();
            long totalBlocks = stat.getBlockCountLong();
            long availableBlocks = stat.getAvailableBlocksLong();

            long totalStorage = totalBlocks * blockSize;
            long availableStorage = availableBlocks * blockSize;

            long usedStorage = totalStorage - availableStorage;
            return (double) usedStorage / totalStorage * 100;
        } else {
            return -1; // 表示无法访问外部存储
        }
    }


    public static void compareIsSamePath() {
        try {
            // 定义需要比较的路径
//            File file1 = new File("/storage/emulated/0");
//            File file2 = new File("/mnt/runtime/default/emulated/0");//如果使用 StatFnew StatFs(path2);判断,会出现这个错误的原因是路径 /mnt/runtime/default/emulated/0 可能需要特殊权限才能访问
//            File file3 = new File("/sdcard");
//          f1 and f2  different
//          f1 and f3 the same
//          f2 and f3 the different

            //判断,子文件。
            File file1 = new File("/storage/emulated/0/DCIM");
            File file2 = new File("/mnt/runtime/default/emulated/0/DCIM");//特殊情况 ,权限。。。虚拟路径,用于多用户环境和权限控制
            File file3 = new File("/sdcard/DCIM");
            //  f1 and f2  different
            //  f1 and f3 the same
            //  f2 and f3 the different

            Log.i(TAG, "compareIsSamePath: path1:"+file1.getPath());// /storage/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath: path2:"+file2.getPath());// /mnt/runtime/default/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath: path3:"+file3.getPath());// /sdcard/DCIM
            // 获取规范路径
            String canonicalPath1 = file1.getCanonicalPath();
            String canonicalPath2 = file2.getCanonicalPath();
            String canonicalPath3 = file3.getCanonicalPath();
            Log.i(TAG, "compareIsSamePath1: "+canonicalPath1);//  /storage/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath2: "+canonicalPath2);//  /mnt/runtime/default/emulated/0/DCIM
            Log.i(TAG, "compareIsSamePath3: "+canonicalPath3);//  /storage/emulated/0/DCIM   <----- /sdcard/DCIMTODO 改变了。ok

            /*
             * /storage/emulated/0 -> /data/media/0
             * /sdcard -> /storage/emulated/0
             *
             * sdcardfs 文件系统: /mnt/runtime/default/emulated/0 是使用 sdcardfs 文件系统虚拟化的路径,这个路径提供了对原始路径的多用户隔离和权限控制。
             * */

            // 比较规范路径
            boolean isSameFile1And2 = canonicalPath1.equals(canonicalPath2);
            boolean isSameFile1And3 = canonicalPath1.equals(canonicalPath3);
            boolean isSameFile2And3 = canonicalPath2.equals(canonicalPath3);

            if (isSameFile1And2) {
                System.out.println("/storage/emulated/0 and /mnt/runtime/default/emulated/0 point to the same file or directory.");
            } else {
                System.out.println("/storage/emulated/0 and /mnt/runtime/default/emulated/0 point to different files or directories.");
            }

            if (isSameFile1And3) {
                System.out.println("/storage/emulated/0 and /sdcard point to the same file or directory.");
            } else {
                System.out.println("/storage/emulated/0 and /sdcard point to different files or directories.");
            }

            if (isSameFile2And3) {
                System.out.println("/mnt/runtime/default/emulated/0 and /sdcard point to the same file or directory.");
            } else {
                System.out.println("/mnt/runtime/default/emulated/0 and /sdcard point to different files or directories.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //一般不这么判断----不推荐
    @Deprecated
    public static void compareIsSamePath2() {
        // 定义需要比较的路径
        String path1 = Environment.getExternalStorageDirectory().getPath();
        String path2 = "/mnt/runtime/default/emulated/0";
        String path3 = "/sdcard";
        Log.i(TAG, "compareIsSamePath: path1:"+path1);
        Log.i(TAG, "compareIsSamePath: path2:"+path2);
        Log.i(TAG, "compareIsSamePath: path3:"+path3);


        // 获取文件系统信息
        StatFs stat1 = new StatFs(path1);

        StatFs stat2 = new StatFs(path2);//出现这个错误的原因是路径 /mnt/runtime/default/emulated/0 可能需要特殊权限才能访问
        // Caused by: java.lang.IllegalArgumentException: Invalid path: /mnt/runtime/default/emulated/0
        // Caused by: android.system.ErrnoException: statvfs failed: EACCES (Permission denied)
        //       at libcore.io.Linux.statvfs(Native Method)
        StatFs stat3 = new StatFs(path3);

        // 比较文件系统信息
        boolean isSameFileSystem = (stat1.getBlockSizeLong() == stat2.getBlockSizeLong()
                && stat1.getBlockSizeLong() == stat3.getBlockSizeLong()
                && stat1.getBlockCountLong() == stat2.getBlockCountLong()
                && stat1.getBlockCountLong() == stat3.getBlockCountLong()
                && stat1.getAvailableBlocksLong() == stat2.getAvailableBlocksLong()
                && stat1.getAvailableBlocksLong() == stat3.getAvailableBlocksLong());

        if (isSameFileSystem) {
            Log.i(TAG, "compareIsSamePath: These paths point to the same file system.");
        } else {
            Log.e(TAG, "compareIsSamePath: These paths point to different file systems.");
        }
    }


    /**
     * 格式化存储大小
     * @param size 存储大小(字节)
     * @return 格式化后的存储大小字符串
     */
    private static String formatSize(long size) {
        String suffix = null;
        float fSize = size;

        if (fSize >= 1024) {
            suffix = "KB";
            fSize /= 1024;
            if (fSize >= 1024) {
                suffix = "MB";
                fSize /= 1024;
                if (fSize >= 1024) {
                    suffix = "GB";
                    fSize /= 1024;
                }
            }
        }

        StringBuilder resultBuffer = new StringBuilder(Long.toString((long) fSize));
        int commaOffset = resultBuffer.length() - 3;
        while (commaOffset > 0) {
            resultBuffer.insert(commaOffset, ',');
            commaOffset -= 3;
        }

        if (suffix != null) resultBuffer.append(suffix);
        return resultBuffer.toString();
    }
    /**
     * 存储信息类
     */
    private static class StorageInfo {
        long totalSpace;
        long availableSpace;
        long usedSpace;
        double usedPercentage;

        StorageInfo(long totalSpace, long availableSpace, long usedSpace, double usedPercentage) {
            this.totalSpace = totalSpace;
            this.availableSpace = availableSpace;
            this.usedSpace = usedSpace;
            this.usedPercentage = usedPercentage;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            StorageInfo that = (StorageInfo) o;
            return totalSpace == that.totalSpace && availableSpace == that.availableSpace && usedSpace == that.usedSpace;
        }

        @Override
        public int hashCode() {
            return Objects.hash(totalSpace, availableSpace, usedSpace);
        }

        @Override
        public String toString() {
            return "StorageInfo{" +
                    "totalSpace=" + totalSpace +
                    ", availableSpace=" + availableSpace +
                    ", usedSpace=" + usedSpace +
                    ", usedPercentage=" + usedPercentage +
                    '}';
        }
    }
}

参考文章:

牛晓伟 .待测试。 mp.weixin.qq.com/s/6mtFmO1Nh…

通用缓存存储设计方案。。Tecent juejin.cn/post/720770…