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/

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值