最近在模仿小米的檔案管理器寫一個自己的檔案管理器,其中有一個功能是全盤掃描檔案並顯示每類檔案的數目。剛開始使用單一執行緒,掃描速度簡直慘不忍睹,換成多執行緒掃描以後,速度有較明顯的提升,便封裝了一個工具類,分享出來。
一、遇到的問題
首先描述一下遇到的問題:
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/