ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android 系统音乐播放实战:用 MediaStore + ContentResolver 构建本地音乐播放器

Android 系统音乐播放实战:用 MediaStore + ContentResolver 构建本地音乐播放器 1. 为什么你的本地音乐播放器读不到歌很多 Android 开发者第一次做本地音乐播放器时都会卡在同一个地方代码写完了MediaPlayer也初始化了但列表里一首歌都没有或者点播放直接抛IOException。我试过最典型的情况是——用MediaStore.Video.Media.TITLE去取音频字段结果getColumnIndex返回 -1getString(-1)直接崩。这个场景的核心链路其实就三步MediaStore 查询音频元数据 → ContentResolver 拿到 URI → MediaPlayer 加载播放。听起来简单但 Android 10 之后分区存储Scoped Storage改了规则DATA字段被限制READ_EXTERNAL_STORAGE在 Android 13 上又被拆成了READ_MEDIA_AUDIO。如果你还照着几年前的老教程写真机上大概率跑不通。这篇文章面向的是想快速跑通「读取系统媒体库并播放本地音乐」的 Android 开发者尤其是还在用 Fragment RecyclerView 结构做课程作业或小项目的同学。我会把权限声明、查询投影、播放器初始化、真机验证、以及几个高频报错的排查路径全部给出来代码可以直接复制到你的工程里改包名就能用。需要说明的是本文聚焦的是本地媒体库读取与播放这条链路本身。如果你在开发过程中需要辅助调试接口、管理密钥或者做模型联调可以配合工具链来提效但核心的 MediaStore 逻辑还是得自己写扎实。2. 前置准备权限、依赖与 TaoToken 辅助工具2.1 权限声明要分版本处理Android 的媒体读取权限经历了三次大改你必须按targetSdkVersion分情况声明否则真机上要么不弹窗要么直接拒绝。!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO /READ_EXTERNAL_STORAGE加上maxSdkVersion32是为了兼容 Android 12 及以下Android 13API 33开始必须用READ_MEDIA_AUDIO。如果你只声明了前者在 Android 13 真机上查询会返回空 Cursor而且不报错——这是最坑的地方。运行时申请也要分支// 在 Fragment 或 Activity 中 private static final int REQ_AUDIO 1001; private void requestAudioPermission() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { requestPermissions(new String[]{Manifest.permission.READ_MEDIA_AUDIO}, REQ_AUDIO); } else { requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQ_AUDIO); } }2.2 查询投影不要传 null原始代码里contentResolver.query(..., null, null, null, null)传了 null 投影意思是「返回所有列」。这在 Android 10 之后会触发性能警告而且DATA列在部分机型上直接不可用。正确做法是显式指定你需要的列String[] projection { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.IS_MUSIC };注意这里用的是MediaStore.Audio.Media不是MediaStore.Video.Media。原 excerpt 里用 Video 的常量去取音频字段是典型的复制粘贴错误getColumnIndex会返回 -1。2.3 关于调试辅助工具在真机联调阶段如果你需要快速验证接口返回、管理 API Key 或者做模型对话测试可以用 TaoToken 的控制台来集中管理。它的 API Key 管理页面在 console接入文档在 doc。这部分和 MediaStore 本身无关只是开发流程里的辅助环节按需使用即可。3. 可复制配置查询、适配器与播放器初始化3.1 数据模型与查询方法先定义一个简单的数据类把_ID也存进去因为播放时推荐用ContentUris.withAppendedId构造 URI而不是拼DATA路径。public class MusicBean { public long id; public String title; public String artist; public long duration; public MusicBean(long id, String title, String artist, long duration) { this.id id; this.title title; this.artist artist; this.duration duration; } }查询方法改成显式投影 IS_MUSIC过滤避免把铃声、通知音也扫进来public ListMusicBean queryLocalMusic(Context context) { ListMusicBean list new ArrayList(); ContentResolver resolver context.getContentResolver(); String[] projection { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION }; String selection MediaStore.Audio.Media.IS_MUSIC ! 0; String sortOrder MediaStore.Audio.Media.TITLE ASC; Cursor cursor resolver.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, null, sortOrder); if (cursor ! null) { int idIdx cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); int titleIdx cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); int artistIdx cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); int durIdx cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); while (cursor.moveToNext()) { list.add(new MusicBean( cursor.getLong(idIdx), cursor.getString(titleIdx), cursor.getString(artistIdx), cursor.getLong(durIdx))); } cursor.close(); } return list; }用getColumnIndexOrThrow而不是getColumnIndex这样一旦列名写错会立刻抛异常而不是静默返回 -1 导致后面崩溃。3.2 播放器封装播放时用_ID构造 URI这是 Android 10 推荐的方式public class MusicPlayer { private MediaPlayer player; private Context context; public MusicPlayer(Context context) { this.context context.getApplicationContext(); this.player new MediaPlayer(); player.setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()); } public void play(long audioId) { try { player.reset(); Uri uri ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, audioId); player.setDataSource(context, uri); player.prepare(); player.start(); } catch (IOException e) { Log.e(MusicPlayer, play failed: e.getMessage()); } } public void pause() { if (player.isPlaying()) player.pause(); } public void resume() { player.start(); } public void release() { if (player ! null) { player.release(); player null; } } }注意setAudioAttributes这一步不设置的话在部分机型上会走通话音频通道声音小且不经过媒体音量控制。3.3 RecyclerView 适配器适配器保持简洁点击回调交给外部处理public class MusicAdapter extends RecyclerView.AdapterMusicAdapter.Holder { private final ListMusicBean data; private final OnItemClick listener; public interface OnItemClick { void onClick(int position); } public MusicAdapter(ListMusicBean data, OnItemClick listener) { this.data data; this.listener listener; } NonNull Override public Holder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_music, parent, false); return new Holder(v); } Override public void onBindViewHolder(NonNull Holder h, int position) { MusicBean bean data.get(position); h.title.setText(bean.title); h.artist.setText(bean.artist); h.itemView.setOnClickListener(v - listener.onClick(h.getAdapterPosition())); } Override public int getItemCount() { return data.size(); } static class Holder extends RecyclerView.ViewHolder { TextView title, artist; Holder(View v) { super(v); title v.findViewById(R.id.tv_title); artist v.findViewById(R.id.tv_artist); } } }4. 真机验证从查询到出声的完整步骤4.1 验证查询结果在 Fragment 的onViewCreated里先跑查询用 Log 确认数量ListMusicBean musicList queryLocalMusic(requireContext()); Log.d(MusicTest, found musicList.size() songs); for (MusicBean b : musicList) { Log.d(MusicTest, b.id | b.title | b.artist); }真机上如果found 0 songs先检查权限是否真的授予了。可以在设置里手动确认或者用adb shell dumpsys package your.package | grep permission查看。4.2 验证播放点击列表项触发播放adapter new MusicAdapter(musicList, position - { MusicBean bean musicList.get(position); player.play(bean.id); Log.d(MusicTest, playing: bean.title); }); recyclerView.setAdapter(adapter);成功的话 Logcat 会输出playing: xxx并且真机扬声器出声。如果 Logcat 报setDataSource failed大概率是 URI 构造错了检查audioId是否来自_ID列。4.3 验证暂停与切歌暂停按钮调用player.pause()再点一次调用player.resume()。切歌时重新调用player.play(newId)因为play内部已经做了reset()不需要额外处理。5. 本篇常见错排查5.1 Cursor 返回空但权限已授予最常见的原因是投影里用了MediaStore.Video.Media的常量。音频必须用MediaStore.Audio.Media。另外检查selection里的IS_MUSIC ! 0有些设备上录音文件IS_MUSIC为 0会被过滤掉这是预期行为。5.2 getColumnIndex 返回 -1 导致崩溃getString(-1)会抛IllegalStateException或CursorIndexOutOfBoundsException。解决办法就是全部换成getColumnIndexOrThrow让错误在查询阶段就暴露出来。5.3 Android 13 上不弹权限窗如果你只声明了READ_EXTERNAL_STORAGEAndroid 13 会直接忽略这个权限请求。必须补上READ_MEDIA_AUDIO并且运行时用Build.VERSION.SDK_INT 33分支申请。5.4 MediaPlayer 报 IOException三个排查方向一是 URI 是否有效用ContentUris.withAppendedId构造二是文件是否已被删除查询结果和实际文件可能不同步三是setDataSource之后必须调用prepare()或prepareAsync()直接start()会抛IllegalStateException。5.5 播放没声音但没报错检查setAudioAttributes是否设置了USAGE_MEDIA。另外确认手机媒体音量不是 0这个听起来傻但真的有人踩过。6. 接入与调试工具链本地音乐播放这条链路跑通之后如果你在做更复杂的音频应用比如需要接入语音识别、音频转文字或者模型对话能力可以把 API Key 统一在 api-keys 页面管理接入方式参考 doc。需要快速验证模型返回时用 模型对话 页面直接测试如果是长期做编码类项目或 Agent 开发coding-plan 会更合适。API 端点统一走https://taotoken.net/api不带额外参数。最后留一个实用技巧调试 MediaStore 查询时用adb shell content query --uri content://media/external/audio/media可以直接在命令行看到系统媒体库里的音频记录比反复改代码打 Log 快得多。如果这条命令返回空说明设备上确实没有可被扫描到的音频文件先往Music目录拷一首 mp3 再试。
返回列表