Kotlin使用协程实现扫描手机文件

2,460 阅读3分钟

前戏:

在android sdk 28以后就不允许用户获取手机的文件存储的根目录了。开发者只能获取自身app的存储空间。所以在新建项目的时候要将targetSdkVersion 最高设置成28.

这样当你使用

val path= Environment.getExternalStorageDirectory().absolutePath
val rootFile = File(path)

的时候rootFile不会为null。

targetSdkVersion = 29拿到的rootFilenull的。

这样我们就拿到了根目录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上欢迎大家讨论研究。

我对文件的过滤和事件监听做了优化。