Android扫描文件并统计各类文件数目

最近在模仿小米的文件管理器写一个自己的文件管理器,其中有一个功能是全盘扫描文件并显示每类文件的数目。刚开始使用单一线程,扫描速度简直惨不忍睹,换成多线程扫描以后,速度有较明显的提升,便封装了一个工具类,分享出来。

一、遇到的问题

首先描述一下遇到的问题:

1 . Android端全盘扫描文件

2 . 开一个子线程扫描太慢,使用多线程扫描

3 . 统计每一类文件的数目(比如:视频文件,图片文件,音频文件的数目)

二、解决思路

接下来描述一下几个点的解决思路:

1 . 首先目录的存储结构是树状结构,这里就设计到了树的遍历,这里我使用树的层次遍历,使用非递归方法实现,具体的遍历思路后面会有代码,这里只说明是借助于队列完成树的层次遍历。

2 . 第二个思路便是我们需要传入的参数,这里其实涉及到的是数据的存储结构问题,这里我使用的数据结构如下:

1
Map<String, Set<String>>

解释一下这个数据结构,map的key表示种类,value是个Set这个Set里面包含该种类的文件的后缀名。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
ConcurrentLinkedQueue<File> mFileConcurrentLinkedQueue;

4 . 还有需要将统计结果进行存储,这里我也选用了线程安全的HashMap

1
private ConcurrentHashMap<String, Integer> mCountResult;

这个Map的key表示文件种类,value表示该类文件的数目,由于涉及到多线程访问,所以选用了线程安全的ConcurrentHashMap

5 . 多线程问题,这里我选用了固定线程数目的线程池,最大线程数目是CPU核心数

1
final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

6 . 数据传递问题,由于Android不能在子线程更新UI,所以这里需要传入Handler,将最终统计结果传递到UI线程并显示

三、实战编码

首先放上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
* 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线程发送消息,这里使用了轮询的方式

1
2
3
4
5
6
7
8
9
10
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 . 上面我创建的线程的数目是根目录下文件夹的数目,大家可以根据自己的需要调整

四、方便调用

下面简单说一下如何调用上面的代码

1
2
3
4
5
6
7
8
private Handler mHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
//接收结果
Map<String, Integer> countRes = (Map<String, Integer>) msg.obj;
//后续显示处理
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 扫描文件
*/
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/

如果博客对您有帮助,不妨请我喝杯咖啡...