首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計疑難解答

Android 6.0+ 存儲權限變更:ContentResolver 查詢返回 null 的根因與解決方案

     編輯:程式設計疑難解答

在 Android 6.0 (API 23) 引入**執行時權限** 模型後,`getContentResolver().query(uri, ...)` 在部分機型 (如小米 4 升級 6.0) 返回 `null` 導致空指針崩潰,成為經典遷移坑。本文解析核心原因並給出標準化適配方案。

現象與根因

  • 現象:`Cursor cursor = getContentResolver().query(uri, proj, null, null, null);` 返回 `null`,後續 `cursor.moveToFirst()` 拋 `NullPointerException`。同代碼在舊版本/其他機型正常。
  • 根因:Android 6.0+ **必須在運行時動態申請 `READ_EXTERNAL_STORAGE` 權限**,否則 `ContentProvider` (如 `MediaStore`) 拒絕提供數據,直接返回 `null` 不拋 `SecurityException` (這是框架層為兼容舊應用的「靜默失敗」設計)。
  • 機型差異:國產 ROM (小米、華為、OPPO 等) 對權限管控更嚴格,甚至在 `targetSdkVersion < 23` 時也強制要求運行時權限,導致「其他手機正常、小米崩」的假象。

標準化適配代碼 (Activity 內)

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();
        }
    }
}

進階:Storage Access Framework (SAF) 徹底替代方案

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();
    }
}

關鍵最佳實踐總結

  1. 必做運行時權限檢查:`targetSdkVersion >= 23` 時,`READ_EXTERNAL_STORAGE` / `WRITE_EXTERNAL_STORAGE` 必須動態申請。
  2. 檢查 `Cursor` 非空:任何 `ContentResolver.query()` 返回值**必須判空** 再使用,防禦性編程。
  3. 子執行緒查詢:`MediaStore` 查詢可能耗時,嚴禁主執行緒執行,防 ANR。
  4. 優先用 SAF (`ACTION_OPEN_DOCUMENT`):免權限、免適配、體驗更好、支援雲端文件、權限自動持久化。
  5. 適配 `Scoped Storage` (Android 10+):直接訪問 `MediaStore.DATA` 路徑已受限,應改用 `openInputStream(uri)` 或 `MediaStore` 列查詢 `_ID`/`DISPLAY_NAME`/`MIME_TYPE` 等非路徑列。

徹底理解「權限守門 → Provider 返回 null」的機制鏈條,並拥抱 SAF 現代文件選擇機制,即可從根本上消除此類崩潰,構建跨版本、跨機型穩健的文件訪問能力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved