Android 設備存儲路徑差異巨大:有無 SD 卡、內部存儲劃分方式、可移動存儲掛載點等。正確獲取可用存儲目錄應使用系統 API 而非硬編碼路徑。
// 返回: /storage/emulated/0/Android/data/com.example.app/files/
// 或: /storage/sdcard1/Android/data/com.example.app/files/ (次級外部存儲)
File appExternalDir = context.getExternalFilesDir(null); // 根目錄
File appExternalPicsDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); // 圖片子目錄
// 卸載應用時自動刪除,無需權限 (API 19+)
// 返回: /storage/emulated/0/Pictures/ 等
File publicPicsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
// 需 READ/WRITE_EXTERNAL_STORAGE 權限 (API 29 以下)
// API 19+ (KitKat)
File[] externalDirs = context.getExternalFilesDirs(null);
// 返回數組: [主外部存儲, 次級外部存儲(SD卡), ...]
// 遍歷查找可用、空間充足的目錄
for (File dir : externalDirs) {
if (dir != null && dir.exists() && dir.canWrite()) {
long freeSpace = dir.getUsableSpace();
if (freeSpace > REQUIRED_SPACE) {
// 使用此目錄
break;
}
}
}
// 返回: /data/user/0/com.example.app/files/
File internalDir = context.getFilesDir();
// 返回: /data/user/0/com.example.app/cache/
File cacheDir = context.getCacheDir();
// 空間有限,僅存關鍵小文件
File chooseStorageDir(Context context, long requiredSpace) {
// 1. 嘗試應用專屬外部存儲 (首選)
File[] externalDirs = context.getExternalFilesDirs(null);
for (File dir : externalDirs) {
if (dir != null && dir.getUsableSpace() >= requiredSpace) {
return dir;
}
}
// 2. 回退內部存儲
File internalDir = context.getFilesDir();
if (internalDir.getUsableSpace() >= requiredSpace) {
return internalDir;
}
// 3. 最後嘗試緩存目錄 (可能被系統清理)
return context.getCacheDir();
}
| API 級別 | 關鍵變化 | 應對策略 |
|---|---|---|
| 19 (KitKat) | 引入 getExternalFilesDirs()、無需權限訪問專屬目錄 | 優先使用專屬目錄 |
| 23 (Marshmallow) | 運行時權限 | 動態申請 WRITE_EXTERNAL_STORAGE |
| 29 (Android 10) | 分區存儲、限制公共目錄訪問 | 遷移至 MediaStore、專屬目錄 |
| 30 (Android 11) | 強制分區存儲、MANAGE_EXTERNAL_STORAGE | 使用 MediaStore、SAF、專屬目錄 |
| 33 (Android 13) | 細粒度媒體權限 (READ_MEDIA_IMAGES 等) | 按類型申請權限 |
getExternalFilesDir():無需權限、應用專屬、自動清理、跨設備兼容dir.getUsableSpace() >= requiredBytesgetExternalFilesDirs() 返回所有可用卷/sdcard/、/storage/emulated/0/ 等不可靠getCacheDir():系統自動清理、無需手動管理
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp");
Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try (OutputStream os = contentResolver.openOutputStream(uri)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
}