在 Android 6.0 (API 23) 引入**執行時權限** 模型後,`getContentResolver().query(uri, ...)` 在部分機型 (如小米 4 升級 6.0) 返回 `null` 導致空指針崩潰,成為經典遷移坑。本文解析核心原因並給出標準化適配方案。
private static final int REQ_PERMISSION_READ_STORAGE = 100;
public void loadImageFromGallery(Intent data) {
Uri uri = data.getData();
if (uri == null) return;
// 1. 檢查權限 (API 23+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// 2. 無權限 -> 請求權限
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQ_PERMISSION_READ_STORAGE);
return; // 等待 onRequestPermissionsResult 回調
}
}
// 3. 有權限 -> 執行查詢
doQuery(uri);
}
private void doQuery(Uri uri) {
String[] proj = {MediaStore.Images.Media.DATA};
// 建議在子執行緒執行查詢,避免 ANR
new Thread(() -> {
Cursor cursor = getContentResolver().query(uri, proj, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
// 回主執行緒更新 UI
runOnUiThread(() -> handleImagePath(path));
}
} finally {
cursor.close(); // 必須關閉游標
}
} else {
runOnUiThread(() -> Toast.makeText(this, "獲取圖片路徑失敗,請授予存儲權限", Toast.LENGTH_SHORT).show());
}
}).start();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQ_PERMISSION_READ_STORAGE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用戶授權,重新執行查詢
Uri uri = ...; // 需保存原始 uri (如存在 member variable)
doQuery(uri);
} else {
Toast.makeText(this, "未授予存儲權限,無法選擇圖片", Toast.LENGTH_SHORT).show();
}
}
}
Android 4.4+ 推薦使用 **`ACTION_OPEN_DOCUMENT` / `ACTION_PICK`** 讓系統選擇器返回 `Uri`,**無需任何存儲權限** 即可透過 `ContentResolver` 讀取/寫入該 `Uri` 指向的文件 (授權機制)。
// 啟動選擇器 (無需權限聲明)
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQ_PICK_IMAGE);
// onActivityResult 直接使用返回的 uri,無需權限檢查
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQ_PICK_IMAGE && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
// 獲取可持久化的 URI 權限
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 直接查詢/開啟輸入流,絕不返回 null (除非 uri 無效)
new Thread(() -> {
try (InputStream is = getContentResolver().openInputStream(uri)) {
Bitmap bmp = BitmapFactory.decodeStream(is);
runOnUiThread(() -> imageView.setImageBitmap(bmp));
} catch (IOException e) { e.printStackTrace(); }
}).start();
}
}
徹底理解「權限守門 → Provider 返回 null」的機制鏈條,並拥抱 SAF 現代文件選擇機制,即可從根本上消除此類崩潰,構建跨版本、跨機型穩健的文件訪問能力。