三种方法,刷新 Android 的 MediaStore!让你保存的图片立即出现在相册里!
公众号原标题:测试:“系统相册里怎么看不到我刚保存的图片,是我操作不对吗?”
一、序
Hi,大家好,我是承香墨影!
App 内,创建一个文件并保存文件到本地的需求,是很常见的 I/O 操作。而如果这个文件变成了一张图片,那你涉及到的就不仅仅是一个 I/O 操作了,还需要考虑如何更新 MediaStore,这样才可以在系统相册中,看到它。
这里说的 MediaStore,本质上是 Android 维护的一个文件系统的数据库,它记录了当前磁盘上所有的文件索引,我们可以通过它,快速的查找当前系统的文件。
MediaStore 刷新的时机是不一定的,也就是说,保存的一张图片文件,MediaStore 并不会立即刷新文件系统,将此文件索引记录下来。而系统本身是存在一些自动刷新 MediaStore 的时机,例如:重启手机。表现就是,当你保存了一张图片到本地文件夹中之后,通过文件管理器类的 App,可以在目录下找到这涨照片,但是在系统相册中,是无法立即看到它的,同时你想用诸如 微信、QQ 去分享这张图片的时候,也是找不到的。所以在我们保存图片文件之后,去触发系统刷新 MediaStore 就尤为重要了。
本文就来讲讲,如何在保存图片之后,刷新系统 MediaStore 那些事。
刷新系统 Media 通常有如下几种方式:
- 通过操作 MediaStore 类。
- 发送广播更新 MediaStore。
- 通过操作 MediaScannerConnection 类。
这三种方式,各有优缺点,我们慢慢分析。
二、操作 MediaStore
这里说的操作 MediaStore,实际是操作它的一个内部类 MediaStore.Image.Media
,它提供了几个 inserImage ()
方法,供我们向 MediaStore 中插入图片数据,并产生一个缩略图。
这个方法传递进去的是一个 Bitmap 对象,其余的 title
和 description
分别是图片文件的名称和一段描述。
举个 Kotlin 的例子:
MediaStore.Images.Media.insertImage(
contentResolver,
mShareBitmap!!,
"image_file",
"file")
使用 inserImage()
方法,不需要我们指定路径,会自动将图片保存至 Picture
目录下。它也不支持我们指定路径。如果我们对图片保存的路径没有要求,并且保存的是一个 Bitmap 对象,此方法是非常的方便的。
细心的朋友可能已经发现了 inserImage()
还有一个其他的重载方法,支持我们传递进去一个图片文件路径,不过我并不推荐使用这个方法,因为它会将原本的图片,再 Copy 一份,到 Picture
目录下,也就是说你最终在磁盘上会得到两张相同的图片。
这一点,看源码是最清晰的。它首先使用 BitmapFactory.decodeFile()
方法,得到一个 Bitmap,然后再去调用保存 Bitmap 对象的 inserImage()
方法,所以我们最终在磁盘上会有两张一模一样的图片。
三、发送广播
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 的源码:
本质上,还是 /mnt/sdcard/
路径就认,而 /sdcard/
就无法使用,所以只要我们不硬编码文件路径,这个问题基本上也就不存在。
这里也提醒我们,一定不要在代码里,硬编码文件路径,算是一个编码规范了。
3.3 删除文件后刷新MediaStore
本文一直都在说添加新文件的时候,如何刷新 MediaStore 的问题。但是其实还涉及到另外一个问题,我们删除了一个已经被收录在 MediaStore 中的文件,怎么办?在本文里也顺便讲一下。
既然放在这一小节讲,首先想到的是,直接再发一个广播出去,刷新这个路径,但是查阅最终执行扫描前的 MediaScanner 的 scanSingleFile()
方法,你就会知道这样的方式是行不通的。
在这里可以看到,当你传递进去的文件路径,指向的文件不存在的时候,会直接 return
出去了,就执行不到刷新的逻辑里。
所幸的是,我在 DownloadManager 类中,找到了刷新删除文件的解决办法,依然是通过 ContentResolver 来解决。
这里通过 ContentResolver 来向 MediaStore 中发起一个删除文件的操作,只需要传递进去一个文件的绝对路径即可。
四、操作 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 的源码,有兴趣可以去这里查看:
这是一个系统服务,我到这里就不继续跟下去了,回过头来继续看源码。
不过看到 connect()
方法的时候,那对应的,一定有 disconnect()
方法存在了,前面 bindService()
了一个系统服务,我们一定要有一个时机去调用 unbindService()
,否则就会造成泄露。
MediaScannerConnection 确实提供了 disconnect()
方法,但是我们通过 scanFile()
方法拿不到这个对象。这里处理的非常的巧妙,不需要我们手动去触发 disconnect()
,它是自维护的。
继续看 scanFile()
里被我们忽略的 ClientProxy
类,逻辑都在这里面。
在 scanNextPath()
中,会去判断传递进去的文件路径是否都扫描过,如果已经没有更多需要扫描的路径了,就自己去调用 disconnect()
方法,回收资源。
到这里,也就解答了我们刚才的疑问,MediaScannerConnection 已经帮我们考虑了很多事情,我们只需要调用它的标准 API 就好了。
五、查缺补漏
5.1 扫描其他类型的媒体文件
在 Android 下,不仅仅只有图片,对于其他媒体文件,使用本文介绍的方法,也是适用的。
5.2 避免某个目录被 MediaStore 扫描
看完到这里应该会知道,哪怕我们什么都不做,在手机下次重启的时候,系统依然会去全盘扫描文件系统,更新 MediaStore。
但是有时候,我们有一些目录下的媒体文件,并不想让 MediaStore 扫描到,例如在 SDCard 上缓存的图片、图标等,这些我们都不想出现在系统相册内。
解决办法其实在官方文档中已经写了。
https://developer.android.com/guide/topics/data/data-storage.html
这里简单说一下,当不需要被 MediaStore 扫描的目录下,创建一个名为 .nomedia
的空文件,它将阻止媒体扫描程序读取这个目录下的媒体文件。也就无法通过 MediaStore 分享给其他程序。
当然,一些重要的文件,依然建议放在自己的私有目录下。
六、小结
关于在 MediaStore 刷新图片,本文基本上就算是讲清楚了。我推荐的方法,是使用 MediaScannerConnection 来实现。
你看了本文,还有什么更多的问题可以在留言区讨论,如果觉得好,可以这篇文章,分享给你需要的朋友们。
今天在公众号后台回复成长『成长』,将会得到我整理的一些学习资料,也能回复『加群』,一起学习进步。
推荐阅读:
- 漫画:程序员,你能“管理”好你的产品经理吗?
- App 多语言翻译,机器翻译也能快如闪电!
- 2017 最权威区块链报告(内含下载)
- Google 的 Flutter 学习资料!
- 远程控制智能电视,方案已开源!
三种方法,刷新 Android 的 MediaStore!让你保存的图片立即出现在相册里!的更多相关文章
- 三种方法解决android帮助文档打开慢
三种方法解决android帮助文档打开慢 经查是因为本地文档中的网页有如下两段js代码会联网加载信息,将其注释掉后就好了 <link rel="stylesheet" h ...
- Asp.Net保存session的三种方法
C#中保存Session的三种方法及Web.Config设置 1.保存session到sql server,需要指定Sql Server服务器,这种方法因为要读写数据库最慢 <sessionSt ...
- 【Android】Eclipse自动编译NDK/JNI的三种方法
[Android]Eclipse自动编译NDK/JNI的三种方法 SkySeraph Sep. 18th 2014 Email:skyseraph00@163.com 更多精彩请直接访问SkySer ...
- Android监听点击事件实现的三种方法
监听点击事件实现的三种方法:1.匿名内部类2.外部类3.直接实现接口 1.匿名内部类: package com.jereh.calculator; import android.content.Con ...
- Chrome模拟手机浏览器(iOS/Android)的三种方法,亲测无误!
大网站都有推出自己的手机访问版本页面,不管是新闻类还是视频网站,我们在电脑是无法直接访问到手机网站的,比如我经常访问一个3g.qq.com这个手机站点,如果在电脑上直接打开它,则会跳转到其它页面,一般 ...
- Android开发之去掉标题栏的三种方法,推荐第三种
Android:去掉标题栏的三种方法和全屏的三种方法 第一种:一般入门的时候常常使用的一种方法 onCreate函数中增加下面代码: requestWindowFeature(Window.FEATU ...
- [Android] Android 异步定时任务实现的三种方法(以SeekBar的进度自动实现为例)
[Android] Android 定时异步任务实现的三种方法(以SeekBar的进度自动实现为例) 一.采用Handler与线程的sleep(long)方法 二.采用Handler与timer及Ti ...
- 用Fiddler可以设置浏览器的UA 和 手动 --Chrome模拟手机浏览器(iOS/Android)的三种方法,亲测无误!
附加以一种软件的方法是:用Fiddler可以设置浏览器的UA 以下3种方法是手动的 通过伪装User-Agent,将浏览器模拟成Android设备. 第一种方法:新建Chrome快捷方式 右击桌面上的 ...
- Android中全屏 取消标题栏,TabHost中设置NoTitleBar的三种方法(转)
Android中全屏 取消标题栏,TabHost中设置NoTitleBar的三种方法http://www.cnblogs.com/zdz8207/archive/2013/02/27/android- ...
随机推荐
- Java 持久化之 --io流与序列化操作
1)File类操作文件的属性 1.File类的常用方法 1. 文件的绝对完整路径:getAbsolutePath() 文件名:getName() 文件相对路径:getPath() 文件的上一级目录:g ...
- JavaScript Cookie使用实例
# Session-Cookie // 利用Cookie防止在1分钟内多次提交: function SetCookie (name, value) { var Days = 30; var exp ...
- python全栈开发-json和pickle模块(数据的序列化)
一.什么是序列化? 我们把对象(变量)从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flat ...
- 算法题丨4Sum
描述 Given an array S of n integers, are there elements a, b, c, and d in S such that a + b + c + d = ...
- 如何在Java中避免equals方法的隐藏陷阱
摘要 本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性. 在<Effective Java>的第8项中,Josh Bloch描述了当继 ...
- Docker学习笔记 - Docker的数据卷容器
一.什么是数据卷容器 如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器. 数据卷容器:用于容器间的数据共享,主动挂载宿主机目录,用于其他容器挂载和共享. 二.数据卷容器的操作 1.创建 ...
- SpringMVC架构的项目,js,css等静态文件导入有问题
发生原因 <servlet> <servlet-name>springmvc-mybaits</servlet-name> <servlet-class> ...
- Docker----起步
最近学习了一下的docker相关的东西,下面介绍一下我个人的学习总结和体会.关于docker的详细介绍和优势,在网上随便都可以找得到,就不做介绍了.这个部分的内容比较简单,有Docker基础的朋友可以 ...
- JS中的递归
递归基础 递归的概念 在程序中函数直接或间接调用自己 直接调用自己 简介调用自己 跳出结构,有了跳出才有结果 递归的思想 递归的调用,最终还是要转换为自己这个函数 如果有个函数foo,如果他是递归 ...
- js中获取元素的样式兼容性的写法
1:设计元素的样式:el.style.color="red"||el.style["color"]="red" 获取元素的样式:el.st ...