三种方法,刷新 Android 的 MediaStore!让你保存的图片当即出如今相册里!

公众号原标题:测试:“系统相册里怎么看不到我刚保存的图片,是我操做不对吗?”html

1、序

Hi,你们好,我是承香墨影!java

App 内,建立一个文件并保存文件到本地的需求,是很常见的 I/O 操做。而若是这个文件变成了一张图片,那你涉及到的就不只仅是一个 I/O 操做了,还须要考虑如何更新 MediaStore,这样才能够在系统相册中,看到它。android

这里说的 MediaStore,本质上是 Android 维护的一个文件系统的数据库,它记录了当前磁盘上全部的文件索引,咱们能够经过它,快速的查找当前系统的文件。程序员

MediaStore 刷新的时机是不必定的,也就是说,保存的一张图片文件,MediaStore 并不会当即刷新文件系统,将此文件索引记录下来。而系统自己是存在一些自动刷新 MediaStore 的时机,例如:重启手机。表现就是,当你保存了一张图片到本地文件夹中以后,经过文件管理器类的 App,能够在目录下找到这涨照片,可是在系统相册中,是没法当即看到它的,同时你想用诸如 微信、QQ 去分享这张图片的时候,也是找不到的。因此在咱们保存图片文件以后,去触发系统刷新 MediaStore 就尤其重要了。数据库

本文就来说讲,如何在保存图片以后,刷新系统 MediaStore 那些事。缓存

刷新系统 Media 一般有以下几种方式:bash

  • 经过操做 MediaStore 类。
  • 发送广播更新 MediaStore。
  • 经过操做 MediaScannerConnection 类。

这三种方式,各有优缺点,咱们慢慢分析。微信

2、操做 MediaStore

这里说的操做 MediaStore,实际是操做它的一个内部类 MediaStore.Images.Media,它提供了几个 inserImage () 方法,供咱们向 MediaStore 中插入图片数据,并产生一个缩略图。ide

这个方法传递进去的是一个 Bitmap 对象,其他的 titledescription 分别是图片文件的名称和一段描述。学习

举个 Kotlin 的例子:

MediaStore.Images.Media.insertImage(
        contentResolver,
        mShareBitmap!!,
        "image_file",
        "file")
复制代码

使用 inserImage() 方法,不须要咱们指定路径,会自动将图片保存至 Picture 目录下。它也不支持咱们指定路径。若是咱们对图片保存的路径没有要求,而且保存的是一个 Bitmap 对象,此方法是很是的方便的。

细心的朋友可能已经发现了 inserImage() 还有一个其余的重载方法,支持咱们传递进去一个图片文件路径,不过我并不推荐使用这个方法,由于它会将本来的图片,再 Copy 一份,到 Picture 目录下,也就是说你最终在磁盘上会获得两张相同的图片。

这一点,看源码是最清晰的。它首先使用 BitmapFactory.decodeFile() 方法,获得一个 Bitmap,而后再去调用保存 Bitmap 对象的 inserImage() 方法,因此咱们最终在磁盘上会有两张如出一辙的图片。

3、发送广播

3.1 那些广播能够更新 MediaStore

说到广播,在 Android 4.4 以前,是能够经过 ACTION_MEDIA_MOUNTED 广播,来通知系统刷新 MediaStore 的,不过假如你如今还在依赖这条广播,你会获得一个错误信息。

E/AndroidRuntime(23718): java.lang.SecurityException: Permission Denial: not allowed to send broadcast android.intent.action.MEDIA_MOUNTED from pid=23718, uid=10097
复制代码

在 Android 4.4 以后,这个广播只能由系统进行广播,App 只能对该广播进行监听,在当前的系统分布环境下,这条路已经走不通了。

这样设计也很好理解,毕竟扫描全盘是很是的耗资源,因此系统确定要把全盘扫描的权限拿在本身手里不开放出来,避免被第三方 App 滥用。

不过 Android 依然给咱们提供了替代方案,那就是用 MediaScannerConnection 或者发送 ACTION_MEDIA_SCANNER_SCAN_FILE 广播。

接下来就来讲说 ACTION_MEDIA_SCANNER_SCAN_FILE 这个广播。

3.2 使用广播刷新

经过广播刷新 MediaStore 的方式很是的简单,只须要指定文件路径和 Action 就行了。

val saveAs = "Your_Created_Image_File_Path"
val contentUri = Uri.fromFile(File(saveAs))
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,contentUri)
sendBroadcast(mediaScanIntent)
复制代码

正常状况下,它是没有问题的,不过假如你发现它不生效,就须要检查一下你文件的路径是否传递正确。

经过查看 MediaScannerReceiver 的源码,能够发现 onReceive() 方法中,针对 ACTION_MEDIA_SCANNER_SCAN_FILE 还有一个限制条件,那就是传递进去的文件绝对路径,必须是以 Environment.getExternalStorageDirectory() 方法的返回值开头。

有兴趣能够仔细阅读源码,这里是 Android 6.0 的源码:

http://androidxref.com/6.0.0_r1/xref/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerReceiver.java

本质上,仍是 /mnt/sdcard/ 路径就认,而 /sdcard/ 就没法使用,因此只要咱们不硬编码文件路径,这个问题基本上也就不存在。

这里也提醒咱们,必定不要在代码里,硬编码文件路径,算是一个编码规范了。

3.3 删除文件后刷新MediaStore

本文一直都在说添加新文件的时候,如何刷新 MediaStore 的问题。可是其实还涉及到另一个问题,咱们删除了一个已经被收录在 MediaStore 中的文件,怎么办?在本文里也顺便讲一下。

既然放在这一小节讲,首先想到的是,直接再发一个广播出去,刷新这个路径,可是查阅最终执行扫描前的 MediaScanner 的 scanSingleFile() 方法,你就会知道这样的方式是行不通的。

在这里能够看到,当你传递进去的文件路径,指向的文件不存在的时候,会直接 return 出去了,就执行不到刷新的逻辑里。

所幸的是,我在 DownloadManager 类中,找到了刷新删除文件的解决办法,依然是经过 ContentResolver 来解决。

这里经过 ContentResolver 来向 MediaStore 中发起一个删除文件的操做,只须要传递进去一个文件的绝对路径便可。

4、操做 MediaScannerConnection 类

4.1 使用 MediaScannerConnection

刷新 MediaStore 还有一个最通用也是我推荐的一个方法,那就是使用 MediaScannerConnection 进行操做。

不一样于 MediaStore.Image.Media 和广播的方式,使用 MediaScannerConnection 不只能够保存文件,还能够指定文件路径,最好的就是,它还支持刷新完成的回调。

若是咱们对时序有要求,而且须要制定文件保存路径的话,最好的方式就是直接使用 MediaScannerConnection 类进行操做,而且这也应该是兼容最好的方式。

这里咱们主要是利用 MediaScannerConnection 类的 scanFile() 方法进行触发扫描。

经过 scanFile() 方法,咱们只须要制定一个待刷新的文件路径和对应的 MimeType 便可,它支持传递多个路径,也可就是支持批量扫描。

注意这里的 MimeType 是必定要填写的,而且不能写通配符 */*null,不然会致使刷新失败,一般咱们保存的是一个图片的话,只须要传递 image/jpeg 便可。

最后一个参数, onScanCompletedListener 中能够监听咱们扫描的结果,须要注意的是,假如这里扫描的是多个文件路径,它也会被回调屡次。因此若是有什么在刷新以后的后续操做,就须要特殊处理一下(缘由后面是说)。

MediaScannerConnection.scanFile(this
        , arrayOf(picFile.absolutePath)
        , arrayOf("image/jpeg"), { path, uri ->

    Log.i("cxmyDev", "onScanCompleted : " + path)

})
复制代码

scanFile() 方法的使用仍是很简单的,没什么须要额外交代的了。

4.2 MediaScannerConnection 原理

依然是从源码中找答案,咱们先来看看 scanFile() 方法的实现。

scanFile() 里,建立了一个 MediaScannerConnection 并调用了 connect() 方法。接下来咱们继续看 connect() 方法。

connect() 方法中,能够看到,它其实是 bindServer()MediaScannerService 这个系统服务,全部的操做都在 MediaScannerService 中。

MediaScannerService 的源码,有兴趣能够去这里查看:

http://androidxref.com/6.0.0_r1/xref/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java

这是一个系统服务,我到这里就不继续跟下去了,回过头来继续看源码。

不过看到 connect() 方法的时候,那对应的,必定有 disconnect() 方法存在了,前面 bindService() 了一个系统服务,咱们必定要有一个时机去调用 unbindService(),不然就会形成泄露。

MediaScannerConnection 确实提供了 disconnect() 方法,可是咱们经过 scanFile() 方法拿不到这个对象。这里处理的很是的巧妙,不须要咱们手动去触发 disconnect(),它是自维护的。

继续看 scanFile() 里被咱们忽略的 ClientProxy 类,逻辑都在这里面。

scanNextPath() 中,会去判断传递进去的文件路径是否都扫描过,若是已经没有更多须要扫描的路径了,就本身去调用 disconnect() 方法,回收资源。

到这里,也就解答了咱们刚才的疑问,MediaScannerConnection 已经帮咱们考虑了不少事情,咱们只须要调用它的标准 API 就行了。

5、查缺补漏

5.1 扫描其余类型的媒体文件

在 Android 下,不只仅只有图片,对于其余媒体文件,使用本文介绍的方法,也是适用的。

5.2 避免某个目录被 MediaStore 扫描

看完到这里应该会知道,哪怕咱们什么都不作,在手机下次重启的时候,系统依然会去全盘扫描文件系统,更新 MediaStore。

可是有时候,咱们有一些目录下的媒体文件,并不想让 MediaStore 扫描到,例如在 SDCard 上缓存的图片、图标等,这些咱们都不想出如今系统相册内。

解决办法其实在官方文档中已经写了。

https://developer.android.com/guide/topics/data/data-storage.html

这里简单说一下,当不须要被 MediaStore 扫描的目录下,建立一个名为 .nomedia 的空文件,它将阻止媒体扫描程序读取这个目录下的媒体文件。也就没法经过 MediaStore 分享给其余程序。

固然,一些重要的文件,依然建议放在本身的私有目录下。

6、小结

关于在 MediaStore 刷新图片,本文基本上就算是讲清楚了。我推荐的方法,是使用 MediaScannerConnection 来实现。

你看了本文,还有什么更多的问题能够在留言区讨论,若是以为好,能够这篇文章,分享给你须要的朋友们。

今天在公众号后台回复成长『成长』,将会获得我整理的一些学习资料,也能回复『加群』,一块儿学习进步。

推荐阅读:

相关文章
相关标签/搜索