春节过后,渐渐进入了工作的状态。在这些天中,主动提出要优化公司产品的下载模块多线程,目前产品是多任务单线程下载模式,这样一旦网络不行的情况下,下载会变的异常或者缓慢。加入文件的多线程下载是每个下载模块的标配了,之前也没有接触这块,所以也算是给自己一个挑战吧。
对下载模块的Api使用
- 一个模块简单易用是最基本的要求
- 配置简单
分析梳理现有的模块和思路
大致的任务启动分析路径
![](./%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 %}
多线程下载原理
- 多线程下载是将文件分成多段,然后从分割段开始下载,多线程需要服务器支持断点续传特性。
- 文件存储和IO,关于文件存储写入的问题也有很多方案,目前采用的方案是预创建整个文件大小,然后利用文件的seek找到对应指针的位置。
- 对于每个任务我们会有一张表记录每个任务中的线程下载记录,然后同步数据从而实现多线程的文件下载,每个任务ID对应其任务的线程。
- 多线程不可避免会出现线程同步问题,基本做法是当前线程需要知道兄弟线程下载的状态,我们定义了一组线程间状态读取接口,同步线程数组锁,询问兄弟线程isCompleteForOthers
- 为了更好的承接上层业务我们封装了业务层,使接入成本降低
实现多线程核心类图构成:
文件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) { } }).start(); }
|