源碼解析,如需轉載,請註明做者:Yuloran (t.cn/EGU6c76)html
造輪子者:Season_zlcjava
本文主要講述 RxDownload2
的多線程斷點下載技術服務器
服務器必須支持按 byte-range
下載,也就是支持 Range: bytes=xxx-xxx
請求頭。詳見 Http 協議 rfc2616 - Range。多線程
很簡單,先讀取 Content-Length
響應頭,獲取文件大小,而後用文件大小除以線程數就可計算出每條線程的下載範圍。app
好比,假設文件大小是 100 bytes
,下載線程數爲 3
。由於 100 / 3 = 33
,因此:框架
線程 0
的下載範圍是 0 ~32
即 [0 * 33 ~ (0 + 1) * 33 - 1]
線程 1
的下載範圍是 33~65
即 [1 * 33 ~ (1 + 1) * 33 - 1]
線程 2
的下載範圍是 66~99
即 [2 * 33 ~ 100 - 1]
上代碼:dom
public void prepareDownload(File lastModifyFile, File tempFile, File saveFile, long fileLength, String lastModify) throws IOException, ParseException {
// 將響應頭中的上次修改時間轉爲 long 類型的 unix 時間戳,而後保存到文件中
writeLastModify(lastModifyFile, lastModify);
// 設置下載文件的大小、計算每條線程的下載範圍並保存到 tempFile 中
prepareFile(tempFile, saveFile, fileLength);
}
複製代碼
private void prepareFile(File tempFile, File saveFile, long fileLength) throws IOException {
RandomAccessFile rFile = null;
RandomAccessFile rRecord = null;
FileChannel channel = null;
try {
rFile = new RandomAccessFile(saveFile, ACCESS);
rFile.setLength(fileLength);//設置下載文件的長度
rRecord = new RandomAccessFile(tempFile, ACCESS);
// 下載範圍在文件中的記錄方式:|start|end|start|end|start|end|...
// 數據類型是 long,long類型在 java 中佔 8 個字節,因此每一個線程的下載範圍都佔 16 字節
// 因此 tempFile 的長度 RECORD_FILE_TOTAL_SIZE = 16 * 線程數
rRecord.setLength(RECORD_FILE_TOTAL_SIZE); //設置指針記錄文件的大小
// NIO 內存映射文件的方式讀寫二進制文件,速度更快
channel = rRecord.getChannel();
// 注意映射方式爲讀寫
MappedByteBuffer buffer = channel.map(READ_WRITE, 0, RECORD_FILE_TOTAL_SIZE);
long start;
long end;
// 計算並保存每條線程的下載範圍,計算方法同上面舉的例子
int eachSize = (int) (fileLength / maxThreads);
for (int i = 0; i < maxThreads; i++) {
if (i == maxThreads - 1) {
start = i * eachSize;
end = fileLength - 1;
} else {
start = i * eachSize;
end = (i + 1) * eachSize - 1;
}
buffer.putLong(start);
buffer.putLong(end);
}
} finally {
closeQuietly(channel);
closeQuietly(rRecord);
closeQuietly(rFile);
}
}
複製代碼
很簡單,上面已經將每條線程的下載範圍保存到了 tempFile
中,只要再從 tempFile
中按位置讀出來就好了。工具
public DownloadRange readDownloadRange(File tempFile, int i) throws IOException {
RandomAccessFile record = null;
FileChannel channel = null;
try {
// 入參 i 表示線程序號
record = new RandomAccessFile(tempFile, ACCESS);
channel = record.getChannel();
MappedByteBuffer buffer = channel
.map(READ_WRITE, i * EACH_RECORD_SIZE, (i + 1) * EACH_RECORD_SIZE);
long startByte = buffer.getLong();
long endByte = buffer.getLong();
return new DownloadRange(startByte, endByte);
} finally {
closeQuietly(channel);
closeQuietly(record);
}
}
複製代碼
注意 MappedByteBuffer buffer = channel.map(READ_WRITE, i * EACH_RECORD_SIZE, (i + 1) * EACH_RECORD_SIZE);
這句代碼是有坑的,可是表現不出來,由於這裏的文件打開方式爲 READ_WRITE
。要是改爲 READ_ONLY
就有致使讀取最後一條線程的下載範圍時拋出IllegalArgumentException
(代碼靜態檢查工具 Fortify
提示要以合適的權限打開文件,我將其改成了 READ_ONLY
,發現了這一問題)。源碼分析
錯誤緣由:map() 方法的最後一個參數表示要映射的字節數,以只讀方式打開時,若參數大小超過了文件剩餘可讀字節數,就會拋出 IllegalArgumentException
。而以讀寫方式打開文件時,會自動擴展文件長度,因此不會拋出異常。post
由於每段下載範圍的長度都是 EACH_RECORD_SIZE = 16 bytes
,因此,上述代碼應修改成: MappedByteBuffer buffer = channel.map(READ_WRITE, i * EACH_RECORD_SIZE, EACH_RECORD_SIZE);
本身寫了個示例代碼,測試了一下:
RandomAccessFile file = new RandomAccessFile("temp.txt", "rw");
file.setLength(48);
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 48);
for (int i = 0; i < 3; i++) {
if (i == 2) {
buffer.putLong(i * 33).putLong(99);
} else {
buffer.putLong(i * 33).putLong((i + 1) * 33 - 1);
}
}
channel.close();
RandomAccessFile file1 = new RandomAccessFile("temp.txt", "r");
FileChannel channel1 = file1.getChannel();
for (int i = 0; i < 3; i++) {
MappedByteBuffer buffer1 = channel1.map(FileChannel.MapMode.READ_ONLY, i * 16, 16);
System.out.println(String.format("long1: %d", buffer1.getLong()));
System.out.println(String.format("long2: %d", buffer1.getLong()));
}
channel1.close();
複製代碼
給 Notepad++
裝個十六進制查看器,查看生成的 temp.txt
中的內容是否和咱們代碼寫的同樣:
上面是十六進制,換算成十進制就是上面示例代碼寫的內容。
很簡單,利用 RandomAccessFile
可從任意位置讀寫的屬性,分別將每條線程下載的數據寫到同一個文件的不一樣位置。
public void saveFile(FlowableEmitter<DownloadStatus> emitter, int i, File tempFile, File saveFile, ResponseBody response) {
RandomAccessFile record = null;
FileChannel recordChannel = null;
RandomAccessFile save = null;
FileChannel saveChannel = null;
InputStream inStream = null;
try {
try {
// 1.映射 tempFile 到內存中
record = new RandomAccessFile(tempFile, ACCESS);
recordChannel = record.getChannel();
MappedByteBuffer recordBuffer = recordChannel
.map(READ_WRITE, 0, RECORD_FILE_TOTAL_SIZE);
// i 表明線程序號,startIndex 表明該線程下載範圍的 start 字段在文件中的指針位置
int startIndex = i * EACH_RECORD_SIZE;
// start 表示該線程的起始下載位置
long start = recordBuffer.getLong(startIndex);
// 新建一個下載狀態對象,用於發射下載進度
DownloadStatus status = new DownloadStatus();
// totalSize 表明文件總大小,也能夠從 saveFile 中讀出
long totalSize = recordBuffer.getLong(RECORD_FILE_TOTAL_SIZE - 8) + 1;
status.setTotalSize(totalSize);
int readLen;
byte[] buffer = new byte[2048];
inStream = response.byteStream();
save = new RandomAccessFile(saveFile, ACCESS);
saveChannel = save.getChannel();
while ((readLen = inStream.read(buffer)) != -1 && !emitter.isCancelled()) {
MappedByteBuffer saveBuffer = saveChannel.map(READ_WRITE, start, readLen);
saveBuffer.put(buffer, 0, readLen);
// 成功下載一段數據後,將已下載位置寫回 start 字段
start += readLen;
recordBuffer.putLong(startIndex, start);
// 計算已下載字節數 = 文件長度 - 每條線程剩餘未下載字節數
status.setDownloadSize(totalSize - getResidue(recordBuffer));
// 發射下載進度
emitter.onNext(status);
}
// 發射下載完成
emitter.onComplete();
} finally {
closeQuietly(record);
closeQuietly(recordChannel);
closeQuietly(save);
closeQuietly(saveChannel);
closeQuietly(inStream);
closeQuietly(response);
}
} catch (IOException e) {
emitter.onError(e);
}
}
複製代碼
下載流程就不分析了,只要熟練使用下圖所示兩個快捷鍵,什麼源碼分析都是手到擒來:
RxDownload2
源碼解析系列至此結束,雖然框架比較簡單,可是仍是有不少值得學習的東西。尤爲是做者對 RxJava2
的使用,能夠說很是之六了。他寫的十篇 Rxjava2
教程也很是的通俗易懂,感興趣的能夠看一看。
RxDownload2 系列文章: