原始碼解析,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)
前言
造輪子者:Season_zlc
本文主要講述 RxDownload2
的多執行緒斷點下載技術
斷點下載技術前提
伺服器必須支援按 byte-range
下載,也就是支援 Range: bytes=xxx-xxx
請求頭。詳見 Http 協議 rfc2616 – Range。
下載範圍分割
很簡單,先讀取 Content-Length
響應頭,獲取檔案大小,然後用檔案大小除以執行緒數就可計算出每條執行緒的下載範圍。
比如,假設檔案大小是 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]
上程式碼:
- prepareDownload() [->
FileHelper.java]
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);
}複製程式碼
- prepareFile() [->
FileHelper.java]
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
中按位置讀出來就行了。
- readDownloadRange() [->
FileHelper.java]
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
。而以讀寫方式開啟檔案時,會自動擴充套件檔案長度,所以不會丟擲異常。
因為每段下載範圍的長度都是 EACH_RECORD_SIZE = 16 bytes
,所以,上述程式碼應修改為:MappedByteBuffer buffer = channel.map(READ_WRITE, i * EACH_RECORD_SIZE, EACH_RECORD_SIZE);
Intellij IDEA 示例程式碼
自己寫了個示例程式碼,測試了一下:
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
可從任意位置讀寫的屬性,分別將每條執行緒下載的資料寫到同一個檔案的不同位置。
- saveFile() [->
FileHelper.java]
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 系列文章: