这样设计也很好理解,毕竟扫描全盘是非常的耗资源,所以系统肯定要把全盘扫描的权限拿在自己手里不开放出来,避免被第三方 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 中发起一个删除文件的操作,只需要传递进去一个文件的绝对路径即可。
四、操作 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.1 扫描其他类型的媒体文件
在 Android 下,不仅仅只有图片,对于其他媒体文件,使用本文介绍的方法,也是适用的。
5.2 避免某个目录被 MediaStore 扫描
看完到这里应该会知道,哪怕我们什么都不做,在手机下次重启的时候,系统依然会去全盘扫描文件系统,更新 MediaStore。
但是有时候,我们有一些目录下的媒体文件,并不想让 MediaStore 扫描到,例如在 SDCard 上缓存的图片、图标等,这些我们都不想出现在系统相册内。
解决办法其实在官方文档中已经写了。
https://developer.android.com/guide/topics/data/data-storage.html
这里简单说一下,当不需要被 MediaStore 扫描的目录下,创建一个名为 .nomedia
的空文件,它将阻止媒体扫描程序读取这个目录下的媒体文件。也就无法通过 MediaStore 分享给其他程序。
最后
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
下面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题全套解析,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
](https://bbs.csdn.net/topics/618156601)**
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!