Android入门第56天-在Android里使用OKHttp多线程下载文件并展示其进度
创始人
2024-05-04 13:43:34
0

简介

OkHttp是一个神器。OkHttp分为异步、同步两种调用。今天我们就会基于OkHttp的异步调用实现一个多线程并行下载文件并以进度条展示总进度的实用例子。当然这不是我们的Android里使用OkHttp的最终目标,我们最终在下一篇中会在今天这一课的基础上加入“断点续传”的功能,从而以这么连续的几篇从易到难的循序渐进的过程,让大家熟悉和掌握Android中使用OkHttp的技巧以便于形成大脑的“肌肉记忆”。

课程目标

 

  1. 熟悉OkHttp的同步、异步调用;
  2. 实现n个线程并行下载文件;
  3. 使用线程中的回调机制实时传输下载时的进度;
  4. 用进度条辅助显示我们的整体下载进度;

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(); //创建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的同步调用例子,访问后根据response.code来作出响应并取response.body()的内容做出相应的业务处理。

OkHttp的异步调用例子

            OkHttpClient client = new OkHttpClient();Request request = new Request.Builder().url(downloadFilePath)//请求接口,如果需要传参拼接到接口后面.build(); //创建Request对象Log.d(TAG, ">>>>>>线程" + (threadId + 1) + "开始下载...");Call call = client.newCall(request);//异步请求call.enqueue(new Callback() {//失败的请求@Overridepublic void onFailure(@NonNull Call call, @NonNull IOException e) {Log.e(TAG, ">>>>>>下载进程加载->" + downloadFilePath + " error:" + e.getMessage(), e);}//结束的回调@Overridepublic void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {Log.d(TAG, ">>>>>>连接->" + downloadFilePath + " 己经连接,进入下载...");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来执行调用,然后至少有2个回调方法:onFailure和onResponse需要自己覆盖来实现业务功能。

各位记得同步、异步的调用还是有很大区别的。比如说有以下调用顺序:

  • OkHttp调用
  • A方法根据OkHttp调用后的结果再执行B

此时你就必须使用同步调用,而不能使用异步。因为如果你用的是异步很有可能B在执行到一半时,第一步OkHttp调用的结果才刚刚到达。

多线程并行下载文件需要解决的几个核心问题

如何从一个远程Http资源得到文件的尺寸

OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)//设置连接超时时间.readTimeout(10, TimeUnit.SECONDS).build();//设置读取超时时间
Request request = new Request.Builder().url(downloadUrl).build(); //创建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的位置就可以做到对文件进行并行写操作了。为此我们需要执行以下这么几步:

创建一个长度=远程资源长度的空的文件

先创建一个空的RandomAccessFile,并把远程资源的长度以如下的API set进去;

file.setLength(fileLength)

为每个线程分配写入的start-end区间段

假设我们有n个线程,每个线程写文件的长度可以用以下公式得到:

int threadlength = (int) fileLength % threadCount == 0 ? (int) fileLength / threadCount : (int) fileLength + 1;

当得到了threadLength即每个线程固定写入的长度后我们就可以得到每个线程起始的写文件位置即: 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界面以作进度展示

答案就是:回调函数。

我们先设一个接口如下

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

而在外层调用时如下实现这个onDownload

multiDownloadHelper.download(new DownloadListener() {@Overridepublic void onDownload(int size, int totalSize) {Log.d(TAG, ">>>>>>download size->" + size);float progress = ((float) size / (float) fileSize) * 100;int pgValue = (int) progress;}
});

就可以在多线程的最外层得到当前写文件的“进度”了。

下面就给出全代码。

全代码

前端


后端

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 calcStartPosition(long fileLength, int threadNo) {int threadlength = (int) fileLength % threadCount == 0 ? (int) fileLength / threadCount : (int) fileLength + 1;int startPosition = threadNo * threadlength;EnumMap downloadThreadInfor = new EnumMap(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(); //创建Request对象Log.d(TAG, ">>>>>>线程" + (threadId + 1) + "开始下载...");Call call = client.newCall(request);//异步请求call.enqueue(new Callback() {//失败的请求@Overridepublic void onFailure(@NonNull Call call, @NonNull IOException e) {Log.e(TAG, ">>>>>>下载进程加载->" + downloadFilePath + " error:" + e.getMessage(), e);}//结束的回调@Overridepublic void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {Log.d(TAG, ">>>>>>连接->" + downloadFilePath + " 己经连接,进入下载...");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(); //创建Request对象try {Call call = client.newCall(request);//异步请求call.enqueue(new Callback() {//失败的请求@Overridepublic void onFailure(@NonNull Call call, @NonNull IOException e) {Log.e(TAG, ">>>>>>加载->" + downloadFilePath + " error:" + e.getMessage(), e);}//结束的回调@Overridepublic 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 downLoadThreadInforObjectEnumMap = new EnumMap(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() {@Overridepublic 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;}});@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic 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);}});}});}
}

 此处需要注意的点是

  1. 需要使用异步线程去驱动我们的多线程;
  2. 使用handler技术来处理进度条的界面变化;

结束今天的课程。

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...