前戏:
在android sdk 28以后就不允许用户获取手机的文件存储的根目录了。开发者只能获取自身app的存储空间。所以在新建项目的时候要将targetSdkVersion 最高设置成28.
这样当你使用
val path= Environment.getExternalStorageDirectory().absolutePath
val rootFile = File(path)
的时候rootFile不会为null。
targetSdkVersion = 29拿到的rootFile是null的。
这样我们就拿到了根目录rootFile,我们从这个根目录一次遍历文件。
遍历文件夹:
首先拿到文件夹中的所有文件List。
val rootFiles:Arry<File>? = rootFile.listFiles();
我们从这个rootFiles遍历文件进行判断是文件夹还是文件。
rootFile?.map {
if (it.isDirectory) {
}else{
}
}
如果是文件夹我们继续上述的操作遍历文件夹。
我们写个方法让它完整这个过程
fun scan(dirOrFile:File){
dirOrFile.map{
if (it.isDirectory) {
scan(it)
}else{
//保存这个文件,或者回调
}
}
}
由于文件量巨大,反复递归调用会很消耗性能。所以使用协程完成这以工作。
使用协程遍历文件夹
我们使用CoroutineScope创建一个协程
var mCoroutineScope = CoroutineScope(Dispatchers.IO)
将遍历判断递归调用的地方使用协程完成
fun scan(dirOrFile:File){
mCoroutineScope?.launch(Dispatchers.IO) {
dirOrFile.map{
if (it.isDirectory) {
scan(it)
}else{
//保存这个文件,或者回调
}
}
}
}
我们可以使用Log验证这一步骤。不过,你的控制台很有可能会直接爆掉。瞬间大量的log日志会让控制台崩溃的。
管理协程
因为使用了CoroutineScope管理协程,我们可以在中途直接取消扫描。
mCoroutineScope?.cancel()
不过当你取消之后,将无法再重新开启这些协程任务了。
我们需要重新实例化一个协程CoroutineScope。
再次调用扫描的时候我们判断一下当前的协程是否已经停止活动了,如果停止了,对其重新实例化。
if (mCoroutineScope == null || mCoroutineScope?.isActive == false) {
mCoroutineScope = CoroutineScope(Dispatchers.IO)
}
流程梳理
1.我们需要一个需要扫描的文件夹路径,通过这个路径拿到这个文件夹。
2.在协程中遍历文件夹的子项。
3.遍历的文件项如果是文件保存FIle信息,如果是文件夹,继续执行2.- 3.的操作。
使用代码完成一下这个流程:
代码流程
var mCoroutineScope: CoroutineScope? = null
fun startScan(scanPath:String){
//检查路径可用性
val rootFile=File(scanPath)
if(rootFile==null){
return
}
//检查协程的活动状态
if (mCoroutineScope == null || mCoroutineScope?.isActive == false) {
mCoroutineScope = CoroutineScope(Dispatchers.IO)
}
//扫描整个文件夹
scan(rootFile)
}
fun scan(dirOrFile:File){
mCoroutineScope?.launch(Dispatchers.IO) {
dirOrFile.map{
if (it.isDirectory) {
scan(it)
}else{
//保存这个文件,或者回调
Log.d(TAG,"扫描的文件 ${it.name}")
//fileList.add(it)//可以保存文件
//scanListener?.callBack(it)//或者使用接口返回文件
//--withContext(Dispatchers.Main){}--//更新UI会让页面卡顿
}
}
}
}
设计一个完成监听
由于扫描的过程中我们只负责保存扫描到的文件,我们下需要知道何时扫描完成。
因此,我们加一个标记。
var mCoroutineSize = 0
fun scan(dirOrFile:File){
plusCoroutineSize()
.....
}
@Synchronized
fun plusCoroutineSize() {
mCoroutineSize++
}
每当调用此方法必会执行一次协程代码,产生一个新的线程。我们记录一下,一共执行多少次,开启了多少个线程。
然后在每一个协程开启的线程执行完毕,结束的时候,我们再将这个mCoroutineSize--,然后检查它是否已经等于0了。如果等于0说明此时此刻已经没有线程被开启了。所以判断为扫描完毕。
mCoroutineScope?.launch(Dispatchers.IO) {
//最后
checkCoroutineSize()
}
@Synchronized
fun checkCoroutineSize(){
mCoroutineSize--
if (mCoroutineSize == 0) {
mCoroutineSize?.launch(Dispatchers.Main) {
//Toast...提示完成
//Log....提示完成
//scanListener?.scanComplete()...//一接口的形式通知完成
}
}
}
由于mCoroutineSize变量的递增和递减都是由@Synchronized标记的函数执行。是同步的。而且,再递归调用的时候永远是先曾后减,所以mCoroutineSize不会出现小于0的可能。
由此,一个看似完整的扫描遍历过程就完成了。
文件过滤一下
当然我们也可以在扫描到文件或者文件夹的时候,加入文件过滤规则。
比如这样,创建一个FilenameFilter然后自己定义其过滤规则在判断文件的时候对这个规则进行验证。
验证通过继续执行,否者不执行
if (it.isDirectory) {
if(filterFile(it)){
scan(it)
}
}else{
if(filterFile(it)){
//保存这个文件,或者回调
Log.d(TAG,"扫描的文件 ${it.name}")
}
}
var mCallBackFilter: FilenameFilter
fun filterFile(file:File):Boolean{
return mCallBackFilter!!.accept(file, file.name)
}
还有一种方式,是在获取文件List的时候加入文件过滤规则
file.listFiles(mCallBackFilter)
我不是很推荐这样的作法了。如果只是获取这个文件夹的内容,而不考虑其子文件夹项是否还有自己需要的文件可以这么做。
这样过滤很有很有可能会将子项的文件夹一起过滤掉,从而影响扫描的精确度。还是建议在获取到具体文件时进行规则判断。
以上
我将之上的代码进行了封装和丰富。并开源在了GitHub上欢迎大家讨论研究。
我对文件的过滤和事件监听做了优化。