RxDownload2 原始碼解析(三)

Yuloran發表於2019-01-04

原始碼解析,如需轉載,請註明作者: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]

上程式碼:

  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);

}複製程式碼
  1. 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 中按位置讀出來就行了。

  1. 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 中的內容是否和我們程式碼寫的一樣:

temp.txt view in HEX

上面是十六進位制,換算成十進位制就是上面示例程式碼寫的內容。

寫下載檔案

很簡單,利用 RandomAccessFile 可從任意位置讀寫的屬性,分別將每條執行緒下載的資料寫到同一個檔案的不同位置。

  1. 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 原始碼解析(三)
  • RxDownload2 原始碼解析系列至此結束,雖然框架比較簡單,但是還是有很多值得學習的東西。尤其是作者對 RxJava2 的使用,可以說非常之六了。他寫的十篇 Rxjava2 教程也非常的通俗易懂,感興趣的可以看一看。

RxDownload2 系列文章:

來源:https://juejin.im/post/5c2ee33651882525ec1ffd42

相關文章