Android 10设备contentResolver.query调用报错原因排查
问题根因
崩溃是两个问题叠加导致的,你测试的设备(三星Note9、官方模拟器)都做了兼容宽松处理,所以无法复现:
- Android 10 分区存储限制拦截查询
Android 10是首个默认启用分区存储的版本,当应用targetSdkVersion >= 29且未配置 legacy 存储标志时,AOSP原生逻辑会禁止通过MediaStore.Audio.Media.DATA(文件真实路径字段)做查询筛选,直接抛出异常。你测试的三星Galaxy Note9在Android10版本对MediaStore做了厂商定制,放宽了DATA字段的访问限制,官方模拟器也未严格执行该拦截逻辑,而部分严格遵循原生规范的小众Android10机型会直接触发崩溃。
错误日志截图如下:
- 查询参数不匹配的边界逻辑bug
现有代码的selection语句拼接和参数数组赋值存在边界漏洞:- 初始化时默认给
selectionPlaceHolders拼了1个DATA like ?占位符,但当carpetasMap.size() == 0且notClickedOnce == false时,carpetasArray始终为null,占位符数量(1个)和传入的查询参数(null)完全不匹配,部分机型的MediaProvider会直接抛出参数非法异常,不会返回null cursor走你写的错误处理分支。 - 当
rutas.length > 2时,你只给carpetasArray的0、1索引赋值,剩余索引位置为null,同样会触发参数异常。
- 初始化时默认给
修复方案
按优先级依次处理即可:
- 补全分区存储兼容配置
若当前应用targetSdkVersion <= 30,在AndroidManifest.xml的<application>标签下添加属性:
可临时绕过Android10的分区存储限制,避免DATA字段查询被系统拦截。android:requestLegacyExternalStorage="true" - 修复查询参数拼接逻辑
重构selection拼接流程,不要提前初始化占位符,改为遍历所有有效筛选路径时动态拼接,保证占位符数量和selectionArgs数组长度完全一致;补全carpetasMap.size() == 0 && notClickedOnce == false的分支逻辑,该场景下直接return终止查询,不要执行query调用。
参考修正后的核心代码:// 权限判断逻辑保持不变 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { return; } List<String> queryArgs = new ArrayList<>(); StringBuilder selectionBuilder = new StringBuilder(); Map<String, ?> carpetasMap = carpetasPreferences.getAll(); // 拼接用户配置的文件夹筛选条件 for (Map.Entry<String, ?> entry : carpetasMap.entrySet()) { if (selectionBuilder.length() > 0) { selectionBuilder.append(" OR "); } // Android10+优先用RELATIVE_PATH,低版本用DATA if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { selectionBuilder.append(MediaStore.Audio.Media.RELATIVE_PATH).append(" like ?"); } else { selectionBuilder.append(MediaStore.Audio.Media.DATA).append(" like ?"); } queryArgs.add("%" + entry.getValue().toString() + "/%"); } // 无用户配置时的默认逻辑 if (carpetasMap.isEmpty() && notClickedOnce) { if (selectionBuilder.length() > 0) { selectionBuilder.append(" OR "); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { selectionBuilder.append(MediaStore.Audio.Media.RELATIVE_PATH).append(" like ?"); queryArgs.add("%Music/%"); // 多路径场景依次追加即可,保证每加一个占位符就加一个对应参数 for (int i = 1; i < rutas.length; i++) { selectionBuilder.append(" OR ").append(MediaStore.Audio.Media.RELATIVE_PATH).append(" like ?"); queryArgs.add("%" + rutas[i] + "%"); } } else { selectionBuilder.append(MediaStore.Audio.Media.DATA).append(" like ?"); queryArgs.add("%" + Environment.getExternalStorageDirectory().getAbsolutePath() + "/Music/%"); for (int i = 1; i < rutas.length; i++) { selectionBuilder.append(" OR ").append(MediaStore.Audio.Media.DATA).append(" like ?"); queryArgs.add("%" + rutas[i] + "%"); } } } // 无任何有效筛选条件直接返回 if (selectionBuilder.length() == 0) { return; } ContentResolver contentResolver = getContentResolver(); Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Cursor cursor = null; try { cursor = contentResolver.query(uri, null, selectionBuilder.toString(), queryArgs.toArray(new String[0]), null); } catch (SecurityException | IllegalArgumentException e) { // 捕获系统层面的查询异常,避免崩溃 e.printStackTrace(); } if (cursor == null) { // 查询失败处理 } else if (!cursor.moveToFirst()) { // 无媒体文件处理 cursor.close(); } else { int idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID); do { long thisId = cursor.getLong(idColumn); listaCanciones.add(thisId); } while (cursor.moveToNext()); cursor.close(); mp.setAudioStreamType(AudioManager.STREAM_MUSIC); } - 加查询异常兜底
给contentResolver.query调用加try-catch,捕获SecurityException、IllegalArgumentException两类系统可能抛出的异常,避免不同厂商ROM的定制逻辑导致崩溃。 - 长期适配建议
后续升级targetSdk到30+时,完全移除对DATA字段的依赖,Android10及以上版本统一使用RELATIVE_PATH、BUCKET_ID等MediaStore公开字段做文件夹筛选,不要依赖文件系统真实路径做查询。
内容的提问来源于stack exchange,提问作者Mike087
相关产品推荐
相关产品推荐

