1. 程式人生 > >Android檔案系統詳解

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資料夾下的資料,可以使用ResourcesopenRawResource方法讀取,示例程式碼如下:

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_STORAGEWRITE_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種返回值,如下:

  1. MEDIA_UNKNOWN:未知狀態
  2. MEDIA_REMOVED:移除狀態(外部儲存不存在)
  3. MEDIA_UNMOUNTED:未裝載狀態(外部儲存存在但是沒有裝載)
  4. MEDIA_CHECKING:磁碟檢測狀態
  5. MEDIA_NOFS:外部儲存存在,但是磁碟為空或使用了不支援的檔案系統
  6. MEDIA_MOUNTED:就緒狀態(可讀、可寫)
  7. MEDIA_MOUNTED_READ_ONLY:只讀狀態
  8. MEDIA_SHARED:共享狀態(外部儲存存在且正通過USB共享資料)
  9. MEDIA_BAD_REMOVAL:異常移除狀態(外部儲存還沒有正確解除安裝就被移除了)
  10. MEDIA_UNMOUNTABLE:不可裝載狀態(外部儲存存在但是無法被裝載,一般是磁碟的檔案系統損壞造成的)

公共檔案(共享檔案)

對於在應用中產生的多媒體型別的檔案,如音樂、圖片、鈴聲等,一般應該儲存在外接儲存中對應的公共目錄下,如/Music、/Pictures、/Ringtones,這樣方便和其他的應用共享這些檔案。同時,系統的媒體掃描器也能正確地對這些檔案進行歸類。

要將這些公共檔案(共享檔案)儲存到指定位置,關鍵是Environment的getExternalStoragePublicDirectory方法,其原型如下:

public static File getExternalStoragePublicDirectory(String type);

這個方法需要提供一個String型別的type引數,以便返回儲存相應型別公共檔案的根目錄,即一個File物件。type的值不可為null,可選值如下(都是Environment中定義的常量):

  1. DIRECTORY_MUSIC:音樂型別
  2. DIRECTORY_PICTURES:圖片型別
  3. DIRECTORY_MOVIES:電影型別
  4. DIRECTORY_DCIM:照片型別
  5. DIRECTORY_DOWNLOADS:下載檔案型別
  6. DIRECTORY_DOCUMENTS:文件型別
  7. DIRECTORY_RINGTONES:鈴聲型別
  8. DIRECTORY_ALARMS:鬧鐘提示音型別
  9. DIRECTORY_NOTIFICATIONS:通知提示音型別
  10. 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中定義的常量):

  1. DIRECTORY_MUSIC:音樂型別
  2. DIRECTORY_PICTURES:圖片型別
  3. DIRECTORY_MOVIES:電影型別
  4. DIRECTORY_RINGTONES:鈴聲型別
  5. DIRECTORY_ALARMS:鬧鐘提示音型別
  6. DIRECTORY_NOTIFICATIONS:通知提示音型別
  7. DIRECTORY_PODCASTS:播客音訊型別

注意,當應用解除安裝時,這些私有儲存目錄中的檔案也會被刪除。此外,雖然系統的媒體掃描器不會訪問外部儲存中的私有儲存目錄,但是其他具有READ_EXTERNAL_STORAGEWRITE_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目錄。注意,這個分割槽始終處於只讀狀態。

參考資料

demo