Android入门第56天-在Android里使用OKHttp多线程下载文件并展示其进度

Posted TGITCIC

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android入门第56天-在Android里使用OKHttp多线程下载文件并展示其进度相关的知识,希望对你有一定的参考价值。

简介

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() 
                //失败的请求
                @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);
                    
                );
            
        );
    

 此处需要注意的点是

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

结束今天的课程。

以上是关于Android入门第56天-在Android里使用OKHttp多线程下载文件并展示其进度的主要内容,如果未能解决你的问题,请参考以下文章

Android入门第55天-在Android里使用OKHttp组件访问网络资源

Android入门第44天-Android里使用动态BroadCast

Android入门第25天-Android里使用SimpleAdapter实现复杂的界面布局

Android入门第53天-在Android手机里使用SQLite内嵌式数据库

Android入门第3天-在Android Studio里配置虚拟器

Android入门第7天-TableLayout