Android入門之使用OKHttp多線程下載文件
簡介
OkHttp是一個神器。OkHttp分為異步、同步兩種調用。今天我們就會基于OkHttp的異步調用實現(xiàn)一個多線程并行下載文件并以進度條展示總進度的實用例子。當然這不是我們的Android里使用OkHttp的最終目標,我們最終在下一篇中會在今天這一課的基礎上加入“斷點續(xù)傳”的功能,從而以這么連續(xù)的幾篇從易到難的循序漸進的過程,讓大家熟悉和掌握Android中使用OkHttp的技巧以便于形成大腦的“肌肉記憶”。
課程目標

- 熟悉OkHttp的同步、異步調用;
- 實現(xiàn)n個線程并行下載文件;
- 使用線程中的回調機制實時傳輸下載時的進度;
- 用進度條輔助顯示我們的整體下載進度;
OkHttp的同步調用例子
public int getDownloadFileSize(String downloadUrl) throws Exception {
int size = -1;
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)//設置連接超時時間
.readTimeout(10, TimeUnit.SECONDS).build();//設置讀取超時時間
Request request = new Request.Builder().url(downloadUrl)//請求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對象
Response response = null;
try {
Call call = client.newCall(request);
response = call.execute();
if (200 == response.code()) {
Log.d(TAG, ">>>>>>response.code()==" + response.code());
Log.d(TAG, ">>>>>>response.message()==" + response.message());
try {
size = (int) response.body().contentLength();
Log.d(TAG, ">>>>>>file length->" + size);
//fileSizeListener.onHttpResponse((int) size);
} catch (Exception e) {
Log.e(TAG, ">>>>>>get remote file size error: " + e.getMessage(), e);
}
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>open connection to path->" + downloadUrl + "\nerror: " + e.getMessage(), e);
throw new Exception(">>>>>>getDownloadFileSize from->" + downloadUrl + "\nerror: " + e.getMessage(), e);
} finally {
try {
response.close();
} catch (Exception e) {
}
}
return size;
}這是一個OkHttp的同步調用例子,訪問后根據(jù)response.code來作出響應并取response.body()的內容做出相應的業(yè)務處理。
OkHttp的異步調用例子
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadFilePath)//請求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對象
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "開始下載...");
Call call = client.newCall(request);
//異步請求
call.enqueue(new Callback() {
//失敗的請求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>下載進程加載->" + downloadFilePath + " error:" + e.getMessage(), e);
}
//結束的回調
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
Log.d(TAG, ">>>>>>連接->" + downloadFilePath + " 己經(jīng)連接,進入下載...");
InputStream is = null;
try {
if (200 == response.code()) {
Log.d(TAG, ">>>>>>response.code()==" + response.code());
Log.d(TAG, ">>>>>>response.message()==" + response.message());
is = response.body().byteStream();
byte[] buffer = new byte[1024];
int len = -1;
int length = 0;
while (length < threadLength && (len = is.read(buffer)) != -1) {
threadFile.write(buffer, 0, len);
//計算累計下載的長度
length += len;
downloadListener.onDownload(length,totalSize);
}
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "已下載完成");
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>線程:" + threadId + " 下載出錯: " + e.getMessage(), e);
} finally {
try {
threadFile.close();
} catch (Exception e) {
}
try {
is.close();
;
} catch (Exception e) {
}
}
}
});這是一個OkHttp的異步調用例子,我們可以看到它首先以call.enqueue來執(zhí)行調用,然后至少有2個回調方法:onFailure和onResponse需要自己覆蓋來實現(xiàn)業(yè)務功能。
各位記得同步、異步的調用還是有很大區(qū)別的。比如說有以下調用順序:
- OkHttp調用
- A方法根據(jù)OkHttp調用后的結果再執(zhí)行B
此時你就必須使用同步調用,而不能使用異步。因為如果你用的是異步很有可能B在執(zhí)行到一半時,第一步OkHttp調用的結果才剛剛到達。
多線程并行下載文件需要解決的幾個核心問題
如何從一個遠程Http資源得到文件的尺寸
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)//設置連接超時時間
.readTimeout(10, TimeUnit.SECONDS).build();//設置讀取超時時間
Request request = new Request.Builder().url(downloadUrl)
.build(); //創(chuàng)建Request對象
Response response = null;
Call call = client.newCall(request);
response = call.execute();
if (200 == response.code()) {
size = (int) response.body().contentLength();
}
我們通過response.body().contentLength()即可以獲得遠程資源文件的尺寸了。
如何對一個本地文件進行并行的寫
在這兒,我們使用RandomAccessFile來進行并行寫操作。因為RandomAccessFile里有一個seek屬性。
seek即寫文件起始、結束位置。因此我們設每個不同的線程處理自己的start-end的位置就可以做到對文件進行并行寫操作了。為此我們需要執(zhí)行以下這么幾步:
創(chuàng)建一個長度=遠程資源長度的空的文件
先創(chuàng)建一個空的RandomAccessFile,并把遠程資源的長度以如下的API set進去;
file.setLength(fileLength)
為每個線程分配寫入的start-end區(qū)間段
假設我們有n個線程,每個線程寫文件的長度可以用以下公式得到:
int threadlength = (int) fileLength % threadCount == 0
? (int) fileLength / threadCount : (int) fileLength + 1;
當?shù)玫搅藅hreadLength即每個線程固定寫入的長度后我們就可以得到每個線程起始的寫文件位置即: startPosition。
int startPosition = threadNo * threadlength;
每個線程在寫入操作時需要先進行:seek(起始位)。
threadFile.seek(startPosition);
然后在寫時每個線程不得超過自己被分配的固定長度,到了寫入的固定長度后就結束寫操作。
is = response.body().byteStream();
byte[] buffer = new byte[1024];
int len = -1;
int length = 0;
while (length < threadLength && (len = is.read(buffer)) != -1) {
threadFile.write(buffer, 0, len);
//計算累計下載的長度
length += len;
}
如何在子線程里把當前正寫入的文件的size傳給Android界面以作進度展示
答案就是:回調函數(shù)。
我們先設一個接口如下
package org.mk.android.demo.http;
public interface DownloadListener {
public void onDownload(int size,int totalSize);
}
然后我們在線程實例化時需要轉入這個接口
public DownLoadThread(int threadId, int startPosition,
RandomAccessFile threadFile, int threadLength, String downloadFilePath,DownloadListener downloadListener,
在寫文件時我們作如下操作
while (length < threadLength && (len = is.read(buffer)) != -1) {
threadFile.write(buffer, 0, len);
//計算累計下載的長度
length += len;
downloadListener.onDownload(length,totalSize);
}
而在外層調用時如下實現(xiàn)這個onDownload
multiDownloadHelper.download(new DownloadListener() {
@Override
public void onDownload(int size, int totalSize) {
Log.d(TAG, ">>>>>>download size->" + size);
float progress = ((float) size / (float) fileSize) * 100;
int pgValue = (int) progress;
}
});
就可以在多線程的最外層得到當前寫文件的“進度”了。
下面就給出全代碼。
全代碼
前端
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/buttonDownload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="download"
android:textSize="20sp" />
<ProgressBar
android:id="@+id/progressBarDownload"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:max="100" />
</LinearLayout>后端
DownloadListener
package org.mk.android.demo.http;
public interface DownloadListener {
public void onDownload(int size,int totalSize);
}
MultiDownloadHelper
package org.mk.android.demo.http;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import org.apache.commons.io.FilenameUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.util.EnumMap;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MultiDownloadHelper {
private static final String TAG = "DemoMultiDownloadWithProgressBar";
private int threadCount = 0;
private String downloadFilePath = "";
public MultiDownloadHelper(int threadCount, String filePath) {
this.threadCount = threadCount;
this.downloadFilePath = filePath;
}
private enum DownLoadThreadInfor {
threadLength, startPosition
}
private EnumMap<DownLoadThreadInfor, Object> calcStartPosition(long fileLength, int threadNo) {
int threadlength = (int) fileLength % threadCount == 0 ? (int) fileLength / threadCount : (int) fileLength + 1;
int startPosition = threadNo * threadlength;
EnumMap<DownLoadThreadInfor, Object> downloadThreadInfor = new EnumMap<DownLoadThreadInfor, Object>(DownLoadThreadInfor.class);
downloadThreadInfor.put(DownLoadThreadInfor.threadLength, threadlength);
downloadThreadInfor.put(DownLoadThreadInfor.startPosition, startPosition);
return downloadThreadInfor;
}
private String generateTempFile(String filePath, long fileLength) throws Exception {
String end = filePath.substring(filePath.lastIndexOf("."));
URL url = new URL(filePath);
//String downloadFilePath = "Cache_" + System.currentTimeMillis() + end;
String urlFileName=FilenameUtils.getName(url.getPath());
RandomAccessFile file = null;
try {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String fileName = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + urlFileName;
Log.d(TAG,">>>>>>寫入->"+fileName);
file = new RandomAccessFile(fileName, "rwd");
file.setLength(fileLength);
return fileName;
} else {
throw new Exception("SD卡不可讀寫");
}
} catch (Exception e) {
throw new Exception("GenerateTempFile error: " + e.getMessage(), e);
} finally {
try {
file.close();
} catch (Exception e) {
}
}
}
private class DownLoadThread extends Thread {
private int threadId;
private int startPosition;
private RandomAccessFile threadFile;
private int threadLength;
private String downloadFilePath;
private DownloadListener downloadListener;
private int totalSize=0;
public DownLoadThread(int threadId, int startPosition,
RandomAccessFile threadFile, int threadLength, String downloadFilePath,DownloadListener downloadListener,
int totalSize) {
this.threadId = threadId;
this.startPosition = startPosition;
this.threadFile = threadFile;
this.threadLength = threadLength;
this.downloadFilePath = downloadFilePath;
this.downloadListener=downloadListener;
this.totalSize=totalSize;
}
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadFilePath)//請求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對象
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "開始下載...");
Call call = client.newCall(request);
//異步請求
call.enqueue(new Callback() {
//失敗的請求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>下載進程加載->" + downloadFilePath + " error:" + e.getMessage(), e);
}
//結束的回調
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
Log.d(TAG, ">>>>>>連接->" + downloadFilePath + " 己經(jīng)連接,進入下載...");
InputStream is = null;
try {
if (200 == response.code()) {
Log.d(TAG, ">>>>>>response.code()==" + response.code());
Log.d(TAG, ">>>>>>response.message()==" + response.message());
is = response.body().byteStream();
byte[] buffer = new byte[1024];
int len = -1;
int length = 0;
while (length < threadLength && (len = is.read(buffer)) != -1) {
threadFile.write(buffer, 0, len);
//計算累計下載的長度
length += len;
downloadListener.onDownload(length,totalSize);
}
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "已下載完成");
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>線程:" + threadId + " 下載出錯: " + e.getMessage(), e);
} finally {
try {
threadFile.close();
} catch (Exception e) {
}
try {
is.close();
;
} catch (Exception e) {
}
}
}
});
}
}
public void download(DownloadListener downloadListener) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadFilePath)//請求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對象
try {
Call call = client.newCall(request);
//異步請求
call.enqueue(new Callback() {
//失敗的請求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>加載->" + downloadFilePath + " error:" + e.getMessage(), e);
}
//結束的回調
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
//響應碼可能是404也可能是200都會走這個方法
Log.i(TAG, ">>>>>>the response code is: " + response.code());
if (200 == response.code()) {
Log.d(TAG, ">>>>>>response.code()==" + response.code());
Log.d(TAG, ">>>>>>response.message()==" + response.message());
try {
long size = response.body().contentLength();
Log.d(TAG, ">>>>>>file length->" + size);
for (int i = 0; i < threadCount; i++) {
EnumMap<DownLoadThreadInfor, Object> downLoadThreadInforObjectEnumMap = new EnumMap<DownLoadThreadInfor, Object>(DownLoadThreadInfor.class);
downLoadThreadInforObjectEnumMap = calcStartPosition(size, i);
String threadFileName = generateTempFile(downloadFilePath, size);
int startPosition = (int) downLoadThreadInforObjectEnumMap.get(DownLoadThreadInfor.startPosition);
int threadLength = (int) downLoadThreadInforObjectEnumMap.get(DownLoadThreadInfor.threadLength);
RandomAccessFile threadFile = new RandomAccessFile(threadFileName, "rwd");
threadFile.seek(startPosition);
new DownLoadThread(i, startPosition, threadFile, threadLength, downloadFilePath,downloadListener,(int)size).start();
Log.d(TAG, ">>>>>>start thread: " + i + 1 + " start position->" + startPosition);
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>get remote file size error: " + e.getMessage(), e);
}
}
}
});
} catch (Exception e) {
Log.e(TAG, ">>>>>>open connection to path->" + downloadFilePath + "\nerror: " + e.getMessage(), e);
}
}
}MainActivity
package org.mk.android.demo.http;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "DemoMultiDownloadWithProgressBar";
private static final String picUrl = "https://tqjimg.tianqistatic.com/toutiao/images/202106/08/3721f7ae444ddfc4.jpg";
private Button buttonDownload;
private ProgressBar progressBarDownload;
private Context ctx=null;
private Handler downloadHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
Log.i(TAG, ">>>>>>receive handler Message msg.what is: " + msg.what);
switch (msg.what) {
case 101:
//Toast.makeText(ctx, "下載圖片完成", Toast.LENGTH_LONG).show();
progressBarDownload.setVisibility(View.VISIBLE);
//progressBarDownload.setProgress();
int inputNum = msg.getData().getInt("pgValue");
progressBarDownload.setProgress(inputNum);
if (inputNum >= 100) {
Toast.makeText(ctx, "下載圖片完成", Toast.LENGTH_LONG).show();
}
break;
}
return false;
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ctx=getApplicationContext();
buttonDownload = (Button) findViewById(R.id.buttonDownload);
progressBarDownload = (ProgressBar) findViewById(R.id.progressBarDownload);
progressBarDownload.setVisibility(View.GONE);
progressBarDownload.setMax(100);
buttonDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Log.i(TAG, ">>>>>>version.SDK->" + Build.VERSION.SDK_INT);
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
return;
}
}
MultiDownloadHelper multiDownloadHelper = new MultiDownloadHelper(3, picUrl);
multiDownloadHelper.download(new DownloadListener() {
@Override
public void onDownload(int size, int totalSize) {
Log.d(TAG, ">>>>>>download size->" + size);
float progress = ((float) size / (float) totalSize) * 100;
int pgValue = (int) progress;
Message msg = new Message();
msg.what = 101;
Bundle bundle = new Bundle();
bundle.putInt("pgValue", pgValue);
msg.setData(bundle);
downloadHandler.sendMessage(msg);
Log.d(TAG, ">>>>>>current pgValue->" + progress);
}
});
}
});
}
}
此處需要注意的點是:
- 需要使用異步線程去驅動我們的多線程;
- 使用handler技術來處理進度條的界面變化;
到此這篇關于Android入門之使用OKHttp多線程下載文件的文章就介紹到這了,更多相關Android OKHttp下載文件內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android滑動刪除數(shù)據(jù)功能的實現(xiàn)代碼
這篇文章主要介紹了Android滑動刪除功能2017-01-01
Android WebView userAgent 設置為桌面UA實例
這篇文章主要介紹了Android WebView userAgent 設置為桌面UA實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android 實現(xiàn)密碼輸入框動態(tài)明文/密文切換顯示效果
在項目中遇到需要提供給用戶一個密碼輸入框明文/密文切換顯示的需求,今天小編借腳本之家平臺給大家分享下Android 實現(xiàn)密碼輸入框動態(tài)明文/密文切換顯示效果,需要的朋友參考下2017-01-01
Android studio將Module打包成Jar的方法
這篇文章主要介紹了Android studio將Module打包成Jar的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10
Android ExpandableListView長按事件的完美解決辦法
本篇文章是對Android中ExpandableListView長按事件的解決方法進行了詳細的分析介紹,需要的朋友參考下2013-06-06
Android利用listview控件操作SQLite數(shù)據(jù)庫實例
我們利用SQLiteOpenHelper類建立一個數(shù)據(jù)庫,并寫好增、刪、查等方法,通過SimpleCursorAdapter連接listview實現(xiàn)數(shù)據(jù)庫的增加、查詢以及長按刪除的功能。2017-04-04

