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

- 熟悉OkHttp的同步、異步調(diào)用;
- 實(shí)現(xiàn)n個(gè)線程并行下載文件;
- 使用線程中的回調(diào)機(jī)制實(shí)時(shí)傳輸下載時(shí)的進(jìn)度;
- 用進(jìn)度條輔助顯示我們的整體下載進(jìn)度;
OkHttp的同步調(diào)用例子
public int getDownloadFileSize(String downloadUrl) throws Exception {
int size = -1;
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)//設(shè)置連接超時(shí)時(shí)間
.readTimeout(10, TimeUnit.SECONDS).build();//設(shè)置讀取超時(shí)時(shí)間
Request request = new Request.Builder().url(downloadUrl)//請(qǐng)求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對(duì)象
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;
}這是一個(gè)OkHttp的同步調(diào)用例子,訪問(wèn)后根據(jù)response.code來(lái)作出響應(yīng)并取response.body()的內(nèi)容做出相應(yīng)的業(yè)務(wù)處理。
OkHttp的異步調(diào)用例子
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadFilePath)//請(qǐng)求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對(duì)象
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "開(kāi)始下載...");
Call call = client.newCall(request);
//異步請(qǐng)求
call.enqueue(new Callback() {
//失敗的請(qǐng)求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>下載進(jìn)程加載->" + downloadFilePath + " error:" + e.getMessage(), e);
}
//結(jié)束的回調(diào)
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
Log.d(TAG, ">>>>>>連接->" + downloadFilePath + " 己經(jīng)連接,進(jìn)入下載...");
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);
//計(jì)算累計(jì)下載的長(zhǎng)度
length += len;
downloadListener.onDownload(length,totalSize);
}
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "已下載完成");
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>線程:" + threadId + " 下載出錯(cuò): " + e.getMessage(), e);
} finally {
try {
threadFile.close();
} catch (Exception e) {
}
try {
is.close();
;
} catch (Exception e) {
}
}
}
});這是一個(gè)OkHttp的異步調(diào)用例子,我們可以看到它首先以call.enqueue來(lái)執(zhí)行調(diào)用,然后至少有2個(gè)回調(diào)方法:onFailure和onResponse需要自己覆蓋來(lái)實(shí)現(xiàn)業(yè)務(wù)功能。
各位記得同步、異步的調(diào)用還是有很大區(qū)別的。比如說(shuō)有以下調(diào)用順序:
- OkHttp調(diào)用
- A方法根據(jù)OkHttp調(diào)用后的結(jié)果再執(zhí)行B
此時(shí)你就必須使用同步調(diào)用,而不能使用異步。因?yàn)槿绻阌玫氖钱惒胶苡锌赡蹷在執(zhí)行到一半時(shí),第一步OkHttp調(diào)用的結(jié)果才剛剛到達(dá)。
多線程并行下載文件需要解決的幾個(gè)核心問(wèn)題
如何從一個(gè)遠(yuǎn)程Http資源得到文件的尺寸
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)//設(shè)置連接超時(shí)時(shí)間
.readTimeout(10, TimeUnit.SECONDS).build();//設(shè)置讀取超時(shí)時(shí)間
Request request = new Request.Builder().url(downloadUrl)
.build(); //創(chuàng)建Request對(duì)象
Response response = null;
Call call = client.newCall(request);
response = call.execute();
if (200 == response.code()) {
size = (int) response.body().contentLength();
}
我們通過(guò)response.body().contentLength()即可以獲得遠(yuǎn)程資源文件的尺寸了。
如何對(duì)一個(gè)本地文件進(jìn)行并行的寫(xiě)
在這兒,我們使用RandomAccessFile來(lái)進(jìn)行并行寫(xiě)操作。因?yàn)镽andomAccessFile里有一個(gè)seek屬性。
seek即寫(xiě)文件起始、結(jié)束位置。因此我們?cè)O(shè)每個(gè)不同的線程處理自己的start-end的位置就可以做到對(duì)文件進(jìn)行并行寫(xiě)操作了。為此我們需要執(zhí)行以下這么幾步:
創(chuàng)建一個(gè)長(zhǎng)度=遠(yuǎn)程資源長(zhǎng)度的空的文件
先創(chuàng)建一個(gè)空的RandomAccessFile,并把遠(yuǎn)程資源的長(zhǎng)度以如下的API set進(jìn)去;
file.setLength(fileLength)
為每個(gè)線程分配寫(xiě)入的start-end區(qū)間段
假設(shè)我們有n個(gè)線程,每個(gè)線程寫(xiě)文件的長(zhǎng)度可以用以下公式得到:
int threadlength = (int) fileLength % threadCount == 0
? (int) fileLength / threadCount : (int) fileLength + 1;
當(dāng)?shù)玫搅藅hreadLength即每個(gè)線程固定寫(xiě)入的長(zhǎng)度后我們就可以得到每個(gè)線程起始的寫(xiě)文件位置即: startPosition。
int startPosition = threadNo * threadlength;
每個(gè)線程在寫(xiě)入操作時(shí)需要先進(jìn)行:seek(起始位)。
threadFile.seek(startPosition);
然后在寫(xiě)時(shí)每個(gè)線程不得超過(guò)自己被分配的固定長(zhǎng)度,到了寫(xiě)入的固定長(zhǎng)度后就結(jié)束寫(xiě)操作。
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);
//計(jì)算累計(jì)下載的長(zhǎng)度
length += len;
}
如何在子線程里把當(dāng)前正寫(xiě)入的文件的size傳給Android界面以作進(jìn)度展示
答案就是:回調(diào)函數(shù)。
我們先設(shè)一個(gè)接口如下
package org.mk.android.demo.http;
public interface DownloadListener {
public void onDownload(int size,int totalSize);
}
然后我們?cè)诰€程實(shí)例化時(shí)需要轉(zhuǎn)入這個(gè)接口
public DownLoadThread(int threadId, int startPosition,
RandomAccessFile threadFile, int threadLength, String downloadFilePath,DownloadListener downloadListener,
在寫(xiě)文件時(shí)我們作如下操作
while (length < threadLength && (len = is.read(buffer)) != -1) {
threadFile.write(buffer, 0, len);
//計(jì)算累計(jì)下載的長(zhǎng)度
length += len;
downloadListener.onDownload(length,totalSize);
}
而在外層調(diào)用時(shí)如下實(shí)現(xiàn)這個(gè)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;
}
});
就可以在多線程的最外層得到當(dāng)前寫(xiě)文件的“進(jìn)度”了。
下面就給出全代碼。
全代碼
前端
<?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,">>>>>>寫(xiě)入->"+fileName);
file = new RandomAccessFile(fileName, "rwd");
file.setLength(fileLength);
return fileName;
} else {
throw new Exception("SD卡不可讀寫(xiě)");
}
} 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)//請(qǐng)求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對(duì)象
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "開(kāi)始下載...");
Call call = client.newCall(request);
//異步請(qǐng)求
call.enqueue(new Callback() {
//失敗的請(qǐng)求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>下載進(jìn)程加載->" + downloadFilePath + " error:" + e.getMessage(), e);
}
//結(jié)束的回調(diào)
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
Log.d(TAG, ">>>>>>連接->" + downloadFilePath + " 己經(jīng)連接,進(jìn)入下載...");
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);
//計(jì)算累計(jì)下載的長(zhǎng)度
length += len;
downloadListener.onDownload(length,totalSize);
}
Log.d(TAG, ">>>>>>線程" + (threadId + 1) + "已下載完成");
}
} catch (Exception e) {
Log.e(TAG, ">>>>>>線程:" + threadId + " 下載出錯(cuò): " + 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)//請(qǐng)求接口,如果需要傳參拼接到接口后面
.build(); //創(chuàng)建Request對(duì)象
try {
Call call = client.newCall(request);
//異步請(qǐng)求
call.enqueue(new Callback() {
//失敗的請(qǐng)求
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(TAG, ">>>>>>加載->" + downloadFilePath + " error:" + e.getMessage(), e);
}
//結(jié)束的回調(diào)
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
//響應(yīng)碼可能是404也可能是200都會(huì)走這個(gè)方法
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);
}
});
}
});
}
}
此處需要注意的點(diǎn)是:
- 需要使用異步線程去驅(qū)動(dòng)我們的多線程;
- 使用handler技術(shù)來(lái)處理進(jìn)度條的界面變化;
到此這篇關(guān)于Android入門(mén)之使用OKHttp多線程下載文件的文章就介紹到這了,更多相關(guān)Android OKHttp下載文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android滑動(dòng)刪除數(shù)據(jù)功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android滑動(dòng)刪除功能2017-01-01
Android WebView userAgent 設(shè)置為桌面UA實(shí)例
這篇文章主要介紹了Android WebView userAgent 設(shè)置為桌面UA實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03
Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果
在項(xiàng)目中遇到需要提供給用戶一個(gè)密碼輸入框明文/密文切換顯示的需求,今天小編借腳本之家平臺(tái)給大家分享下Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果,需要的朋友參考下2017-01-01
Android防止按鈕過(guò)快點(diǎn)擊造成多次事件的解決方法
這篇文章主要介紹了Android防止按鈕過(guò)快點(diǎn)擊造成多次事件的解決方法的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
Android利用Intent啟動(dòng)和關(guān)閉Activity
這篇文章主要為大家詳細(xì)介紹了Android利用Intent啟動(dòng)和關(guān)閉Activity的相關(guān)操作,感興趣的小伙伴們可以參考一下2016-06-06
Android studio將Module打包成Jar的方法
這篇文章主要介紹了Android studio將Module打包成Jar的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10
Android ExpandableListView長(zhǎng)按事件的完美解決辦法
本篇文章是對(duì)Android中ExpandableListView長(zhǎng)按事件的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
Android利用listview控件操作SQLite數(shù)據(jù)庫(kù)實(shí)例
我們利用SQLiteOpenHelper類建立一個(gè)數(shù)據(jù)庫(kù),并寫(xiě)好增、刪、查等方法,通過(guò)SimpleCursorAdapter連接listview實(shí)現(xiàn)數(shù)據(jù)庫(kù)的增加、查詢以及長(zhǎng)按刪除的功能。2017-04-04

