Android掃描檔案並統計各類檔案數目

冰鑑IT發表於2018-01-04

最近在模仿小米的檔案管理器寫一個自己的檔案管理器,其中有一個功能是全盤掃描檔案並顯示每類檔案的數目。剛開始使用單一執行緒,掃描速度簡直慘不忍睹,換成多執行緒掃描以後,速度有較明顯的提升,便封裝了一個工具類,分享出來。

一、遇到的問題

首先描述一下遇到的問題:

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/

相關文章