開發拍照功能時,照片儲存代碼執行無報錯,路徑打印正常,但手機相簿、檔案管理器均找不到圖片,甚至無法通過路徑載入圖片。
WRITE_EXTERNAL_STORAGE,舊版需在 Manifest 靜態宣告/data/data/package 等應用私有目錄,系統媒體掃描器無法掃描Intent.ACTION_MEDIA_SCANNER_SCAN_FILE 讓相簿刷新MediaStore API 或 getExternalFilesDir(Environment.DIRECTORY_PICTURES)<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Android 10+ 可選,避免分區儲存限制 -->
<application android:requestLegacyExternalStorage="true" ...>
// Activity/Fragment 中
private static final int REQUEST_STORAGE = 100;
private void checkAndRequestPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_STORAGE);
} else {
saveImageToGallery(bitmap);
}
} else {
saveImageToGallery(bitmap);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_STORAGE && grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
saveImageToGallery(bitmap);
} else {
Toast.makeText(this, "需要儲存權限才能保存圖片", Toast.LENGTH_SHORT).show();
}
}
private void saveImageToGallery(Bitmap bitmap) {
String fileName = "IMG_" + System.currentTimeMillis() + ".jpg";
// 1. 獲取標準圖片目錄(自動適配 Android 10+ 分區儲存)
File directory = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
// 或公開目錄:Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
if (directory == null) {
Log.e("SaveImage", "無法獲取儲存目錄");
return;
}
File imageFile = new File(directory, fileName);
try (FileOutputStream fos = new FileOutputStream(imageFile)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
// 2. 關鍵步驟:通知媒體掃描器刷新相簿
MediaScannerConnection.scanFile(this,
new String[]{imageFile.getAbsolutePath()},
new String[]{"image/jpeg"},
(path, uri) -> Log.d("SaveImage", "掃描完成: " + uri));
runOnUiThread(() -> Toast.makeText(this, "已保存至相簿: " + imageFile.getAbsolutePath(), Toast.LENGTH_LONG).show());
} catch (IOException e) {
Log.e("SaveImage", "保存失敗", e);
runOnUiThread(() -> Toast.makeText(this, "保存失敗: " + e.getMessage(), Toast.LENGTH_SHORT).show());
}
}
@RequiresApi(api = Build.VERSION_CODES.Q)
private void saveImageToMediaStore(Bitmap bitmap) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_" + System.currentTimeMillis() + ".jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp");
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
getContentResolver().delete(uri, null, null); // 失敗時清理
Log.e("SaveImage", "MediaStore 保存失敗", e);
}
}
}
| 症狀 | 原因 | 解決 |
|---|---|---|
| 程式碼無錯但找不到檔案 | 寫入應用私有目錄 | 使用 getExternalFilesDir() 或公開目錄 |
| 檔案存在但相簿不顯示 | 未觸發媒體掃描 | 呼叫 MediaScannerConnection.scanFile() |
| Android 11+ 直接崩潰 | 分區儲存限制 | 遷移至 MediaStore API |
| 權限已給但寫入失敗 | 目錄不存在 | 檢查 mkdirs() 返回值 |
adb shell ls -la /sdcard/Pictures/ 直接檢查檔案是否存在Android/data/com.your.package/files/Pictures/ 檢查應用專屬目錄MediaScanner 觀察掃描日誌Android 圖片儲存「看似簡單,實則坑多」:權限→路徑→掃描 三步缺一不可。建議統一封裝 ImageSaver 工具類,內部根據 API Level 自動選擇傳統路徑或 MediaStore,業務層一行代碼調用,徹底屏蔽版本差異。