Android文件系统(04)从优秀开源框架中学习MediaStore图片的查询

参考资料

我也想过直接整,但是还是先基于MediaStore 把增删改查写了,因为他本质上是不难的技术,只是说不能经常写,所以比较生疏,从业务场景上讲,查的的场景大于写的场景,而且查询就直接涉及到性能问题,所以通过图片等查询,可以很方便的打好查询的基础。这次的代码直接看PictureSelector 这是一个高星库,还是可以从里面学习到很多东西的。但是我们主要是看图片查询相关的代码。

正文

这个库的实现了蛮多功能的,比如说:

  • 分页查询
  • 目录列表
  • 可选的git
  • 视频和图片一起查询
  • 一次性查询等
  • 指定查询目录,这个是指APP位于外置卡android/data/包名 下的目录,所以通过file 获取MIME type
  • 降序/升序

那么我们一点点基于功能去拆解这个的库的实现思路。

如何实现查询全部和分页查询?

scss 复制代码
mLoader = selectorConfig.isPageStrategy
        ? new LocalMediaPageLoader(getAppContext(), selectorConfig)
        : new LocalMediaLoader(getAppContext(), selectorConfig);

可以看到,分为了两个类。LocalMediaLoader 作为查询全部的loader,LocalMediaPageLoader作为分页查询的loader。

如何做到[(图片、音频、视频一起查询?),(可选gif),(指定目录)]

这个就是技术方案的选择了,那么我们基于LocalMediaLoader,去查找如何实现的。基于上一篇的mediaStore,可以发现他图片查询和视频查询的URL是两个,但是mideaStore 支持查询文件,而恰好我们知道图片和文件的MIME type ,我们再次回顾下query()的入参:

  1. Uri uri:这是查询请求的URI。它标识着要查询的数据,并且是唯一标识符。比如,如果你正在查询联系人,那么你可能使用ContactsContract.Contacts.CONTENT_URI作为你的URI。
  2. String[] projection:这是一个字符串数组,表示你希望查询哪些列。如果你不指定任何列,那么会返回所有的列。如果你想返回特定的列,你可以指定这些列的名称。
  3. String selection:这是一个可选参数,表示查询中的筛选条件。这与SQL语句中的WHERE子句类似。如果你不提供这个参数,那么所有的记录都将被返回。
  4. String[] selectionArgs:这是与selection参数一起使用的参数。它是一个字符串数组,可以替换selection中的占位符。
  5. String sortOrder:这是可选参数,表示返回记录的排序方式。这与SQL语句中的ORDER BY子句类似。如果你不提供这个参数,那么记录的排序方式将由ContentProvider决定。

那么我们就可以写一个基于文件的条件查询,同样的gif 文件也有MIME type 那就是 image/gif,最后是目录,我们知道有目录参数,所以我们子需要判断包含关系即可。

查询所有不包括GIF

  • selection: (media_type=? AND (mime_type!='image/gif') OR media_type=? AND 0 <= duration and duration <= 9223372036854775807) AND 0 <= _size and _size <= 9223372036854775807
  • selectionArgs:["1","3"]

查询图片不包括gif

  • selection: media_type=? AND (mime_type!='image/gif') AND 0 <= _size and _size <= 9223372036854775807
  • selectionArgs: ["1"]

查询视频不包括gif

  • selection:media_type=? AND 0 <= duration and duration <= 9223372036854775807
  • selectionArgs:["3"]

查询音频不包括gif

  • selection : media_type=? AND 0 <= duration and duration <= 9223372036854775807
  • selectionArgs:["2"]

查询指定目录与查询目录

我们结合query() 可查询到字段,发现有这两个字段。

json 复制代码
"bucket_id": "这是包含项目的存储桶的ID。",
"bucket_display_name": "这是包含项目的存储桶的显示名称。",

结合的查询拼接,我们可以直接在selection中和selectionArgs 中添加筛选条件,比如我们想要查询 Pictures 中的图片:

  • selection:media_type=? AND bucket_display_name = ?
  • selectionArgs:["1","Pictures"]

遇到的问题,我在模拟器上查询到的uri 只有等于 MediaStore.Files.getContentUri("external") 才没有崩溃,奔溃的原因大致为:

  • 这个URI查询到的数据里面没有这个字段。

但是,通过代码可以发现这个库使用的是bucket_id 作为查询参数。所以文件或者图片所在目录就是bucket_display_name这个字段。筛选就基于这个处理 bucket_id 处理。

总结

通过上面的参数可以看到,这个组件对于文件类型的判断通过media_type进行了区分。同时约束了文件大小duration。通过源码:

ini 复制代码
public interface FileColumns extends MediaColumns {
    String MEDIA_TYPE = "media_type";
    int MEDIA_TYPE_AUDIO = 2;
    int MEDIA_TYPE_DOCUMENT = 6;
    int MEDIA_TYPE_IMAGE = 1;
    int MEDIA_TYPE_NONE = 0;
    int MEDIA_TYPE_PLAYLIST = 4;
    int MEDIA_TYPE_SUBTITLE = 5;
    int MEDIA_TYPE_VIDEO = 3;
    String MIME_TYPE = "mime_type";
    String PARENT = "parent";
}

我们可以看到,FileColumns定义了很多种类的。AND (mime_type!='image/gif') 表示mime_type 不等于gif,所以这个也是排除gif的语句。可以看到再全部模式下,media从逻辑上是筛选不出来音频的。

例如:图片的URI就没有这个字段,就会导致SQL 拼接的时候发生错误。

如何做到降序或升序?

答案依旧再query() 的入参里面,那就是sortOrder。分别对应下面两个值:

  • MediaStore.MediaColumns.DATE_MODIFIED + " DESC"
  • MediaStore.MediaColumns.DATE_MODIFIED + " ASC"

DATE_MODIFIED作为文件的最后修改时间。

在SQL中,ASC是ascending的缩写,表示升序排列,即从小到大排序。例如,在查询语句中使用"ORDER BY column_name ASC"将按照指定列(column_name)的升序进行排序。

DESC是descending的缩写,表示降序排列,即从大到小排序。例如,在查询语句中使用"ORDER BY column_name DESC"将按照指定列(column_name)的降序进行排序。

如何通过Query() 查询所有相册目录

在LocalMediaLoader未分页的查询模式中,通过query() 查询数据的同时就把相册目录查询出来了。在分页模式下,也没有感觉有什么特殊的点,有点懵。

如何通过bucket_id 查询到相册的首张图作为封面

分页查询和全部查询到在通过bucket_id 查询第一张图片都是类似的,通过源码可以看到在这个时候查询到字段只有:

arduino 复制代码
new String[]{
        MediaStore.Files.FileColumns._ID,
        MediaStore.MediaColumns.MIME_TYPE,
        MediaStore.MediaColumns.DATA}

但是查询的入参发生了变更,在Android R(30) 及其以上的版本,构建了一个Bundle,而在其他版本则还是通过selection、selectionArgs、sortOrder这3个参数提供筛选。

bundle 作为参数查询

通过下列函数提供了一个bundle 对象

arduino 复制代码
public static Bundle createQueryArgsBundle(String selection, String[] selectionArgs, int limitCount, int offset, String orderBy) {
    Bundle queryArgs = new Bundle();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
        queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
        queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, orderBy);
        if (SdkVersionUtils.isR()) {
            queryArgs.putString(ContentResolver.QUERY_ARG_SQL_LIMIT, limitCount + " offset " + offset);
        }
    }
    return queryArgs;
}

调用:

scss 复制代码
data = getContext().getContentResolver().query(QUERY_URI, new String[]{
        MediaStore.Files.FileColumns._ID,
        MediaStore.MediaColumns.MIME_TYPE,
        MediaStore.MediaColumns.DATA}, queryArgs, null);

总结

可以看到,bundle 也是基于selection、selectionArgs、sortOrder作为参数,只是说封装了下,这里的查询条件和上面的查询拼接是一个道理。当获取到Cursor后直接读取第一项即可,但是对于返回封面图片的地址还是有系统版本上的差异的。

当系统版本大于等于29(Android Q)的时候。传入了id 和mimeType,返回了一个URI

ini 复制代码
public static String getRealPathUri(long id, String mimeType) {
    Uri contentUri;
    if (PictureMimeType.isHasImage(mimeType)) {
        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    } else if (PictureMimeType.isHasVideo(mimeType)) {
        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
    } else if (PictureMimeType.isHasAudio(mimeType)) {
        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
    } else {
        contentUri = MediaStore.Files.getContentUri("external");
    }
    return ContentUris.withAppendedId(contentUri, id).toString();
}

其他情况返回的是_data。可以看到这里使用了一个ContentUris.withAppendedId(contentUri, id) 获取URI,同时进行了系统版本的判断,不同的版本采用了不同的策略。

query() 查询文件时查询了哪些字段

我们知道当 projection 为空的时候,查询的是所有的字段。而且不同的Android 版本字段有些差异。同时不同的URI查询到的数据的字段也有差异。 所以这个库使用:MediaStore.Files.getContentUri("external") 查询文件。

这两个字段的区别在于COUNT,但是没有看明白有啥用。

查询全部字段

arduino 复制代码
protected static final String[] PROJECTION = {
        MediaStore.Files.FileColumns._ID,
        MediaStore.MediaColumns.DATA,
        MediaStore.MediaColumns.MIME_TYPE,
        MediaStore.MediaColumns.WIDTH,
        MediaStore.MediaColumns.HEIGHT,
        COLUMN_DURATION,
        MediaStore.MediaColumns.SIZE,
        COLUMN_BUCKET_DISPLAY_NAME,
        MediaStore.MediaColumns.DISPLAY_NAME,
        COLUMN_BUCKET_ID,
        MediaStore.MediaColumns.DATE_ADDED,
        COLUMN_ORIENTATION};

分页查询字段

arduino 复制代码
protected static final String[] ALL_PROJECTION = {
        MediaStore.Files.FileColumns._ID,
        MediaStore.MediaColumns.DATA,
        MediaStore.MediaColumns.MIME_TYPE,
        MediaStore.MediaColumns.WIDTH,
        MediaStore.MediaColumns.HEIGHT,
        COLUMN_DURATION,
        MediaStore.MediaColumns.SIZE,
        COLUMN_BUCKET_DISPLAY_NAME,
        MediaStore.MediaColumns.DISPLAY_NAME,
        COLUMN_BUCKET_ID,
        MediaStore.MediaColumns.DATE_ADDED,
        COLUMN_ORIENTATION,
        "COUNT(*) AS " + COLUMN_COUNT};

总结

整体整下来,还是可以学到不少的东西的。比如:

  • Android 不同版本的查询写法。
  • 不同的URI 查询出来的字段是不一致的,不同的系统版本也不一致,所以选择字段得慎重。
  • query() 条件查询,排序等
  • 通过id 转URI等

当然了,还有很多细碎的知识点,就不罗列了。当然了,还有一个坑,分页没有描述,但是熟悉sqlite做分页的都知道,这玩意简单,就是条件查询。所以就没有描述了。 这个讲道理,从应用层的角度上来说,写到这里已经写无可写了。我们知道MediaStore 支持查询文件,而且PictureSelector 是基于文件查询做的图片查询。 那么,我们写任何一个文件查询都可以参考他的思路,无非就是mime type 不一样而已。

相关推荐
Digitally1 小时前
如何将文件从 iPhone 传输到 Android(新指南)
android·ios·iphone
whysqwhw2 小时前
OkHttp深度架构缺陷分析与演进规划
android
用户7093722538512 小时前
Android14 SystemUI NotificationShadeWindowView 加载显示过程
android
木叶丸2 小时前
跨平台方案该如何选择?
android·前端·ios
顾林海3 小时前
Android ClassLoader加载机制详解
android·面试·源码
用户2018792831673 小时前
🎨 童话:Android画布王国的奇妙冒险
android
whysqwhw4 小时前
OkHttp框架的全面深入架构分析
android
你过来啊你4 小时前
Android App冷启动流程详解
android
泓博4 小时前
KMP(Kotlin Multiplatform)改造(Android/iOS)老项目
android·ios·kotlin
移动开发者1号5 小时前
使用Baseline Profile提升Android应用启动速度的终极指南
android·kotlin