本文将以实用的角度来讲解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文件有以下几个特点需要注意:
- 系统将在磁盘空间不足时自动删除此目录中的文件。
- 系统将始终首先删除旧文件。
- 我们可以使用StorageManager类的相关方法更好的管理我们的删除规则。
- App所占缓存空间的大小可以通过StorageManager.getCacheQuotaBytes(java.util.UUID)来获得。
- 超过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方法的使用限制
- 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}
- 参数不能为null。
- 在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;
}
我们在确认外部存储可用之后,就可以安全的访问外部存储设备上的数据了。