9、本地媒体扫描:MediaStore API、文件过滤、封面图加载

好,咱们进入第九章。本地媒体扫描,这可以说是车载多媒体应用的「地基」。你想想看,用户上车第一件事是什么?插U盘、连手机、听歌。如果扫描半天扫不出来,或者封面图加载失败,那体验直接就崩了。

我个人习惯把这块内容分成三个层次来讲:怎么扫(MediaStore API)、扫什么(文件过滤)、扫完怎么展示(封面图加载)。咱们一个一个来。

9.1 MediaStore API:别再自己遍历文件了

很多新手会写递归遍历SD卡,自己解析文件后缀。嗯,我刚开始做车载项目时也这么干过。后来发现,这方式有两大坑:一是性能差,几百个文件还好,几千个文件直接卡UI;二是权限问题,Android 10以后分区存储,你根本拿不到完整路径。

正确的做法是用 MediaStore。它是Android官方提供的内容提供者,专门管理多媒体文件。说白了,系统已经帮你扫过一次了,你只需要查数据库就行。

核心思路:通过ContentResolver查询MediaStore,获取音频文件列表。

看个基本查询代码:

// 查询所有音频文件
ContentResolver resolver = context.getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

String[] projection = {
    MediaStore.Audio.Media._ID,
    MediaStore.Audio.Media.TITLE,
    MediaStore.Audio.Media.ARTIST,
    MediaStore.Audio.Media.ALBUM_ID,
    MediaStore.Audio.Media.DATA,
    MediaStore.Audio.Media.DURATION,
    MediaStore.Audio.Media.SIZE
};

Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor != null) {
    while (cursor.moveToNext()) {
        long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID));
        String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
        // ... 处理数据
    }
    cursor.close();
}

这里有个细节:DATA 字段在Android 10以上可能为空。我建议你改用 MediaStore.getMediaUri() 来获取Uri,而不是直接拼路径。

9.2 文件过滤:别把系统铃声也扫进来

直接扫MediaStore会有什么问题?你会发现系统铃声、通知音、录音文件全进来了。用户只想听自己的歌,结果列表里一堆「默认铃声」,这体验能好吗?

所以必须做过滤。我个人习惯用 selection 参数来过滤,而不是扫完再遍历过滤。性能差距很大。

经验之谈:在车载场景下,我一般过滤掉小于30秒的文件,以及大小小于50KB的文件。这些大概率是铃声或碎片音频。

过滤条件示例:

String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"
    + " AND " + MediaStore.Audio.Media.DURATION + " > ?"
    + " AND " + MediaStore.Audio.Media.SIZE + " > ?";

String[] selectionArgs = {
    String.valueOf(30 * 1000),  // 30秒
    String.valueOf(50 * 1024)   // 50KB
};

Cursor cursor = resolver.query(uri, projection, selection, selectionArgs, null);

另外,IS_MUSIC 这个字段其实不太靠谱。有些文件明明是音乐,但系统没标记。我曾经踩过这个坑,后来加了一层白名单过滤:只保留 .mp3.flac.wav.aac 这几种常见格式。

过滤逻辑可以这样:

String mimeTypeFilter = "(" 
    + MediaStore.Audio.Media.MIME_TYPE + " LIKE 'audio/mpeg' OR "
    + MediaStore.Audio.Media.MIME_TYPE + " LIKE 'audio/flac' OR "
    + MediaStore.Audio.Media.MIME_TYPE + " LIKE 'audio/wav' OR "
    + MediaStore.Audio.Media.MIME_TYPE + " LIKE 'audio/aac'"
    + ")";

selection = selection + " AND " + mimeTypeFilter;

9.3 封面图加载:别在主线程解码

封面图加载,这是最容易出性能问题的地方。你想想看,列表滑动时,每个item都要加载封面图,如果处理不好,卡顿是必然的。

MediaStore提供了两种方式获取封面图:

方式 方法 适用场景
通过ALBUM_ID查询 MediaStore.Audio.Albums.getAlbumArtUri(albumId) 按专辑展示时
通过ContentResolver直接读取 MediaStore.Audio.Media.getContentUri() + 缩略图API 单曲展示时

我个人推荐第二种方式,因为更灵活。但要注意,千万不要在主线程解码Bitmap。我曾经见过一个项目,直接在Adapter的bindView里解码封面图,滑动起来跟PPT一样。

正确的做法是用Glide或Coil这类图片加载库。它们内部做了缓存、异步解码、复用池,比你手写强太多。

// 使用Glide加载封面图
Uri albumArtUri = ContentUris.withAppendedId(
    Uri.parse("content://media/external/audio/albumart"), 
    albumId
);

Glide.with(context)
    .load(albumArtUri)
    .placeholder(R.drawable.default_album_art)
    .error(R.drawable.default_album_art)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(imageView);

注意:有些车载设备存储空间有限,Glide的磁盘缓存不要设太大。我一般限制在50MB以内,避免撑爆存储。

9.4 整体流程:一张图说清楚

好,咱们把上面讲的串起来。整个本地媒体扫描的流程,我画了张图:

用户插入U盘/存储卡 1. 监听存储挂载广播 (ACTION_MEDIA_MOUNTED) 2. 通过MediaStore API查询音频文件 3. 文件过滤:时长、大小、MIME类型 4. 异步加载封面图 (Glide/Coil) 展示到列表 注意:不要用FileObserver 广播更可靠 使用ContentResolver 不要自己遍历文件 过滤掉系统铃声 和碎片音频 使用图片加载库 不要手动解码Bitmap

这张图把整个流程串起来了。你注意看,核心就四个步骤:监听挂载、查询MediaStore、过滤、加载封面图。每一步都有坑,咱们上面都讲到了。

9.5 避坑指南

最后,我把自己踩过的坑总结一下,你遇到类似问题可以直接翻到这里:

  • 扫描太慢? 检查是不是在主线程做了数据库查询。MediaStore查询虽然快,但数据量大时也会卡。建议用 AsyncQueryHandler 或者协程。
  • 封面图加载不出来? 检查Uri是否正确。有些设备上 album_art_uri 可能为null,需要准备默认图。
  • 重复扫描? 监听 ACTION_MEDIA_SCANNER_FINISHED 广播,等系统扫描完再查,避免读到不完整数据。
  • U盘拔出时崩溃? 记得监听 ACTION_MEDIA_EJECT,及时释放Cursor和图片缓存。

我的习惯:在车载项目里,我会把扫描结果缓存到本地数据库(Room)。这样下次启动时,先展示缓存数据,后台再重新扫描更新。用户感知不到等待时间。

嗯,关于本地媒体扫描,核心内容就这些。你只要把MediaStore查询、文件过滤、封面图加载这三个点吃透,车载多媒体应用的地基就算打牢了。

公众号:蓝海资料掘金营,微信deep3321