處理檔案是程式開發過程中常會碰到的問題,在 Android 平台上讀寫檔案的也是利用 Java 的 File、InputStream 以及 OutputStream 物件來達成。不過 Android 系統對 App 的使用空間與檔案操作有一套自己的管理方式,透過系統提供的 Context 與 Environment 物件可以讓開發人員快速的進行檔案的各種操作。
Google 官方的教學:
https://developer.android.com/training/basics/data-storage/files.html?hl=zh-tw
A. 使用的物件以及方法
- 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)
- 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. 原理說明
-
- 在 Android 設備上的儲存體 (storage) 可分為內部 (internal) 以及外部(external) 兩種,內部儲存體指的是內建的 Flash,外部儲存體指的是外接的 SD 卡。有些設備即使沒有外接的儲存設備,Android 系統也會將儲存體分為內部以及外部兩個區域。因此,內部儲存體一定存在,外部儲存體則不一定,如果沒有外接儲存設備就不會有外部儲存體。
-
- 在預設的情況下 App 會將新建立檔案存在內部儲存體,存在內部儲存體的檔案預設只能被該 App 存取。當 App 被移除時,儲存在該空間的檔案也會一併被刪除。因此,內部儲存體適合用來擺放專屬於該 App 的檔案,當 App 被移除時這些檔案也沒有存在的必要。
-
- 除了內部儲存體外,App 也可以將檔案存放在外部儲存體,放在外部儲存體的檔案可以被其他的 App 讀取。當 App 被移除時,存放在外部儲存體的檔案並不會被移除,唯一的例外是存放在 getExternalFilesDir() 目錄底下的檔案會被移除 (該目錄底下的檔案算是 App 的私有檔案,雖然是放在外部儲存體,不過 App 被移除時系統也會將檔案刪除)。
-
- 在安裝 App 時預設會裝在內部儲存體,也可以在 AndroidManifest.xml 中設定 android:installLocation 屬性,將 App 安裝在外部儲存體 (除非 App 的大小超過內部儲存體的空間大小,否則很少這樣做)。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation=["auto" | "internalOnly" | "preferExternal"] ... </manifest>
- 在預設的情況下 App 具有讀/寫內部儲存體的權限,因此,可以讀取 (read) 或寫入 (write) 內部儲存體裡面的檔案,並不需要在 AndroidManifest.xml 中宣告額外的權限。
- 在安裝 App 時預設會裝在內部儲存體,也可以在 AndroidManifest.xml 中設定 android:installLocation 屬性,將 App 安裝在外部儲存體 (除非 App 的大小超過內部儲存體的空間大小,否則很少這樣做)。
-
- 在預設的情況下,App 具有讀取 (沒有寫入) 外部儲存體的權限,不過這個權限在未來的 Android 版本可能會做調整,因此,若 App 有讀取外部儲存體的需求,最好還是在 AndroidManifest.xml 檔案中宣告 READ_EXTERNAL_STORAGE 的權限會比較保險,如:
<manifest ...> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ... </manifest>
- 如果要將檔案存放在外部儲存體,必須取得寫入外部儲存體的權限才行,因此要在 AndroidManifest.xml 中宣告 WRITE_EXTERNAL_STORAGE 權限:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
如果 App 具有寫入外部儲存體的權限,隱含的意義就是該 App 也同時取得了讀取外部儲存體的權限 (能夠寫入就表示一定能夠讀取)。
- 在預設的情況下,App 具有讀取 (沒有寫入) 外部儲存體的權限,不過這個權限在未來的 Android 版本可能會做調整,因此,若 App 有讀取外部儲存體的需求,最好還是在 AndroidManifest.xml 檔案中宣告 READ_EXTERNAL_STORAGE 的權限會比較保險,如:
- 在 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 的檔案的絕對路徑
- abstract File getFilesDir()
-
- 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: 外部儲存體內建在系統中,無法被移除
- static File getDataDirectory()
- 當 Android 系統發現空間不足時,會將存放在暫存目錄 getCacheDir() 裡面的檔案刪除。因此,App 在執行時不能假設存放在該目錄裡面的檔案一定存在,也不能假設該目錄底下的檔案一定會被系統刪除,最好是在檔案不用時 App 自己將它刪除,以免占用內部儲存體的空間。
- Environment 物件提供 Android 系統環境的相關資訊,包含外部儲存體的狀態,以及相關檔案的擺放位置,如:
- 由於外部儲存體不一定存在,所以在使用前必須先檢查它的狀態,以避免在讀寫時發生錯誤。透過 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: 外部儲存體存在但未被掛載
-
- 外部儲存體的另一個涵義指的是所有 App 的共用空間,對 App 來說存放在外部儲存體的檔案可以分為公開檔案 (public files) 與私有檔案 (private files) 兩種。擺放在 Environment.getExternalStoragePublicDirectory() 目錄底下的為公開檔案,擺放在 Context.getExternalFilesDir() 目錄底下的為私有檔案。
-
- 公開檔案就像是照片或是音樂,由目前 App 產生可以提供其他 App 使用的檔案。私有檔案就像是 App 執行時產生的暫存檔,對其他 App 來說並沒有使用上的價值。擺放在外部儲存體的檔案都可以被其他 App 存取,不過當 App 被移除時,只有私有檔案會被移除,公開檔案並不會被移除。
- 由於公開檔案可以提供其它 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 要擺放的檔案型態不屬於上述那幾類,也可以直接將檔案擺放在根目錄。
- 將資料寫入儲存體時如果造成空間不足就發產生 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");