Android檔案系統詳解
前言
檔案系統一直是Android開發過程中經常接觸的東西。而關於內部儲存、外部儲存、外接儲存、私有儲存、公共儲存,以及訪問哪些檔案需要申請執行時許可權等問題,一直是許多開發者頭疼的問題。本文就將詳細地講解這些重要而模糊的知識點。
內部儲存
內部儲存主要用於儲存應用的私有檔案,其他應用無法訪問這些資料。當應用解除安裝的時候,這些資料也會被刪除。使用內部儲存不需要任何額外許可權。
寫入資料
String filename="innerFile";
String outData="CodingEnding";//需要寫入內部儲存的資料
FileOutputStream fos=null ;
try {
fos=openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(outData.getBytes());//寫入資料
Log.i(TAG,"寫入成功");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {//關閉檔案流
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
核心程式碼如下:
fos=openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(outData.getBytes());//寫入資料
fos.close();
首先使用Context
的openFileOutput方法獲取FileOutputStream,然後按照Java的檔案寫入方式操作就行了。
讀取資料
String filename="innerFile";
FileInputStream fis=null ;
try {
fis=openFileInput(filename);
BufferedReader bf=new BufferedReader(new InputStreamReader(fis));
StringBuilder builder=new StringBuilder();
String line=null;
while((line=bf.readLine())!=null){
builder.append(line);
}
Log.i(TAG,"已讀取資料:"+builder.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {//關閉資源
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
關鍵程式碼如下:
fis=openFileInput(filename);
BufferedReader bf=new BufferedReader(new InputStreamReader(fis));
StringBuilder builder=new StringBuilder();
String line=null;
while((line=bf.readLine())!=null){
builder.append(line);
}
通過Context
的openFileInput方法獲取FileInputStream,然後按照Java的檔案讀取方式操作就行了。
讀取靜態檔案
對於res/raw
資料夾下的資料,可以使用Resources
的openRawResource
方法讀取,示例程式碼如下:
InputStream fis=null;
try {
fis=getResources().openRawResource(R.raw.rawfile);
BufferedReader bf=new BufferedReader(new InputStreamReader(fis));
StringBuilder builder=new StringBuilder();
String line=null;
while((line=bf.readLine())!=null){
builder.append(line);
}
Log.i(TAG,"已讀取Raw資料:"+builder.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {//關閉資源
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
關鍵程式碼如下:
fis=getResources().openRawResource(R.raw.rawfile);
可以看到,這裡只是改用openRawResource
方法獲取InputStream,後續操作和讀取內部儲存資料一致。
注意,在raw資料夾中,檔名只能包含小寫字母、數字和下劃線。
快取資料
對於應用的私有快取資料,可以儲存在內部儲存的快取目錄中,關鍵方法是Context的getCacheDir
方法。
public abstract File getCacheDir();
這個方法會返回一個File型別的物件,這個File物件對應的就是內部儲存中用於儲存快取資料的根目錄。通過這個File物件,我們就可以利用Java檔案流的方式去讀取和寫入快取資料了。
注意,應用的私有快取檔案不應該過大。如果內部儲存空間不足,系統可能會刪除這些快取檔案。為了保證良好的使用者體驗,應用應該定期主動清除自己的快取資料。
外部儲存
除了內部儲存,Android系統還為開發者提供了外部儲存。需要注意的是,外部儲存並不僅僅指SD卡,它可能是可移除的儲存介質(典型如SD卡),也可能是不可移除的儲存介質(如現在很多一體機內建的儲存器)。外部儲存是相對於內部儲存的概念,用於儲存全域性範圍可讀取的檔案。這也就意味著,儲存在外部儲存中的資料可以被裝置中的任何應用訪問,甚至也可以被使用者檢視、修改。
獲取許可權
和訪問內部儲存不同,要讀取外部儲存中的檔案首先需要獲取許可權,即READ_EXTERNAL_STORAGE
和WRITE_EXTERNAL_STORAGE
許可權。如果應用同時有讀、寫的需求,只需要申請WRITE_EXTERNAL_STORAGE
許可權即可。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
注意,Android 6.0(API 23)引入了執行時許可權的概念,以上提到的兩種許可權都需要動態地獲取。如果不瞭解執行時許可權的概念,可以參考這篇部落格:
另外,在Android 4.4(API 19)及以上,如果只是在外部儲存中讀、寫應用的私有檔案,就不需要申請這些許可權。因此,我們可以使用maxSdkVersion
屬性實現只在較低版本申請許可權,如下所示:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
可用性檢查
由於外部儲存存在被移除的情況,我們在使用外部儲存前首先應該進行可用性檢查。使用Environment的getExternalStorageState
方法可以獲得外部儲存的狀態,通過判斷返回的狀態就實現了對外部儲存的可用性檢查。下面提供兩個簡單的示例:
1.判斷外部儲存是否可寫和可讀
String state=Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state)){
//外部儲存可寫、可讀
}
2.判斷外部儲存是否至少可讀
String state=Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state)||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)){
//外部儲存至少可讀
}
實際上,getExternalStorageState
方法有10種返回值,如下:
- MEDIA_UNKNOWN:未知狀態
- MEDIA_REMOVED:移除狀態(外部儲存不存在)
- MEDIA_UNMOUNTED:未裝載狀態(外部儲存存在但是沒有裝載)
- MEDIA_CHECKING:磁碟檢測狀態
- MEDIA_NOFS:外部儲存存在,但是磁碟為空或使用了不支援的檔案系統
- MEDIA_MOUNTED:就緒狀態(可讀、可寫)
- MEDIA_MOUNTED_READ_ONLY:只讀狀態
- MEDIA_SHARED:共享狀態(外部儲存存在且正通過USB共享資料)
- MEDIA_BAD_REMOVAL:異常移除狀態(外部儲存還沒有正確解除安裝就被移除了)
- MEDIA_UNMOUNTABLE:不可裝載狀態(外部儲存存在但是無法被裝載,一般是磁碟的檔案系統損壞造成的)
公共檔案(共享檔案)
對於在應用中產生的多媒體型別的檔案,如音樂、圖片、鈴聲等,一般應該儲存在外接儲存中對應的公共目錄下,如/Music、/Pictures、/Ringtones
,這樣方便和其他的應用共享這些檔案。同時,系統的媒體掃描器也能正確地對這些檔案進行歸類。
要將這些公共檔案(共享檔案)儲存到指定位置,關鍵是Environment的getExternalStoragePublicDirectory
方法,其原型如下:
public static File getExternalStoragePublicDirectory(String type);
這個方法需要提供一個String型別的type引數,以便返回儲存相應型別公共檔案的根目錄,即一個File物件。type的值不可為null,可選值如下(都是Environment中定義的常量):
- DIRECTORY_MUSIC:音樂型別
- DIRECTORY_PICTURES:圖片型別
- DIRECTORY_MOVIES:電影型別
- DIRECTORY_DCIM:照片型別
- DIRECTORY_DOWNLOADS:下載檔案型別
- DIRECTORY_DOCUMENTS:文件型別
- DIRECTORY_RINGTONES:鈴聲型別
- DIRECTORY_ALARMS:鬧鐘提示音型別
- DIRECTORY_NOTIFICATIONS:通知提示音型別
- DIRECTORY_PODCASTS:播客音訊型別
注意,返回的檔案目錄可能還不存在,因此在執行檔案操作前應該確保相應的檔案目錄已經存在,否則使用File的mkdirs
方法建立檔案目錄。
下面演示如何在下載檔案型別根目錄下建立自己的檔案目錄(具體儲存檔案的程式碼請參考demo):
String directoryName="PublicFileTest";
File publicDownloadDirectory=Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS);
File myDownloadDirectory=new File(publicDownloadDirectory,directoryName);
if(!myDownloadDirectory.exists()){//確保指定目錄已經建立
boolean createResult=myDownloadDirectory.mkdirs();
if(createResult){
Log.i(TAG,"建立成功");
}else{
Log.i(TAG,"建立失敗");
}
}
小技巧:如果不希望系統的媒體掃描器訪問我們的媒體檔案,可以在媒體檔案所在的目錄下新建一個名為.nomedia
的空檔案,這會阻止媒體掃描器歸類我們的檔案並提供給其他應用。
私有檔案
對於應用私有的檔案,則應該使用Context的getExternalFilesDir
方法訪問外部儲存中的私有儲存目錄,媒體掃描器不會掃描這些目錄。可以為這個方法傳入一個String型別的type引數,用於獲取私有儲存目錄中相應的媒體檔案子目錄。當然,也可以傳入null直接獲取私有儲存的根目錄。這個方法的返回值也是一個File物件。
public abstract File getExternalFilesDir(String type);
注意,某些移動裝置可能既提供了內建儲存器作為外部儲存空間,同時又提供了SD卡作為外部儲存空間。也就是說,在這些裝置中外部儲存實際上包含了兩塊磁碟。在Android 4.3(API 18)及以下,Context的getExternalFilesDir
方法僅僅會返回內建儲存器對應的外部儲存控制元件,而無法訪問SD卡對應的儲存空間。從Android 4.4(API 19)開始,Context新增了getExternalFilesDirs
方法。這個方法的返回值是一個File陣列,包含兩個物件(可能為null),這樣就可以實現對內建儲存器和SD卡的訪問。陣列的第一個物件預設是外部主儲存,官方的開發建議是除非這個位置已滿或不可用,否則應該使用這個位置。
public abstract File[] getExternalFilesDirs(String type);
另外,出於相容性的考慮,可以使用ContextCompat的getExternalFilesDirs方法。這是一個靜態方法,返回值也是一個File陣列。在Android 4.4及以上,效果和Context的getExternalFilesDirs方法一致;而在Android 4.3及以下,返回的File陣列始終只包含一個物件。
public static File[] getExternalFilesDirs(Context context, String type)
對於以上方法的type引數,有以下幾種可選值(都是Environment中定義的常量):
- DIRECTORY_MUSIC:音樂型別
- DIRECTORY_PICTURES:圖片型別
- DIRECTORY_MOVIES:電影型別
- DIRECTORY_RINGTONES:鈴聲型別
- DIRECTORY_ALARMS:鬧鐘提示音型別
- DIRECTORY_NOTIFICATIONS:通知提示音型別
- DIRECTORY_PODCASTS:播客音訊型別
注意,當應用解除安裝時,這些私有儲存目錄中的檔案也會被刪除。此外,雖然系統的媒體掃描器不會訪問外部儲存中的私有儲存目錄,但是其他具有READ_EXTERNAL_STORAGE
或WRITE_EXTERNAL_STORAGE
許可權的應用依舊可以讀/寫這些私有儲存目錄中的檔案。因此對於真正重要的檔案,還是應該儲存在應用的內部儲存中。
補充:私有檔案根目錄的參考路徑:Android/data/包名/files/
(具體儲存檔案的程式碼請參考demo)
快取檔案
在外部儲存中也有專門儲存快取檔案的空間,可以通過Context的getExternalCacheDir
方法訪問快取檔案目錄,返回值是一個File物件。上文曾說過,外部儲存可能同時包含內建儲存器和SD卡兩個儲存空間,因此在Android 4.4(API 19)及以上還可以通過Context的getExternalCacheDirs
方法訪問這兩個儲存空間。這個方法會返回一個File陣列,包含兩個物件,第一個物件預設是外部主儲存對應的快取檔案目錄。
public abstract File getExternalCacheDir();
public abstract File[] getExternalCacheDirs();
同樣,為了相容性,也可以使用ContextCompat的getExternalCacheDirs
方法。這是一個靜態方法,返回值也是一個File陣列。在Android 4.4及以上,效果和Context的getExternalCacheDirs方法一致;而在Android 4.3及以下,返回的File陣列始終只包含一個物件。
public static File[] getExternalCacheDirs(Context context);
注意,當應用解除安裝時,快取目錄下的檔案也會被系統刪除。當然,官方建議開發者應該主動移除不再需要的快取檔案,這有助於節省儲存空間並保持應用效能。
補充:快取檔案根目錄的參考路徑:Android/data/包名/cache/
其他常用API
除了以上提過的方法,Android檔案系統還提供了其他可用的API,下面簡單進行講解:
Context
public abstract File[] getExternalMediaDirs();
該方法將以File陣列的形式返回外部儲存中所有可以儲存媒體檔案的目錄。這些目錄中的檔案將會被系統媒體掃描器訪問,並可以通過MediaStore
提供給其他應用。
public abstract File getDataDir();
這個方法以絕對路徑的方式訪問應用的私有檔案目錄(內部儲存)。官方並不建議直接使用這個方法返回的路徑,因為如果應用遷移到其他位置(如遷移到SD卡),檔案路徑將發生改變。
public abstract File getDir(String name, @FileMode int mode);
以File形式返回一個檔案目錄,應用可以在這個目錄中儲存自己的資料檔案。如果這個目錄還不存在,系統將會自動建立它。
public abstract File getFilesDir();
以File形式返回openFileOutput
方法所使用的檔案目錄(即內部儲存根目錄)。注意,這個方法是通過絕對路徑進行檔案訪問的,因此應用發生遷移將導致返回的路徑發生變化。
public abstract File getFileStreamPath(String name);
以File形式返回通過openFileOutput
方法儲存的檔案。注意,這個方法是通過絕對路徑進行檔案訪問的,因此應用發生遷移將導致返回的路徑發生變化。
public abstract File getObbDir();
以File形式(絕對路徑)返回應用Obb檔案的儲存目錄。如果當前應用並不存在Obb檔案,則這個目錄也不存在,返回null。
public abstract File[] getObbDirs();
和上一個方法型別,只不過返回的物件是File陣列,因此得以訪問SD卡中的Obb檔案目錄。這個方法在Android 4.4(API 19)及以上可用。
ContextCompat
public static File getDataDir(Context context);
這是Context#getDataDir
方法的相容性版本。
public static File[] getObbDirs(Context context);
這是Context#getObbDirs
方法的相容性版本。
Environment
public static File getDataDirectory();
以File形式返回使用者資料目錄,即/data
目錄。
public static File getDownloadCacheDirectory();
以File形式返回下載快取資料目錄,即/cache
目錄。
public static File getExternalStorageDirectory();
以File形式返回外部儲存根目錄。
public static File getRootDirectory();
以File形式返回存放系統OS的檔案根目錄,即system
目錄。注意,這個分割槽始終處於只讀狀態。