本文将以实用的角度来讲解Android中文件操作的常用方式。

存储的”内“和“外”


所有Android设备都有两个文件存储区域:内部存储空间(internal Storage)和外部存储空间(external Storage)。这些名称是在Android早期确定的,那时候大部分设备都提供内置的非易失性内存(内部存储空间)以及可移动存储媒介(如,Micro SD卡,提供外部存储空间)。现在,很多设备将永久性存储空间划分为单独的“内部”和“外部”分区。因此,即使没有可移动存储媒介,这两种存储空间也始终存在,并且无论外部存储空间是否可移动,这两种存储空间的API行为在Android系统上都是相同的。

所以,Android系统从逻辑上,只分为"internal Storage" 与 “external Storage” 两个存储分区。

内部存储分区(internal Storage)

内部存储分区,物理位置主要包括了Android系统根目录下的/data、/System、/cache等目录。

内部存储分区的特点:
  • 内部分区总是可用。
  • 它存放App私有文件,并且不可被其他App访问。
  • App卸载后,存储在内部分区上的该App数据将会被清除。
  • 不需要额外申请权限。

外部存储分区(external Storage)

它有以下几个特点:

  • 外部分区并不总是可用。
  • 保存在这里的文件可能被其他程序访问。
  • 当用户卸载app时,系统仅仅会删除external中的缓存目录(Context.getExternalCacheDir())和file目录(Context.getExternalFilesDir())下的相关文件。
  • 需要申请WRITE_EXTERNAL_STORAGE或READ_EXTERNAL_STORAGE权限。

我们在开发过程中,经常需要读取或者存储一些数据,这些数据可以存储在内部分区中,也可以存储在外部分区中,但不同的操作方式会有很大区别,我们下面来详细进行分析。

内部存储分区的访问


本节重点来分析内部存储分区的数据访问。内部存储包含了/system、/data、/cache等目录及其子目录。

/system

系统存放目录,它和/sdcard以及/data是同级的,是存储根目录的一级子目录。

访问方式

可以通过Environment类的getRootDirectory方法访问:


private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; //环境变量
    private static final File DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system");//如果环境变量指定了,则使用指定值,否则使用"/system"
    public static @NonNull File getRootDirectory() {
        return DIR_ANDROID_ROOT;
    }

这里通常返回目录是"/system"。

子目录

/system/app:存放rom本身附带的软件即系统软件。
/system/data:存放/system/app中,核心系统软件的数据文件信息。
/system/priv-app:存放手机厂商定制的系统级别的应用的apk文件。
/system/bin:存放系统的本地程序,里面主要是Linux系统自带的组件。
/system/media:存放一些音效、铃声、开关机动画等。

/data目录

/data目录时我们App私有数据存储的顶级目录,可以通过Environment.getDataDirectory()获取。

Environment.getDataDirectory()源码:

private static final File DIR_ANDROID_DATA = getDirectory(ENV_ANDROID_DATA, "/data");
    /**
     * Return the user data directory.
     */
    public static File getDataDirectory() {
        return DIR_ANDROID_DATA;
    }

我们通常不会直接使用该目录进行数据存储操作。

应用程序私有根目录

应用程序私有目录,它的根目录位于/data/data/<app包名>/文件夹下。

可通过Context对象的getDataDir()方法来获取,在开发时,通常我们不应该直接使用该目录,而应该使用file、cache等系统已经定义好的目录。

getDataDir()方法

getDataDir()方法的实现实在ContextImpl中:

@Override
    public File getDataDir() {
        if (mPackageInfo != null) {
            File res = null;
            if (isCredentialProtectedStorage()) {
                res = mPackageInfo.getCredentialProtectedDataDirFile();
            } else if (isDeviceProtectedStorage()) {
                res = mPackageInfo.getDeviceProtectedDataDirFile();
            } else {
                res = mPackageInfo.getDataDirFile(); //mPackageInfo是LoadedApk的对象。
            }
            ……
        } else {
            throw new RuntimeException(
                    "No package details found for package " + getPackageName());
        }
    }

这里其实有个判断,但通常情况下,逻辑会走到res = mPackageInfo.getDataDirFile()这里,mPackageInfo是LoadedApk的对象,最终数据来源是ApplicationInfo对象传递进来的。

应用程序files目录

Context对象的getFilesDir()方法可以获得应用私有目录的file目录,位置是通常是:/data/data/<app包名>/files文件夹。

我们对文件操作常用的方法,Context对象的openFileOutput()方法的文件根目录地址就是files目录。

ContextImpl中的源码:

@Override
    public File getFilesDir() {
        synchronized (mSync) {
            if (mFilesDir == null) {
                mFilesDir = new File(getDataDir(), "files");
            }
            return ensurePrivateDirExists(mFilesDir);
        }
    }

该目录是我们需要经常使用的目录。

应用程序cache目录

cache目录是我们App内部存储的缓存目录。它可以通过Context对象的getCacheDir()方法来获得,位置是通常是:/data/data/<app包名>/cache文件夹。如果您想暂时保留而非永久存储某些数据,则应使用特殊的缓存目录来保存这些数据。不应依赖系统为您清理这些文件,而应始终自行维护缓存文件,使其占用的空间保持在合理的限制范围内(例如 1MB)。当用户卸载您的应用时,这些文件也会随之移除。

getCacheDir()方法

Context对象的getCacheDir()方法可以获取cache目录。

ContextImpl中的源码:

@Override
    public File getCacheDir() {
        synchronized (mSync) {
            if (mCacheDir == null) {
                mCacheDir = new File(getDataDir(), "cache");
            }
            return ensurePrivateCacheDirExists(mCacheDir, XATTR_INODE_CACHE);
        }
    }
cache文件有以下几个特点需要注意:
  1. 系统将在磁盘空间不足时自动删除此目录中的文件。
  2. 系统将始终首先删除旧文件。
  3. 我们可以使用StorageManager类的相关方法更好的管理我们的删除规则。
  4. App所占缓存空间的大小可以通过StorageManager.getCacheQuotaBytes(java.util.UUID)来获得。
  5. 超过App所分配限额的缓存空间将被优先删除,我们应该尽可能的使我们的cache空间内的文件低于限额值,这会使得我们的cache文件最大可能的减少被删除的概率。

databases目录

databases目录存放了应用程序的数据库文件,位置是通常是:/data/data/<app包名>/databases文件夹。

getDatabasesDir()方法

Context对象的getDatabasesDir()方法可以获取databases目录。

ContextImpl中的源码:

private File getDatabasesDir() {
        synchronized (mSync) {
            if (mDatabasesDir == null) {
                if ("android".equals(getPackageName())) {
                    mDatabasesDir = new File("/data/system");
                } else {
                    mDatabasesDir = new File(getDataDir(), "databases");
                }
            }
            return ensurePrivateDirExists(mDatabasesDir);
        }
    }

该方法是一个私有方法,不能直接访问,我们通常使用DB的相关封装方法来进行访问。这里我们可以看到,如果应用程序的包名是“android”,则DB的目录是"/data/system",否则,DB的目录是/data/data/<app包名>/databases。

shared_prefs目录

如果应用想存储一些数据量较小的键值对信息,可以使用SharedPreferences来保存数据,例如,一些应用相关的配置信息等。

它可以通过Context对象的getSharedPreferences方法来进行访问操作,位置是通常是:/data/data/<app包名>/shared_prefs文件夹。

getPreferencesDir()方法

sp目录可以通过getPreferencesDir()方法来进行获取,我们不能直接使用该方法。

ContextImpl中的源码:

private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }

/cache目录

下载缓存内容目录,它和/system以及/data是同级的,目录是/cache。

该目录可以通过Environment的getDownloadCacheDirectory方法返回:

private static final File DIR_DOWNLOAD_CACHE = getDirectory(ENV_DOWNLOAD_CACHE, "/cache");
    public static File getDownloadCacheDirectory() {
        return DIR_DOWNLOAD_CACHE;
    }

外部存储分区的访问


外部存储可能是不可用的,比如遇到SD卡被拔出等情况时,因此在访问之前应对其可用性进行检查。我们可以通过执行getExternalStorageState()来查询外部存储设备的状态,若返回状态为MEDIA_MOUNTED, 则可以读写。

/sdcard

外部存储的sd卡根目录,也就是我们平时从文件管理器中能看到的最顶级目录,它的File绝对路径为:/storage/emulated/0。

访问方式

可以通过Environment类的getExternalStorageDirectory方法访问:

@Deprecated
    public static File getExternalStorageDirectory() {
        throwIfUserRequired();
        return sCurrentUser.getExternalDirs()[0];
    }

getExternalDirs方法返回的是所有外部存储的文件列表,getExternalStorageDirectory返回的是列表中的第一个元素,也就是主外部存储的目录。

应用的外部私有文件

外部私有文件,存储在外部分区,当应用被卸载后,与该应用相关的数据也清除掉。这里的私有并非其他应用访问不到,而是指该类数据是当前应用私有的,对其他应用并无用处,并且该类文件会在卸载时,被系统删除。

这部分存储的位置位于/Android/data/<app包名>/下。

getExternalFilesDir()方法

通过Context.getExternalFilesDir()方法可以获取SDCard/Android/data/<app包名>/files/目录,一般放一些长时间保存的数据。

ContextImpl中的源码:
@Override
    public File getExternalFilesDir(String type) {
        // Operates on primary external storage
        final File[] dirs = getExternalFilesDirs(type);
        return (dirs != null && dirs.length > 0) ? dirs[0] : null;
    }

    @Override
    public File[] getExternalFilesDirs(String type) {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
            if (type != null) {
                dirs = Environment.buildPaths(dirs, type);
            }
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }

getExternalFilesDir方法的参数,表示将要创建的/files目录下的子目录。通过源码我们看到,它调用了Environment.buildExternalStorageAppFilesDirs(getPackageName())来获取/files目录。buildExternalStorageAppFilesDirs根据外部存储的数量,返回的是一个File的数组,getExternalFilesDir只取第一个,也就是主外部存储的目录。

Environment.buildExternalStorageAppFilesDirs方法:
public static final String DIR_ANDROID = "Android";
    private static final String DIR_DATA = "data";
    private static final String DIR_MEDIA = "media";
    private static final String DIR_OBB = "obb";
    private static final String DIR_FILES = "files";
    private static final String DIR_CACHE = "cache";
    
    @UnsupportedAppUsage
    public static File[] buildExternalStorageAppFilesDirs(String packageName) {
        throwIfUserRequired();
        return sCurrentUser.buildExternalStorageAppFilesDirs(packageName);
    }
内部类UserEnvironment的buildExternalStorageAppFilesDirs方法:
public File[] buildExternalStorageAppFilesDirs(String packageName) {
            return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_FILES);
        }

这里按照目录的父子关系,依次创建了相应的目录:Android、data、packageName目录、files。

Context.getExternalCacheDir()方法

通过Context.getExternalCacheDir()方法可以获取到SDCard/Android/data/<app包名>/cache/目录,一般存放临时缓存数据时使用。

ContextImpl中的源码:
@Override
    public File getExternalCacheDir() {
        // Operates on primary external storage
        final File[] dirs = getExternalCacheDirs();
        return (dirs != null && dirs.length > 0) ? dirs[0] : null;
    }

    @Override
    public File[] getExternalCacheDirs() {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }

逻辑与files目录类似,最终调用到Environment内部类UserEnvironment的buildExternalStorageAppCacheDirs方法。

内部类UserEnvironment的buildExternalStorageAppCacheDirs方法:
public File[] buildExternalStorageAppCacheDirs(String packageName) {
            return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE);
        }

这里创建了cache目录。

外部公共存储目录

使用Environment的getExternalStoragePublicDirectory方法可以访问外部公共存储目录。

Environment的getExternalStoragePublicDirectory方法:
public static File getExternalStoragePublicDirectory(String type) {
        throwIfUserRequired();
        return sCurrentUser.buildExternalStoragePublicDirs(type)[0];
    }
内部类UserEnvironment的buildExternalStorageAppCacheDirs方法
public File[] buildExternalStoragePublicDirs(String type) {
            return buildPaths(getExternalDirs(), type);
        }

getExternalDirs方法返回的是所有外部存储的文件列表,getExternalStoragePublicDirectory返回的是列表中的第一个元素,也就是主外部存储中的目录。

该方法会根据传递的参数名为子目录,在/sdcard下创建一个子目录作为公共访问目录。

Environment的getExternalStoragePublicDirectory方法的使用限制
  1. Environment的getExternalStoragePublicDirectory方法的参数应该是以下几种特定类型:
*            {@link #DIRECTORY_MUSIC}, 
     *            {@link #DIRECTORY_PODCASTS},
     *            {@link #DIRECTORY_RINGTONES}, 
     *            {@link #DIRECTORY_ALARMS},
     *            {@link #DIRECTORY_NOTIFICATIONS}, 
     *            {@link #DIRECTORY_PICTURES},
     *            {@link #DIRECTORY_MOVIES}, 
     *            {@link #DIRECTORY_DOWNLOADS},
     *            {@link #DIRECTORY_DCIM}, or 
     *            {@link #DIRECTORY_DOCUMENTS}
  1. 参数不能为null。
  2. 在Android Q中,该接口已经废弃。替代方案建议使用Context.getExternalFilesDir、MediaStore、Intent.ACTION_OPEN_DOCUMENT。

验证外部存储是否可用

由于外部存储可能会不可用,例如,当用户将存储安装到另一台机器或移除了提供外部存储的SD卡时。因此在访问外部存储之前,我们需要首先验证外部存储是否可用,然后再进行访问操作。

我们可以通过调用getExternalStorageState()来查询外部存储的状态。如果返回的状态为MEDIA_MOUNTED,则可以读取和写入文件。如果返回的是MEDIA_MOUNTED_READ_ONLY,则只能读取文件。

示例代码:

/* Checks if external storage is available for read and write */
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }

    /* Checks if external storage is available to at least read */
    public boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }

我们在确认外部存储可用之后,就可以安全的访问外部存储设备上的数据了。