简介
OkHttp是一个神器。OkHttp分为异步、同步两种调用。今天我们就会基于OkHttp的异步调用实现一个多线程并行下载文件并以进度条展示总进度的实用例子。当然这不是我们的Android里使用OkHttp的最终目标,我们最终在下一篇中会在今天这一课的基础上加入“断点续传”的功能,从而以这么连续的几篇从易到难的循序渐进的过程,让大家熟悉和掌握Android中使用OkHttp的技巧以便于形成大脑的“肌肉记忆”。
课程目标
- 熟悉OkHttp的同步、异步调用;
- 实现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(); //创建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() {
//失败的请求
@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 + " 己经连接,进入下载...");
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() {
@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(); //创建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 + " 己经连接,进入下载...");
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() {
//失败的请求
@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技术来处理进度条的界面变化;
结束今天的课程。