存取應用程式特定檔案

在許多情況下,應用程式會建立其他應用程式不需要或不應存取的檔案。系統提供以下用來儲存此類應用程式特定檔案的位置:

  • 內部儲存空間目錄:這些目錄包含儲存永久檔案的專屬位置,以及另一個用於儲存快取資料的位置。系統會防止其他應用程式存取這些位置;而在 Android 10 (API 級別 29) 及以上版本中,這些位置均已加密。這些特性會使這些位置非常適合儲存僅限應用程式本身存取的機密資料。

  • 外部儲存目錄:這些目錄包含儲存永久檔案的專屬位置,以及另一個用於儲存快取資料的位置。雖然其他應用程式在擁有適當權限的情況下,可以存取這些目錄,但這些目錄中儲存的檔案僅供您的應用程式使用。如果您想建立其他應用程式可存取的檔案,請改為將這些檔案儲存在外部儲存空間的共用儲存空間中。

使用者解除安裝應用程式時,系統會移除儲存在應用程式特定儲存空間中的檔案。有鑑於這項行為,您不應將此儲存空間用於儲存使用者期待在應用程式外保留的檔案。舉例來說,如果應用程式允許使用者拍攝相片,使用者會預期在解除安裝應用程式後,仍能存取這些相片。因此,您應改用共用儲存空間,將這類檔案儲存至適當的媒體集合

以下各節說明如何儲存及存取應用程式特定目錄中的檔案。

從內部儲存空間存取

系統會為每個應用程式提供內部儲存空間內的目錄,以供應用程式整理其檔案。一個目錄專為應用程式的永久檔案設計,另一個則包含應用程式的快取檔案。應用程式無須任何系統權限,即可在這些目錄中讀取及寫入檔案。

其他應用程式無法存取儲存在內部儲存空間中的檔案。如要讓其他應用程式無法存取應用程式資料,內部儲存空間是不錯的選擇。

但請記住,這些目錄通常較小。將應用程式特定檔案寫入內部儲存空間之前,您的應用程式應在裝置上查詢可用空間

存取永久檔案

應用程式的一般永久檔案位於目錄中,您可以透過結構定義物件的 filesDir 屬性存取該目錄。該架構提供幾種方法,可協助您存取及儲存此目錄中的檔案。

存取及儲存檔案

您可以使用 File API 來存取及儲存檔案。

為協助維持應用程式的效能,請勿多次開啟和關閉同一個檔案。

下列程式碼片段展示如何使用 File API:

Kotlin

val file = File(context.filesDir, filename)

Java

File file = new File(context.getFilesDir(), filename);

使用串流儲存檔案

除了使用 File API,您可以呼叫 openFileOutput() 以取得寫入 filesDir 目錄中檔案的 FileOutputStream

下列程式碼片段顯示了如何將一些文字寫入檔案:

Kotlin

val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(fileContents.toByteArray())
}

Java

String filename = "myfile";
String fileContents = "Hello world!";
try (FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE)) {
    fos.write(fileContents.toByteArray());
}

如要允許其他應用程式存取儲存在內部儲存空間內此目錄中的檔案,請使用 FileProvider 搭配 FLAG_GRANT_READ_URI_PERMISSION 屬性。

使用串流存取檔案

如要以串流形式讀取檔案,請使用 openFileInput()

Kotlin

context.openFileInput(filename).bufferedReader().useLines { lines ->
    lines.fold("") { some, text ->
        "$some\n$text"
    }
}

Java

FileInputStream fis = context.openFileInput(filename);
InputStreamReader inputStreamReader =
        new InputStreamReader(fis, StandardCharsets.UTF_8);
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
    String line = reader.readLine();
    while (line != null) {
        stringBuilder.append(line).append('\n');
        line = reader.readLine();
    }
} catch (IOException e) {
    // Error occurred when opening raw file for reading.
} finally {
    String contents = stringBuilder.toString();
}

檢視檔案清單

您可以呼叫 fileList() 來取得包含 filesDir 目錄中所有檔案名稱的陣列,如以下程式碼片段所示:

Kotlin

var files: Array<String> = context.fileList()

Java

Array<String> files = context.fileList();

建立巢狀目錄

您還可以呼叫採用 Kotlin 程式碼的 getDir(),或將根目錄和新目錄名稱傳遞至採用 Java 程式碼的 File 建構函式,來建立巢狀目錄或是開啟內部目錄:

Kotlin

context.getDir(dirName, Context.MODE_PRIVATE)

Java

File directory = context.getFilesDir();
File file = new File(directory, filename);

建立快取檔案

如果只需暫時儲存機密資料,您應在內部儲存空間內使用應用程式指定的快取目錄來儲存資料。如同所有應用程式特定儲存空間,此目錄中儲存的檔案會在使用者解除安裝應用程式時一併移除,但此目錄中的檔案也可能在解除安裝前就遭到移除

如要建立快取檔案,請呼叫 File.createTempFile()

Kotlin

File.createTempFile(filename, null, context.cacheDir)

Java

File.createTempFile(filename, null, context.getCacheDir());

您的應用程式會使用結構定義物件的 cacheDir 屬性和 File API 來存取此目錄中的檔案:

Kotlin

val cacheFile = File(context.cacheDir, filename)

Java

File cacheFile = new File(context.getCacheDir(), filename);

移除快取檔案

雖然 Android 有時會自行刪除快取檔案,但不應單靠系統清除這些檔案。您應持續維護在內部儲存空間內應用程式的快取檔案。

如要從內部儲存空間的快取目錄中移除檔案,請使用下列方法之一:

  • 代表檔案的 File 物件上的 delete() 方法:

    Kotlin

    cacheFile.delete()

    Java

    cacheFile.delete();
  • 應用程式結構定義的 deleteFile() 方法,採用以下檔案名稱傳遞:

    Kotlin

    context.deleteFile(cacheFileName)

    Java

    context.deleteFile(cacheFileName);

從外部儲存空間存取

如果內部儲存空間不足,無法儲存應用程式特定檔案,請考慮改用外部儲存空間。系統提供外部儲存空間目錄,以便應用程式在其中整理檔案,僅向使用者提供應用程式內的值。一個目錄專為應用程式永久檔案設計,另一個則包含應用程式的快取檔案

在 Android 4.4 (API 級別 19) 或以上版本中,您的應用程式無須請求任何儲存空間相關權限,即可存取外部儲存空間內的應用程式特定目錄。解除安裝應用程式時,同時也會移除儲存在這些目錄中的檔案。

在搭載 Android 9 (API 級別 28) 或以下版本的裝置上,應用程式可存取屬於其他應用程式的應用程式特定檔案 (前提是應用程式具備適當的儲存空間權限)。為了讓使用者進一步控管檔案,並避免檔案過於散亂,根據預設,指定 Android 10 (API 級別 29) 及以上目標版本的應用程式會取得限定範圍外部儲存空間的存取權,或稱為限定範圍儲存空間。限定範圍儲存空間啟用時,應用程式將無法存取屬於其他應用程式的應用程式特定目錄。

確認儲存空間是否可用

由於外部儲存空間位於使用者可能會移除的實體磁碟區,因此請務必先確認該磁碟區可供存取,再嘗試從外部儲存空間讀取應用程式特定資料,或是將應用程式特定資料寫入外部儲存空間。

您可以呼叫 Environment.getExternalStorageState() 來查詢該磁碟區的狀態。如果傳回的狀態為 MEDIA_MOUNTED,則可在外部儲存空間內讀取及寫入應用程式特定檔案。如為 MEDIA_MOUNTED_READ_ONLY,則只能讀取這些檔案。

舉例來說,可使用下列方法確定儲存空間可用性:

Kotlin

// Checks if a volume containing external storage is available
// for read and write.
fun isExternalStorageWritable(): Boolean {
    return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

// Checks if a volume containing external storage is available to at least read.
fun isExternalStorageReadable(): Boolean {
     return Environment.getExternalStorageState() in
        setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}

Java

// Checks if a volume containing external storage is available
// for read and write.
private boolean isExternalStorageWritable() {
    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}

// Checks if a volume containing external storage is available to at least read.
private boolean isExternalStorageReadable() {
     return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ||
            Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
}

在沒有卸除式外部儲存空間的裝置上,請使用下列指令來啟用測試外部儲存空間可用性邏輯的虛擬化磁碟區:

adb shell sm set-virtual-disk true

請選取實體儲存空間位置

有時,裝置會配置其內部記憶體的分區來做為外部儲存空間,同時提供 SD 卡插槽。也就是說,裝置具有多個可能含外部儲存空間的實體磁碟區,因此您必須選取要用於應用程式特定儲存空間的實體磁碟區。

如要存取其他位置,請呼叫 ContextCompat.getExternalFilesDirs()。如程式碼片段所示,系統會將傳回陣列中的第一個元素視為主要外部儲存空間磁碟區。除非空間已滿或無法使用,否則請使用此磁碟區。

Kotlin

val externalStorageVolumes: Array<out File> =
        ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]

Java

File[] externalStorageVolumes =
        ContextCompat.getExternalFilesDirs(getApplicationContext(), null);
File primaryExternalStorage = externalStorageVolumes[0];

存取永久檔案

如要從外部儲存空間存取應用程式特定檔案,請呼叫 getExternalFilesDir()

為協助維持應用程式的效能,請勿多次開啟和關閉同一個檔案。

下列程式碼片段展示如何呼叫 getExternalFilesDir()

Kotlin

val appSpecificExternalDir = File(context.getExternalFilesDir(null), filename)

Java

File appSpecificExternalDir = new File(context.getExternalFilesDir(null), filename);

建立快取檔案

如要將應用程式特定檔案新增至外部儲存空間內的快取,請參閱 externalCacheDir 參考資料:

Kotlin

val externalCacheFile = File(context.externalCacheDir, filename)

Java

File externalCacheFile = new File(context.getExternalCacheDir(), filename);

移除快取檔案

如要從外部快取目錄中移除檔案,請在代表該檔案的 File 物件上使用 delete() 方法:

Kotlin

externalCacheFile.delete()

Java

externalCacheFile.delete();

媒體內容

如果應用程式使用的媒體檔案只會在應用程式內向使用者提供值,建議您最好將這些檔案儲存在外部儲存空間內的應用程式特定目錄中,如以下程式碼片段所示:

Kotlin

fun getAppSpecificAlbumStorageDir(context: Context, albumName: String): File? {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    val file = File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName)
    if (!file?.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created")
    }
    return file
}

Java

@Nullable
File getAppSpecificAlbumStorageDir(Context context, String albumName) {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (file == null || !file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

請務必使用依 API 常數 (例如 DIRECTORY_PICTURES) 提供的目錄名稱。這些目錄名稱可確保系統能正確處理檔案。如果預先定義的子目錄名稱均不適合您的檔案,則可將 null 傳遞至 getExternalFilesDir()。這會傳回外部儲存空間內的根應用程式特定目錄。

查詢可用空間

許多使用者的裝置儲存空間有限,因此應用程式應謹慎取用儲存空間。

如果您事先知道自己儲存的資料量,則可以呼叫 getAllocatableBytes() 來瞭解裝置有多少可用空間可提供給應用程式。getAllocatableBytes() 的傳回值可能會大於裝置目前的可用空間大小。這是因為系統已識別出可從其他應用程式快取目錄移除的檔案。

如果有足夠的空間可儲存應用程式資料,請呼叫 allocateBytes()。否則,您的應用程式可要求使用者從裝置移除部分檔案,或從裝置中移除所有快取檔案

以下程式碼片段顯示應用程式如何查詢裝置上的可用空間:

Kotlin

// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
        appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
    val storageIntent = Intent().apply {
        // To request that the user remove all app cache files instead, set
        // "action" to ACTION_CLEAR_APP_CACHE.
        action = ACTION_MANAGE_STORAGE
    }
}

Java

// App needs 10 MB within internal storage.
private static final long NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

StorageManager storageManager =
        getApplicationContext().getSystemService(StorageManager.class);
UUID appSpecificInternalDirUuid = storageManager.getUuidForPath(getFilesDir());
long availableBytes =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid);
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
            appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP);
} else {
    // To request that the user remove all app cache files instead, set
    // "action" to ACTION_CLEAR_APP_CACHE.
    Intent storageIntent = new Intent();
    storageIntent.setAction(ACTION_MANAGE_STORAGE);
}

建立儲存空間管理活動

應用程式可以宣告並建立自訂活動,在啟動後,可讓使用者管理應用程式儲存在使用者裝置上的資料。您可以利用資訊清單檔案中的 android:manageSpaceActivity 屬性,來宣告此自訂「管理空間」活動。即使應用程式未匯出活動 (也就是您的活動將 android:exported 設定為 false 時),檔案管理員應用程式也可以叫用此活動

要求使用者移除部分裝置檔案

如要要求使用者選擇要從裝置上移除的檔案,請叫用含有 ACTION_MANAGE_STORAGE 動作的意圖。這項意圖會向使用者顯示提示。如有需要,該提示可顯示裝置上的可用空間大小。如要顯示使用者容易理解的資訊,請使用下列計算結果:

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

要求使用者移除所有快取檔案

或者,您可以要求使用者在裝置上清除所有應用程式的快取檔案。如要執行此操作,請叫用含有 ACTION_CLEAR_APP_CACHE 意圖動作的意圖。

其他資源

如要進一步瞭解如何將檔案儲存到裝置儲存空間,請參閱下列資源。

影片