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 再试。
企业数字化 ERP 产品动态
相关推荐
公众号文章批量抓取与静态博客搭建实战 1. 从"翻历史消息翻到手指抽筋"说起公众号文章有个很别扭的地方:你明明记得某天看过一篇特别有用的干货,但想再找回来的时候,只能在那条对话里往上滑,滑到天荒地老。微信自带的搜索功能对公众号历史文章的覆盖也不算友好… · 2026/9/26 15:09:34
【分享】中小学教材课本 PDF 资源获取指南:用 TaoToken 统一 Key 打通 AI 工具链 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 15:09:27
VS Code最强AI插件大盘点:TaoToken统一Key接入与效率翻倍配置指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 15:09:27
客房部绩效考核管理制度与绩效提升策略 客房部作为酒店服务质量的前线窗口,其员工绩效直接关系到客户体验与经营成效。为了保证服务标准与团队效率,科学的绩效考核机制成为日常管理的核心抓手。传统评分方式面临主观性强、数据价值未被充分利用等问题,推动考核机制升级成为必然趋势。
本文聚焦客房部绩效考核制度… · 2026/9/26 15:37:30
大堂经理绩效考核指标量表与提升路径 在高竞争的酒店行业中,前厅部作为客户接触最直接的核心区域,其管理质量直接决定了客户的第一印象和整体满意度。大堂副理作为前厅管理的关键岗位,既承担着运营执行责任,也代表着服务质量标准。
本文围绕大堂副理的绩效考核体系,详解各项KPI指标的设置与应用,并通过统计分… · 2026/9/26 15:37:24
【运维监控】Prometheus+grafana监控spring boot 3运行情况 运维监控系列文章入口:【运维监控】系列文章汇总索引 最近在研究 AI BI(智能数据分析) 的落地实践。
敬请期待后续专题实战系列:《从零手把手教你搭建 AI 驱动的 BI 系统》,将覆盖 Text2SQL、多轮对话、语义层、权限… · 2026/9/26 15:37:24
【Claude Code】最佳实践:用 TaoToken 统一 Key 打通翻译工作流 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 15:37:17
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46