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

573 阅读8分钟

参考资料

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

正文

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

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

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

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

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() 可查询到字段,发现有这两个字段。

"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。通过源码:

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 查询第一张图片都是类似的,通过源码可以看到在这个时候查询到字段只有:

new String[]{
        MediaStore.Files.FileColumns._ID,
        MediaStore.MediaColumns.MIME_TYPE,
        MediaStore.MediaColumns.DATA}

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

bundle 作为参数查询

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

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;
}

调用:

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

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,但是没有看明白有啥用。

查询全部字段

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};

分页查询字段

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 不一样而已。