数据存储之——Android内、外存储分区&常用存储目录详解(Android(安卓)Q)
本文将以实用的角度来讲解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//文件夹下。
可通过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//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//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//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//databases。
shared_prefs目录
如果应用想存储一些数据量较小的键值对信息,可以使用SharedPreferences来保存数据,例如,一些应用相关的配置信息等。
它可以通过Context对象的getSharedPreferences方法来进行访问操作,位置是通常是:/data/data//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//下。
getExternalFilesDir()方法
通过Context.getExternalFilesDir()方法可以获取SDCard/Android/data//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//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; }
我们在确认外部存储可用之后,就可以安全的访问外部存储设备上的数据了。
更多相关文章
- Android基础笔记(二)-数据存储和界面展现
- 想抢先体验Android操作系统的魅力吗?那就使用Android(安卓)LiveCD
- Android进阶知识树——Android系统的启动过程
- IOS和Android(安卓)OpenGL游戏引擎的集成AdMob新版教程
- Android(安卓)程式开发:(一)详解Activity —— 1.1生命周期
- 2011Android技术面试整理附有详细答案(包括百度、新浪、中科软等
- android makefile(android.mk)分析(序)
- Android(安卓)gradle build 修改文件名称及目录
- Android(安卓)LayoutInflater原理分析,带你一步步深入了解View(一