Android 平台的檔案讀寫方式

Posted in :

處理檔案是程式開發過程中常會碰到的問題,在 Android 平台上讀寫檔案的也是利用 Java 的 File、InputStream 以及 OutputStream 物件來達成。不過 Android 系統對 App 的使用空間與檔案操作有一套自己的管理方式,透過系統提供的 Context 與 Environment 物件可以讓開發人員快速的進行檔案的各種操作。

 

Google 官方的教學:

https://developer.android.com/training/basics/data-storage/files.html?hl=zh-tw

 


A. 使用的物件以及方法

  1. Context
    • abstract boolean deleteFile(String name)
    • abstract String[] fileList()
    • abstract File getCacheDir()
    • abstract File getDir(String name, int mode)
    • abstract File getExternalCacheDir()
    • abstract File getExternalFilesDir(String type)
    • abstract File getFileStreamPath(String name)
    • abstract File getFilesDir()
    • abstract FileInputStream openFileInput(String name)
    • abstract FileOutputStream openFileOutput(String name, int mode)
  2. Environment
    • static File getDataDirectory()
    • static File getDownloadCacheDirectory()
    • static File getExternalStorageDirectory()
    • static File getExternalStoragePublicDirectory(String type)
    • static String getExternalStorageState()
    • static File getRootDirectory()
    • static boolean isExternalStorageEmulated()
    • static boolean isExternalStorageRemovable()

B. 原理說明

    1. 在 Android 設備上的儲存體 (storage) 可分為內部 (internal) 以及外部(external) 兩種,內部儲存體指的是內建的 Flash,外部儲存體指的是外接的 SD 卡。有些設備即使沒有外接的儲存設備,Android 系統也會將儲存體分為內部以及外部兩個區域。因此,內部儲存體一定存在,外部儲存體則不一定,如果沒有外接儲存設備就不會有外部儲存體。
    1. 在預設的情況下 App 會將新建立檔案存在內部儲存體,存在內部儲存體的檔案預設只能被該 App 存取。當 App 被移除時,儲存在該空間的檔案也會一併被刪除。因此,內部儲存體適合用來擺放專屬於該 App 的檔案,當 App 被移除時這些檔案也沒有存在的必要。
    1. 除了內部儲存體外,App 也可以將檔案存放在外部儲存體,放在外部儲存體的檔案可以被其他的 App 讀取。當 App 被移除時,存放在外部儲存體的檔案並不會被移除,唯一的例外是存放在 getExternalFilesDir() 目錄底下的檔案會被移除 (該目錄底下的檔案算是 App 的私有檔案,雖然是放在外部儲存體,不過 App 被移除時系統也會將檔案刪除)。
    1. 在安裝 App 時預設會裝在內部儲存體,也可以在 AndroidManifest.xml 中設定 android:installLocation 屬性,將 App 安裝在外部儲存體 (除非 App 的大小超過內部儲存體的空間大小,否則很少這樣做)。
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          android:installLocation=["auto" | "internalOnly" | "preferExternal"]
          ...
      </manifest>
    2. 在預設的情況下 App 具有讀/寫內部儲存體的權限,因此,可以讀取 (read) 或寫入 (write) 內部儲存體裡面的檔案,並不需要在 AndroidManifest.xml 中宣告額外的權限。
    1. 在預設的情況下,App 具有讀取 (沒有寫入) 外部儲存體的權限,不過這個權限在未來的 Android 版本可能會做調整,因此,若 App 有讀取外部儲存體的需求,最好還是在 AndroidManifest.xml 檔案中宣告 READ_EXTERNAL_STORAGE 的權限會比較保險,如:
      <manifest ...>
          <uses-permission 
           android:name="android.permission.READ_EXTERNAL_STORAGE" />
          ...
      </manifest>
      
    2. 如果要將檔案存放在外部儲存體,必須取得寫入外部儲存體的權限才行,因此要在 AndroidManifest.xml 中宣告 WRITE_EXTERNAL_STORAGE 權限:
      <manifest ...>
          <uses-permission 
           android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
          ...
      </manifest>

      如果 App 具有寫入外部儲存體的權限,隱含的意義就是該 App 也同時取得了讀取外部儲存體的權限 (能夠寫入就表示一定能夠讀取)。

  1. 在 Android 平台上讀寫檔案的方式是透過 java.io.File 物件來達成,至於檔案的擺放位置或建立檔案的方式,可透過 Context 物件裡面的以下方法來達成:
    • abstract File getFilesDir()
      取得 App 內部儲存體存放檔案的目錄 (絕對路徑)
      預設路徑為 /data/data/[package.name]/files/
    • abstract File getCacheDir()
      取得 App 內部儲存體存放暫存檔案的目錄 (絕對路徑)
      預設路徑為 /data/data/[package.name]/cache/
    • abstract File getExternalFilesDir(String type)
      取得 App 外部儲存體存放檔案的目錄 (絕對路徑)
    • abstract File getExternalCacheDir()
      取得 App 外部儲存體存放暫存檔案的目錄 (絕對路徑)
    • abstract File getDir(String name, int mode)
      取得 App 可以擺放檔案的目錄,若該目錄不存在則建立一個新的
      ex: getDir(“music”, 0) -> /data/data/[package.name]/app_music
    • abstract boolean deleteFile(String name)
      刪除 getFilesDir() 目錄底下名稱為 name 的檔案
    • abstract String[] fileList()
      回傳 getFilesDir() 目錄底下的檔案及目錄名稱
    • abstract FileInputStream openFileInput(String name)
      開啟 getFilesDir() 目錄下檔名為 name 的檔案來進行讀取
    • abstract FileOutputStream openFileOutput(String name, int mode)
      在 getFilesDir() 目錄底下開啟或建立檔名為 name 的檔案來進行寫入
    • abstract File getFileStreamPath(String name)
      取得 openFileOutput() 所建立之名稱為 name 的檔案的絕對路徑
    1. Environment 物件提供 Android 系統環境的相關資訊,包含外部儲存體的狀態,以及相關檔案的擺放位置,如:
      • static File getDataDirectory()
        取得系統的資料擺放目錄,預設位置為 /data
      • static File getDownloadCacheDirectory()
        取得系統檔案下載或暫存檔案的擺放目錄,預設位置為 /cache
      • static File getExternalStorageDirectory()
        取得外部儲存體的根目錄,預設位置為 /mnt/sdcard
      • static File getExternalStoragePublicDirectory(String type)
        取得外部儲存體存放公開檔案的目錄
      • static String getExternalStorageState()
        取得外部儲存體的狀態資訊
      • static File getRootDirectory()
        取得檔案系統的根目錄,預設位置為 /system
      • static boolean isExternalStorageEmulated()
        判斷外部儲存體是否使用內部儲存體模擬產生
        true: 外部儲存體不存在,而是使用內部儲存體模擬產生
        false: 外部儲存體存在,並非使用內部儲存體模擬
      • static boolean isExternalStorageRemovable()
        判斷外部儲存體是否可以移除,回傳值的意義如下:
        true: 外部儲存體屬於外接式的,且可以移除
        false: 外部儲存體內建在系統中,無法被移除
    2. 當 Android 系統發現空間不足時,會將存放在暫存目錄 getCacheDir() 裡面的檔案刪除。因此,App 在執行時不能假設存放在該目錄裡面的檔案一定存在,也不能假設該目錄底下的檔案一定會被系統刪除,最好是在檔案不用時 App 自己將它刪除,以免占用內部儲存體的空間。
  1. 由於外部儲存體不一定存在,所以在使用前必須先檢查它的狀態,以避免在讀寫時發生錯誤。透過 Environment 物件的 getExternalStorageState() 方法可以查詢目前外部儲存體的狀態,其中狀態可以是以下這幾種:
    • MEDIA_BAD_REMOVAL: 外部儲存體在正常卸載之前就被拔除
    • MEDIA_CHECKING: 外部儲存體存在且正在進行磁碟檢查
    • MEDIA_MOUNTED: 外部儲存體存在且可以進行讀取與寫入
    • MEDIA_MOUNTED_READ_ONLY: 外部儲存體存在但只能進行讀取
    • MEDIA_NOFS: 外部儲存體存在,但內容是空的或是 Android 不支援該檔案系統
    • MEDIA_REMOVED: 外部儲存體不存在
    • MEDIA_SHARED: 外部儲存體存在但未被掛載,且為 USB 的裝置
    • MEDIA_UNMOUNTABLE: 外部儲存體存在但不能被掛載
    • MEDIA_UNMOUNTED: 外部儲存體存在但未被掛載
    1. 外部儲存體的另一個涵義指的是所有 App 的共用空間,對 App 來說存放在外部儲存體的檔案可以分為公開檔案 (public files) 與私有檔案 (private files) 兩種。擺放在 Environment.getExternalStoragePublicDirectory() 目錄底下的為公開檔案,擺放在 Context.getExternalFilesDir() 目錄底下的為私有檔案。
    1. 公開檔案就像是照片或是音樂,由目前 App 產生可以提供其他 App 使用的檔案。私有檔案就像是 App 執行時產生的暫存檔,對其他 App 來說並沒有使用上的價值。擺放在外部儲存體的檔案都可以被其他 App 存取,不過當 App 被移除時,只有私有檔案會被移除,公開檔案並不會被移除。
  1. 由於公開檔案可以提供其它 App 使用,所以在放置這些檔案時 Android 系統提供了一些基本的分類,讓 App 可以依檔案屬性將檔案放置在不同目錄裡面,方便其它 App 可以使用。因此,getExternalStoragePublicDirectory(String type) 可以接受一個 type 參數,該參數表示目錄中儲存的檔案型態,例如:getExternalStoragePublicDirectory(DIRECTORY_PICTURES) 會回傳用來擺放圖片檔的目錄,如果 App 產生的圖片要提供給其它 App 使用,就可以擺放在這個目錄。目前 Android 定義的目錄型態包含以下這幾種:
    • DIRECTORY_ALARMS: 鬧鐘的音效檔
    • DIRECTORY_DCIM: 相機的圖片與影片檔
    • DIRECTORY_DOWNLOADS: 使用者下載的檔案
    • DIRECTORY_MOVIES: 電影檔
    • DIRECTORY_MUSIC: 音樂檔
    • DIRECTORY_NOTIFICATIONS: 通知音效檔
    • DIRECTORY_PICTURES: 一般的圖片檔
    • DIRECTORY_PODCASTS: 訂閱的廣播檔
    • DIRECTORY_RINGTONES: 鈴聲檔

type 參數如果為 null 時可取得擺放公開檔案的根目錄,如果 App 要擺放的檔案型態不屬於上述那幾類,也可以直接將檔案擺放在根目錄。

  1. 將資料寫入儲存體時如果造成空間不足就發產生 IOException,使用 File 物件的 getTotalSpace() 與 getFreeSpace() 可以取得儲存體的總容量與剩餘空間資訊 (單位是 bytes)。如果可以事先知道要寫入的檔案大小,就可以在寫入前先判斷剩餘空間是否足夠,以避免寫入過程發生錯誤。

C. 使用方式

1. 將資料寫入內部儲存體的檔案中

(1) 將檔案存放在 getFilesDir() 目錄
//**** 方法一 ****//
//取得內部儲存體擺放檔案的目錄
//預設擺放路徑為 /data/data/[package.name]/files/
File dir = context.getFilesDir();

//在該目錄底下開啟或建立檔名為 "test.txt" 的檔案
File outFile = new File(dir, "test.txt");

//將資料寫入檔案中,若 package name 為 com.myapp
//就會產生 /data/data/com.myapp/files/test.txt 檔案
writeToFile(outFile, "Hello! 大家好");

...

//writeToFile 方法如下
private void writeToFile(File fout, String data) {
    FileOutputStream osw = null;
    try {
        osw = new FileOutputStream(fout);
        osw.write(data.getBytes());
        osw.flush();
    } catch (Exception e) {
        ;
    } finally {
        try {
            osw.close();
        } catch (Exception e) {
            ;
        }
    }
}


//**** 方法二 ****//
FileOutputStream out = null;
try {
    //在 getFilesDir() 目錄底下建立 test.txt 檔案用來進行寫入
    out = openFileOutput("test.txt", Context.MODE_PRIVATE);

    //將資料寫入檔案中
    out.write("Hello! 大家好\n".getBytes());
    out.flush();
} catch (Exception e) {
    ;
} finally {
    try {
        out.close();
    } catch (Exception e) {
        ;
    }
}
(2) 將檔案存放在 getCacheDir() 目錄
//取得內部儲存體擺放暫存檔案的目錄
//預設擺放路徑為 /data/data/[package.name]/cache/
File dir = context.getCacheDir();

//在該目錄底下開啟或建立檔名為 "test.txt" 的檔案
File outFile1 = new File(dir, "test.txt");

//也可以使用 File.createTempFile() 來建立暫存檔案
File outFile2 = File.createTempFile("test", ".txt", dir);

//將資料寫入檔案中,若 package name 為 com.myapp
//就會產生 /data/data/com.myapp/cache/test.txt 檔案
writeToFile(outFile1, "Hello! 大家好");

//會產生 /data/data/com.myapp/cache/test-[亂數].txt 檔案
writeToFile(outFile2, "Hello! 大家好");

2. 讀取內部儲存體中的檔案內容

//** 方法一 **//
//取得內部儲存體擺放檔案的目錄
//預設擺放目錄為 /data/data/[package.name]/
File dir = context.getFilesDir();

//開啟或建立該目錄底下檔名為 "test.txt" 的檔案
File inFile = new File(dir, "test.txt");

//讀取 /data/data/com.myapp/test.txt 檔案內容
String data = readFromFile(inFile);

...

//readFromFile 方法如下
private String readFromFile(File fin) {
    StringBuilder data = new StringBuilder();
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new InputStreamReader(
                 new FileInputStream(fin), "utf-8"));
        String line;
        while ((line = reader.readLine()) != null) {
            data.append(line);
        }
    } catch (Exception e) {
        ;
    } finally {
        try {
            reader.close();
        } catch (Exception e) {
            ;
        }
    }
    return data.toString();
}



//** 方法二 **//
FileInputStream in = null;
StringBuffer data = new StringBuffer();
try {
    //開啟 getFilesDir() 目錄底下名稱為 test.txt 檔案
    in = openFileInput("test.txt");

    //讀取該檔案的內容
    BufferedReader reader = new BufferedReader(
                   new InputStreamReader(in, "utf-8"));
    String line;
    while ((line = reader.readLine()) != null) {
       data.append(line);
    }
} catch (Exception e) {
    ;
} finally {
    try {
        in.close();
    } catch (Exception e) {
        ;
    }
}

3. 將資料寫入外部儲存體的檔案中

(1) 檢查外部儲存體的狀態是否可以讀寫
//檢查外部儲存體是否可以進行寫入
public boolean isExtStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

//檢查外部儲存體是否可以進行讀取
public boolean isExtStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}
(2) 將檔案存放在外部儲存體 (私有檔案)
//將檔案存放在 getExternalFilesDir() 目錄
if (isExtStorageWritable()){
    File dir = context.getExternalFilesDir(null);
    File outFile = new File(dir, "test.txt");
    writeToFile(outFile, "Hello! 大家好");
}

//將檔案存放在 getExternalCacheDir() 目錄
if (isExtStorageWritable()){
    File dir = context.getExternalCacheDir();
    File outFile = new File(dir, "test.txt");
    writeToFile(outFile, "Hello! 大家好");
}
(3) 將檔案存放在外部儲存體 (公開檔案)
//取得存放公開圖片檔的目錄,並在該目錄下建立 subDir 子目錄
public File getExtPubPicDir(String subDir) {
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), subDir);
    //若目錄不存在則建立目錄
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "無法建立目錄");
    }
    return file;
}

...

//取得外部儲存體存放圖片公開檔案目錄底下的 flowers 子目錄
File path = getExtPubPicDir("flowers");

//在該目錄下建立檔名為 flower.jpg 的檔案
File file = new File(path, "flower.jpg");

//將圖片內容由 App 拷貝到該目錄下
InputStream is = getResources().openRawResource(R.drawable.flower);
OutputStream os = new FileOutputStream(file);

byte[] buffer = new byte[1024];
while (true) {
    int bytesRead = in.read(buffer);
    if (bytesRead == -1) break;
    os.write(buffer, 0, bytesRead);
}

is.close();
os.close();

4. 刪除檔案

當 App 被移除時,Android 系統會刪除所有由該 App 產生存放在內部儲存體的檔案,以及存放在外部儲存體的私有檔案 (Context.getExternalFilesDir() 目錄底下的檔案),不過最好還是在檔案不用時就將它刪除,以免佔用不必要的空間。
//刪除暫存目錄中 test.txt 檔案
File f = new File(context.getCacheDir(),"test.txt");
f.delete();

//刪除 getFilesDir() 目錄底下 test.txt 檔案
context.deleteFile("test.txt");

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *