OkHttp是一个神器。OkHttp分为异步、同步两种调用。今天我们就会基于OkHttp的异步调用实现一个多线程并行下载文件并以进度条展示总进度的实用例子。当然这不是我们的Android里使用OkHttp的最终目标,我们最终在下一篇中会在今天这一课的基础上加入“断点续传”的功能,从而以这么连续的几篇从易到难的循序渐进的过程,让大家熟悉和掌握Android中使用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()的内容做出相应的业务处理。
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需要自己覆盖来实现业务功能。
各位记得同步、异步的调用还是有很大区别的。比如说有以下调用顺序:
此时你就必须使用同步调用,而不能使用异步。因为如果你用的是异步很有可能B在执行到一半时,第一步OkHttp调用的结果才刚刚到达。
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)
假设我们有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;}
答案就是:回调函数。
我们先设一个接口如下
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;}
});
就可以在多线程的最外层得到当前写文件的“进度”了。
下面就给出全代码。
package org.mk.android.demo.http;public interface DownloadListener {public void onDownload(int size,int totalSize);
}
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);}}
}
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);}});}});}
}
此处需要注意的点是:
结束今天的课程。