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-01Android WebView userAgent 設(shè)置為桌面UA實(shí)例
這篇文章主要介紹了Android WebView userAgent 設(shè)置為桌面UA實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果
在項(xiàng)目中遇到需要提供給用戶一個(gè)密碼輸入框明文/密文切換顯示的需求,今天小編借腳本之家平臺(tái)給大家分享下Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果,需要的朋友參考下2017-01-01Android防止按鈕過(guò)快點(diǎn)擊造成多次事件的解決方法
這篇文章主要介紹了Android防止按鈕過(guò)快點(diǎn)擊造成多次事件的解決方法的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07Android利用Intent啟動(dòng)和關(guān)閉Activity
這篇文章主要為大家詳細(xì)介紹了Android利用Intent啟動(dòng)和關(guān)閉Activity的相關(guān)操作,感興趣的小伙伴們可以參考一下2016-06-06Android studio將Module打包成Jar的方法
這篇文章主要介紹了Android studio將Module打包成Jar的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10Android ExpandableListView長(zhǎng)按事件的完美解決辦法
本篇文章是對(duì)Android中ExpandableListView長(zhǎng)按事件的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06Android利用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