下载模块多线程实现之路

春节过后,渐渐进入了工作的状态。在这些天中,主动提出要优化公司产品的下载模块多线程,目前产品是多任务单线程下载模式,这样一旦网络不行的情况下,下载会变的异常或者缓慢。加入文件的多线程下载是每个下载模块的标配了,之前也没有接触这块,所以也算是给自己一个挑战吧。

对下载模块的Api使用

  1. 一个模块简单易用是最基本的要求
  2. 配置简单

分析梳理现有的模块和思路

大致的任务启动分析路径

![](./%E4%B8%8B%E8%BD%BD%E6%A8%A1%E5%9D%97%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%AE%9E%E7%8E%B0%E4%B9%8B%E8%B7%AF/img/FlowchartDiagram下载模块流程图.png %}

多线程下载原理

  1. 多线程下载是将文件分成多段,然后从分割段开始下载,多线程需要服务器支持断点续传特性。
  2. 文件存储和IO,关于文件存储写入的问题也有很多方案,目前采用的方案是预创建整个文件大小,然后利用文件的seek找到对应指针的位置。
  3. 对于每个任务我们会有一张表记录每个任务中的线程下载记录,然后同步数据从而实现多线程的文件下载,每个任务ID对应其任务的线程。
  4. 多线程不可避免会出现线程同步问题,基本做法是当前线程需要知道兄弟线程下载的状态,我们定义了一组线程间状态读取接口,同步线程数组锁,询问兄弟线程isCompleteForOthers
  5. 为了更好的承接上层业务我们封装了业务层,使接入成本降低

实现多线程核心类图构成:

文件IO

对于文件IO这块,我们使用了Okio做为文件IO的底层工具,使用RandomAccessFile来实现文件的seek,从而实现文件的断点操作。为了最大化的加快文件的写入操作,我们加入了文件的预创建过程。

IO性能测试

这里的测试数据是将内存中的10MB的数据写入到磁盘的平均时间

FileOutputStream BufferedOutputStream RandomAccessFile BufferedSink
2983 2461 2369 2369
2493 5073 2430 2393
2780 2460 2421 2389
2492 2966 2427 2361
2870 2501 2570 2365
2490 3011 2430 2372
2963 2559 2400 2361
2493 2446 2438 2346
2615 4811 2400 2367
2518 3010 2398 2344

平均值:

2669.7 3129.8 2428.3 2366.7

这里的测试环境是有一个文件输入流,一个文件的输出流,然后每次读取相同的BUFFER_SIZE = 1024 * 8 来测试IO性能

FileOutputStream BufferedOutputStream RandomAccessFile BufferedSink
2144 2162 2265 2313
2447 3991 2205 2313
2211 2241 2196 2309
2474 2276 2191 2293
2230 2446 2184 2338
3305 2305 2201 2329
4772 2191 2827 2271
2418 2304 2795 2294
2319 2204 2166 2279
2316 2261 2797 2244

平均值:

2663.6 2438.1 2382.7 2298.3

可以从上面的数据看出,使用Okio文件写入非常稳定。

预创建文件

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

public static long KBSIZE = 1024;
public static long MBSIZE1 = 1024 * 1024;
public static long MBSIZE10 = 1024L * 1024 * 10;

public static boolean createFile(File file, long fileLength, final ValueCallback valueCallback) {
FileOutputStream fos = null;
try {

if (!file.exists()) {
boolean ret = file.createNewFile();
if (!ret) return false;
}

long batchSize = 0;
batchSize = fileLength;
if (fileLength > KBSIZE) {
batchSize = KBSIZE;
}
if (fileLength > MBSIZE1) {
batchSize = MBSIZE1;
}
if (fileLength > MBSIZE10) {
batchSize = MBSIZE10;
}
long count = fileLength / batchSize;
long last = fileLength % batchSize;

fos = new FileOutputStream(file);
FileChannel fileChannel = fos.getChannel();
for (long i = 0; i < count; i++) {
ByteBuffer buffer = ByteBuffer.allocate((int) batchSize);
fileChannel.write(buffer);

if (i % 3 == 0 && valueCallback != null) {
float x = i;
float y = count;
int progress = (int) ((x / y) * 100);
valueCallback.onReceiveValue(progress);
}
}

ByteBuffer buffer = ByteBuffer.allocate((int) last);
fileChannel.write(buffer);

if (valueCallback != null) {
valueCallback.onReceiveValue(100);
}

return true;

} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}

测试用例图

开始一个任务,上层任务保留了文件的基本特性,使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void startTask(String url, String contentDisposition, String mimetype) {
String mT = MimeUtils.guessMimeTypeFromExtension(MimeUtils.getFileExtensionFromFileName(MimeUtils.guessFileName(url, contentDisposition, mimetype)));
ProDownloadRequest request = new ProDownloadRequest.Builder()
.url(url)
.title(MimeUtils.guessFileName(url, contentDisposition, mimetype))
.refUrl(url)
.mimeType(StringUtils.isEmpty(mT) ? mimetype : mT)
.build();

ProDownloadManager.getInstance().createTask(request).addOnStateChangeListener(new OnStateChangeListener() {
@Override
public void onStateChange(ProDownloadTask task, int status, long sofar, long total) {
//your code
}
}).start();
}