Android扫描文件并统计各类文件数目
Posted 写代码的林克
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android扫描文件并统计各类文件数目相关的知识,希望对你有一定的参考价值。
最近在模仿小米的文件管理器写一个自己的文件管理器,其中有一个功能是全盘扫描文件并显示每类文件的数目。刚开始使用单一线程,扫描速度简直惨不忍睹,换成多线程扫描以后,速度有较明显的提升,便封装了一个工具类,分享出来。
一、遇到的问题
首先描述一下遇到的问题:
1 . android端全盘扫描文件
2 . 开一个子线程扫描太慢,使用多线程扫描
3 . 统计每一类文件的数目(比如:视频文件,图片文件,音频文件的数目)
二、解决思路
接下来描述一下几个点的解决思路:
1 . 首先目录的存储结构是树状结构,这里就设计到了树的遍历,这里我使用树的层次遍历
,使用非递归方法实现,具体的遍历思路后面会有代码,这里只说明是借助于队列完成树的层次遍历。
2 . 第二个思路便是我们需要传入的参数,这里其实涉及到的是数据的存储结构问题,这里我使用的数据结构如下:
Map<String, Set<String>>
解释一下这个数据结构,map的key表示种类,value是个Set这个Set里面包含该种类的文件的后缀名。如下:
Map<String, Set<String>> CATEGORY_SUFFIX = new HashMap<>();
Set<String> set = new HashSet<>();
set.add("mp4");
set.add("avi");
set.add("wmv");
set.add("flv");
CATEGORY_SUFFIX.put("video", set);
set.add("txt");
set.add("pdf");
set.add("doc");
set.add("docx");
set.add("xls");
set.add("xlsx");
CATEGORY_SUFFIX.put("document", set);
set = new HashSet<>();
set.add("jpg");
set.add("jpeg");
set.add("png");
set.add("bmp");
set.add("gif");
CATEGORY_SUFFIX.put("picture", set);
set = new HashSet<>();
set.add("mp3");
set.add("ogg");
CATEGORY_SUFFIX.put("music", set);
set = new HashSet<>();
set.add("apk");
CATEGORY_SUFFIX.put("apk", set);
set = new HashSet<>();
set.add("zip");
set.add("rar");
set.add("7z");
CATEGORY_SUFFIX.put("zip", set);
这里的后缀为什么使用Set来存储呢,主要是考虑到后面需要涉及到查找(获得一个文件的后缀,需要在查找属于哪个类别),Set的查找效率比较高
3 . 前面说了目录的遍历需要借助于队列进行层次遍历,又因为是多线程环境下,所以我们选用线程安全的队列ConcurrentLinkedQueue
ConcurrentLinkedQueue<File> mFileConcurrentLinkedQueue;
4 . 还有需要将统计结果进行存储,这里我也选用了线程安全的HashMap
private ConcurrentHashMap<String, Integer> mCountResult;
这个Map的key表示文件种类,value表示该类文件的数目,由于涉及到多线程访问,所以选用了线程安全的ConcurrentHashMap
5 . 多线程问题,这里我选用了固定线程数目的线程池,最大线程数目是CPU核心数
final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
6 . 数据传递问题,由于Android不能在子线程更新UI,所以这里需要传入Handler,将最终统计结果传递到UI线程并显示
三、实战编码
首先放上代码
/**
* Created by 尚振鸿 on 17-12-16. 14:26
* mail:szh@codekong.cn
* 扫描文件并统计工具类
*/
public class ScanFileCountUtil
//扫描目录路径
private String mFilePath;
//各个分类所对应的文件后缀
private Map<String, Set<String>> mCategorySuffix;
//最终的统计结果
private ConcurrentHashMap<String, Integer> mCountResult;
//用于存储文件目录便于层次遍历
private ConcurrentLinkedQueue<File> mFileConcurrentLinkedQueue;
private Handler mHandler = null;
public void scanCountFile()
if (mFilePath == null)
return;
final File file = new File(mFilePath);
//非目录或者目录不存在直接返回
if (!file.exists() || file.isFile())
return;
//初始化每个类别的数目为0
for (String category : mCategorySuffix.keySet())
//将最后统计结果的key设置为类别
mCountResult.put(category, 0);
//获取到根目录下的文件和文件夹
final File[] files = file.listFiles(new FilenameFilter()
@Override
public boolean accept(File file, String s)
//过滤掉隐藏文件
return !file.getName().startsWith(".");
);
//临时存储任务,便于后面全部投递到线程池
List<Runnable> runnableList = new ArrayList<>();
//创建信号量(最多同时有10个线程可以访问)
final Semaphore semaphore = new Semaphore(100);
for (File f : files)
if (f.isDirectory())
//把目录添加进队列
mFileConcurrentLinkedQueue.offer(f);
//创建的线程的数目是根目录下文件夹的数目
Runnable runnable = new Runnable()
@Override
public void run()
countFile();
;
runnableList.add(runnable);
else
//找到该文件所属的类别
for (Map.Entry<String, Set<String>> entry : mCategorySuffix.entrySet())
//获取文件后缀
String suffix = f.getName().substring(f.getName().indexOf(".") + 1).toLowerCase();
//找到了
if (entry.getValue().contains(suffix))
mCountResult.put(entry.getKey(), mCountResult.get(entry.getKey()) + 1);
break;
//固定数目线程池(最大线程数目为cpu核心数,多余线程放在等待队列中)
final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
for (Runnable runnable : runnableList)
executorService.submit(runnable);
//不允许再添加线程
executorService.shutdown();
//等待线程池中的所有线程运行完成
while (true)
if (executorService.isTerminated())
break;
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
//传递统计数据给UI界面
Message msg = Message.obtain();
msg.obj = mCountResult;
mHandler.sendMessage(msg);
/**
* 统计各类型文件数目
*/
private void countFile()
//对目录进行层次遍历
while (!mFileConcurrentLinkedQueue.isEmpty())
//队头出队列
final File tmpFile = mFileConcurrentLinkedQueue.poll();
final File[] fileArray = tmpFile.listFiles(new FilenameFilter()
@Override
public boolean accept(File file, String s)
//过滤掉隐藏文件
return !file.getName().startsWith(".");
);
for (File f : fileArray)
if (f.isDirectory())
//把目录添加进队列
mFileConcurrentLinkedQueue.offer(f);
else
//找到该文件所属的类别
for (Map.Entry<String, Set<String>> entry : mCategorySuffix.entrySet())
//获取文件后缀
String suffix = f.getName().substring(f.getName().indexOf(".") + 1).toLowerCase();
//找到了
if (entry.getValue().contains(suffix))
mCountResult.put(entry.getKey(), mCountResult.get(entry.getKey()) + 1);
//跳出循环,不再查找
break;
public static class Builder
private Handler mHandler;
private String mFilePath;
//各个分类所对应的文件后缀
private Map<String, Set<String>> mCategorySuffix;
public Builder(Handler handler)
this.mHandler = handler;
public Builder setFilePath(String filePath)
this.mFilePath = filePath;
return this;
public Builder setCategorySuffix(Map<String, Set<String>> categorySuffix)
this.mCategorySuffix = categorySuffix;
return this;
private void applyConfig(ScanFileCountUtil scanFileCountUtil)
scanFileCountUtil.mFilePath = mFilePath;
scanFileCountUtil.mCategorySuffix = mCategorySuffix;
scanFileCountUtil.mHandler = mHandler;
scanFileCountUtil.mCountResult = new ConcurrentHashMap<String, Integer>(mCategorySuffix.size());
scanFileCountUtil.mFileConcurrentLinkedQueue = new ConcurrentLinkedQueue<>();
public ScanFileCountUtil create()
ScanFileCountUtil scanFileCountUtil = new ScanFileCountUtil();
applyConfig(scanFileCountUtil);
return scanFileCountUtil;
上面代码中关键的点都有注释或者是前面已经讲到了,下面说几点补充:
1 . 必须要等所有线程运行结束才能向UI线程发送消息,这里使用了轮询的方式
while (true)
if (executorService.isTerminated())
break;
try
TimeUnit.SECONDS.sleep(1);
catch (InterruptedException e)
e.printStackTrace();
2 . 由于上面的轮询会阻塞调用线程,所以调用应该放在子线程中
3 . 上面工具类实例的创建使用到了建造者模式,不懂的可以看我的另一篇博客
http://blog.csdn.net/bingjianit/article/details/53607856
4 . 上面我创建的线程的数目是根目录下文件夹的数目,大家可以根据自己的需要调整
四、方便调用
下面简单说一下如何调用上面的代码
private Handler mHandler = new Handler(Looper.getMainLooper())
@Override
public void handleMessage(Message msg)
//接收结果
Map<String, Integer> countRes = (Map<String, Integer>) msg.obj;
//后续显示处理
;
/**
* 扫描文件
*/
private void scanFile()
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
return;
final String path = Environment.getExternalStorageDirectory().getAbsolutePath();
final Map<String, Set<String>> CATEGORY_SUFFIX = new HashMap<>(FILE_CATEGORY_ICON.length);
Set<String> set = new HashSet<>();
set.add("mp4");
set.add("avi");
set.add("wmv");
set.add("flv");
CATEGORY_SUFFIX.put("video", set);
set.add("txt");
set.add("pdf");
set.add("doc");
set.add("docx");
set.add("xls");
set.add("xlsx");
CATEGORY_SUFFIX.put("document", set);
set = new HashSet<>();
set.add("jpg");
set.add("jpeg");
set.add("png");
set.add("bmp");
set.add("gif");
CATEGORY_SUFFIX.put("picture", set);
set = new HashSet<>();
set.add("mp3");
set.add("ogg");
CATEGORY_SUFFIX.put("music", set);
set = new HashSet<>();
set.add("apk");
CATEGORY_SUFFIX.put("apk", set);
set = new HashSet<>();
set.add("zip");
set.add("rar");
set.add("7z");
CATEGORY_SUFFIX.put("zip", set);
//单一线程线程池
ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
singleExecutorService.submit(new Runnable()
@Override
public void run()
//构建对象
ScanFileCountUtil scanFileCountUtil = new ScanFileCountUtil
.Builder(mHandler)
.setFilePath(path)
.setCategorySuffix(CATEGORY_SUFFIX)
.create();
scanFileCountUtil.scanCountFile();
);
五、后记
刚开始我是采用单线程扫描,扫描时间差不多是3分钟,经过使用多线程以后,扫描时间缩短到30-40秒。对了,上面的程序要想在Android中顺利运行还需要添加访问SD卡的权限和注意Android6.0的动态权限申请。
如果觉得不错,可以关注我,也可以去GitHub看看我的文件管理器,正在不断完善中,地址:
https://github.com/codekongs/FileExplorer/
以上是关于Android扫描文件并统计各类文件数目的主要内容,如果未能解决你的问题,请参考以下文章