本文將以實用的角度來講解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;
}
我們在確認外部存儲可用之後,就可以安全的訪問外部存儲設備上的數據了。