android入门第57天-使用okhttp多线程制作像迅雷一样的断点续传功能(代码片段)

TGITCIC TGITCIC     2023-01-09     618

关键词:

简介

今天我们将继续使用OkHttp组件并制作一个基于多线程的可断点续传的下载器来结束Android OkHttp组件的所有知识内容。在这一课里我们会在上一次课程的基础上增加SQLite的使用以便于我们的App可以暂存下载时的实时进度,每次下载开始都会判断是覆盖式还是续传式下载。同时由于Android自带的进度条太丑了,我们对它稍稍进行了一些美化。可以说今天这篇教程也是一篇阶段性的功能整合实验。

下面开始进入课程。

课程目标

  1. 使用SQLite进行下载时的进度信息的暂存;

  1. 自定义ProgressBar的样式;

断点下载的原理

如果你认真的在看完了上篇教程后并且脱离我的Sample代码自己动手实现了一个多线程下载器的话那么今天这篇教程对于你来说会变得相当的简单。

因为所谓的断点下载就是把每一条线程当前在下载的信息存入一个SQLite的表内。而断点下载就是通过暂存的信息去改变RandomAcessFile在写入时的seek。

当然这里面还伴随着一些小技巧,我们需要我们的APP的“STOP”动作可以打断正在下载的进度,打断后如果再次点击了“DOWNLOAD”按钮,此时各子线程做的任务为“续传”,续传的进度是否完成了呢这也需要子线程和主线程间进行状态通信。

需要知道每个子线程运行是否已经结束了

这边并不是需要知道每个子线程的返回、中间态。我们只是需要知道每一个子线程是否运行完了。

在平时开发中我们经常会面临这样的一种情况。比如说我们外部需要长时间的等待?或者也有开发搞了一个全局的栈去计算、也有用future接口的。很多时候往往为了取一个状态,开发创造了一堆的“轮子”,导致了整个项目代码过于复杂以及不好调试。因此这些手法都不是很优雅。今天笔者给各位推荐一种更为优雅的写法,以便于在外部判断每一个子线程是否都运行完毕了。

使用状态反转来不断check子线程状态

其实它的核心思路是:

  1. 在外部有一个无限while 循环,while(notFinish);

  1. 循环入口上手就把循环终止, notFinish=false;

  1. 接着依次检查每一个子线程内的一个状态值-finish,这个值在每个子线程内任务结束后会设为true。只要这个值在外部被检测到不为true,那么把外部循环的状态再改为notFinish=true,以使得外部循环不断运行直到所有子线程检测下来都确为finish,此时外部的while循环跳出;

每个子线程下载的实时信息存储

我们设计了一个这样的表结构用来存储下载的实时信息。

  • 每次下载进程开始时,先根据下载URL去该表中查出所有的下载信息。比如说我们开启了3个线程,那么对于同一个URL:/test.zip可以根据download_path查出3条数据。把3条数据的download_length相加拼在一起,如果<当前远程文件size说明上次下载没有完成,那么继续下载。否则新建一个空文件并把这个空文件的长度设定为远程资源文件的长度;

  • 每个子线程在下载时不断根据download_path update这张表里的数据把当前的实时进度写进去;

  • 下载完后根据download_path清空这个表里的数据;

Http Get请求如何支持断点续传

Request.addHeader("Range", "bytes=" + startPos + "-" + endPos)

假设线程编号从1开始,开了3个子线程,共有1-3个线程,线程编号为1-3,此处的startPos和endPos的计算公式如下:

  • startPos=每个线程分页下载文件大小*线程编号+上一次下载进度,如果线程为1号线程那么startPos=上一次的下载进度;

  • endPos=每个线程分页下载文件大小*当前线程编号-1,-1代表“不计算文件末尾结束符”;

int startPos = block * (threadId - 1) + downLength;//开始位置
int endPos = block * threadId - 1;//结束位置

自定义Android里的ProgressBar的样式

第一步:

res\\values\\colors.xml文件中加入一个ProgressBar的底色theme_progressbar

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="theme_progressbar">#D0E3F7</color>
</resources>

这是一个很浅很淡的蓝色。

第二步:

res\\drawable\\下,新建一个progressbar_color.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- 背景  gradient是渐变,corners定义的是圆角 -->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="3dp"/>

            <solid android:color="@color/theme_progressbar" />
        </shape>
    </item>
    <!-- 进度条 -->
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="3dp"/>
                <solid android:color="#FF51AAE6" />
            </shape>
        </clip>
    </item>

</layer-list>

第三步:

在activity_main.xml文件里定义progressbar时引用这个progressbar_color.xml文件。

 <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"
        android:progressDrawable="@drawable/progressbar_color"
        android:visibility="visible" />

以上内容都准备好了,我们就可以进入全代码了。

全代码

项目结构

前端

<?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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
    <Button
        android:id="@+id/buttonDownload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="download"
        android:layout_marginRight="10dp"
        android:textSize="20sp" />

    <Button
        android:id="@+id/buttonStop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="stop"
        android:textSize="20sp" />
    </LinearLayout>
    <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"
        android:progressDrawable="@drawable/progressbar_color"
        android:visibility="visible" />
</LinearLayout>

后端

DbOpeerateHelper.java

package org.mk.android.demo.http;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DbOperateHelper extends SQLiteOpenHelper 
    private static final String TAG = "DemoContinueDownload";
    private static final String DB_NAME = "dw_manager.db";
    private static final String DB_TABLE = "dw_infor";
    private static final int DB_VERSION = 1;
    public DbOperateHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) 
        super(context, name, factory, version);
    

    private static final String DB_CREATE =

            "CREATE TABLE dw_infor ("
                    +"dw_id    INTEGER PRIMARY KEY AUTOINCREMENT,"
                    +"download_path    VARCHAR,"
                    +"thread_id    INTEGER,"
                    +"download_length    INTEGER);";

    @Override
    public void onCreate(SQLiteDatabase db) 
        Log.i(TAG, ">>>>>>execute create table->" + DB_CREATE);
        db.execSQL(DB_CREATE);
        Log.i(TAG, ">>>>>>db init successfully");
    

    @Override
    public void onUpgrade(SQLiteDatabase db, int _oldVersion, int _newVersion) 
        //db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
        //onCreate(_db);
        db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
        onCreate(db);

    

DBService.java

package org.mk.android.demo.http;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class DBService 
    private static final String TAG = "DemoContinueDownload";
    private DbOperateHelper dbHelper;
    private static final String DB_NAME = "dw_manager.db";
    private static final String DB_TABLE = "dw_infor";
    private static final int DB_VERSION = 1;

    public DBService(Context ctx)
        dbHelper=new DbOperateHelper(ctx,DB_NAME,null,DB_VERSION);
    
    /**
     * 获得指定URI的每条线程已经下载的文件长度
     * @param downloadPath
     * @return
     * */
    public List<DWManagerInfor> getData(String downloadPath)
    
        //获得可读数据库句柄,通常内部实现返回的其实都是可写的数据库句柄
        //根据下载的路径查询所有现场的下载数据,返回的Cursor指向第一条记录之前
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = db.rawQuery("select thread_id, download_length from dw_infor where download_path=?",
                new String[]downloadPath);
        List<DWManagerInfor> data=new ArrayList<DWManagerInfor>();
        try 
            //从第一条记录开始遍历Cursor对象
            //cursor.moveToFirst();
            while (cursor.moveToNext()) 
                DWManagerInfor dwInfor =new DWManagerInfor();
                dwInfor.setThreadId(cursor.getInt(cursor.getColumnIndexOrThrow("thread_id")));
                dwInfor.setDownloadLength(cursor.getInt(cursor.getColumnIndexOrThrow("download_length")));
                data.add(dwInfor);
            
        catch(Exception e)
            Log.e(TAG,">>>>>>getData from db error: "+e.getMessage(),e);
        finally
            try 
                cursor.close();//关闭cursor,释放资源;
            catch(Exception e)
            try 
                db.close();
            catch(Exception e)
        
        return data;
    

    /**
     * 保存每条线程已经下载的文件长度
     */

    public void save(String downloadPath, Map<Integer,Integer> map)
    
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        db.beginTransaction();
        try

            //使用增强for循环遍历数据集合
            for(Map.Entry<Integer, Integer> entry : map.entrySet())
            

                db.execSQL("insert into dw_infor(download_path, thread_id, download_length) values(?,?,?)",
                        new Object[]downloadPath, entry.getKey(),entry.getValue());
            
            //设置一个事务成功的标志,如果成功就提交事务,如果没调用该方法的话那么事务回滚
            //就是上面的数据库操作撤销
            db.setTransactionSuccessful();
        catch(Exception e)
            Log.e(TAG,">>>>>>save download infor into db error: "+e.getMessage(),e);
        finally
            //结束一个事务
            db.endTransaction();
            try
                db.close();
            catch(Exception e)
        
    

    public int updateItem(DWManagerInfor dwInfor)throws Exception
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        try
            ContentValues newValues = new ContentValues();
            newValues.put("download_length",dwInfor.getDownloadLength());
            newValues.put("thread_id",dwInfor.getThreadId());
            newValues.put("download_path",dwInfor.getDownloadPath());
            return db.update(DB_TABLE,newValues,"thread_id='"+dwInfor.getThreadId()+"' and download_path='"+dwInfor.getDownloadPath()+"'",null);
        catch(Exception e)
            Log.e(TAG,"update item error: "+e.getMessage(),e);
            throw new Exception("update item error: "+e.getMessage(),e);
        finally
            try
                db.close();
            catch(Exception e)
        
    
    public void delete(String path)
    
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        try 
            String deleteSql = "delete from dw_infor where download_path=?";
            db.execSQL(deleteSql, new Object[]path);
        catch(Exception e)
            Log.e(TAG,">>>>>>delete from path->"+path+" error: "+e.getMessage(),e);
        finally
            try
                db.close();
            catch(Exception e)
        
    
    public long addItem(DWManagerInfor dwInfor)throws Exception
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        try
            ContentValues newValues = new ContentValues();
            newValues.put("download_path", dwInfor.getDownloadPath());
            newValues.put("thread_id", dwInfor.getThreadId());
            newValues.put("download_length", dwInfor.getDownloadLength());
            Log.i(TAG, "addItem successfully");
            return db.insert(DB_TABLE, null, newValues);
        catch(Exception e)
            Log.e(TAG,">>>>>>addItem into db error: "+e.getMessage(),e);
            throw new Exception(">>>>>>addItem into db error: "+e.getMessage(),e);
        finally
            try
                db.close();
            catch(Exception e)
        
    

DownloadProgressListener.java

package org.mk.android.demo.http;

public interface DownloadProgressListener 
    public void onDownloadSize(int size);

DWManagerInfor.java

package org.mk.android.demo.http;

import java.io.Serializable;

public class DWManagerInfor implements Serializable 
    private int dwId=0;
    private int threadId=0;


    public int getDwId() 
        return dwId;
    

    public void setDwId(int dwId) 
        this.dwId = dwId;
    

    public int getThreadId() 
        return threadId;
    

    public void setThreadId(int threadId) 
        this.threadId = threadId;
    

    public int getDownloadLength() 
        return downloadLength;
    

    public void setDownloadLength(int downloadLength) 
        this.downloadLength = downloadLength;
    

    public String getDownloadPath() 
        return downloadPath;
    

    public void setDownloadPath(String downloadPath) 
        this.downloadPath = downloadPath;
    

    private int downloadLength=0;
    private String downloadPath="";


DownloadService.java

这是一个主要的用于启动多线程下载和操作断点信息的类,在这个类内会分出3个子线程,每个子线程内又把这个类的this传入在子线程内进行回调、写下载时的实时信息入库、传递子线程状态,因此它是一个核心类。

package org.mk.android.demo.http;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import androidx.annotation.NonNull;

import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.sql.Array;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class DownloadService 
    private static final String TAG = "DemoContinueDownload";
    private File saveFile;
    private int downloadedSize = 0;               //已下载的文件长度
    private Context context = null;
    private int threadCount = 3;
    private int fileSize = 0;
    private int block = 0;
    private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();  //缓存个条线程的下载的长度
    //private DBAdapter dbAdapter = null;
    private DBService dbService=null;
    private DownloadThread[] threads;        //根据线程数设置下载的线程池
    private boolean exited = false;
    private String downloadUrl = "";

    public DownloadService(Context context, String downloadUrl) 
        this.context = context;
        //dbAdapter = new DBAdapter(context);
        dbService=new DBService(context);
        this.threads = new DownloadThread[threadCount];
        this.downloadUrl = downloadUrl;
    

    public int getFileSize() 
        return this.fileSize;
    

    /**
     * 退出下载
     */
    public void exit() 
        Log.i(TAG, ">>>>>>触发了exited");
        this.exited = true;    //将退出的标志设置为true;
    

    public boolean getExited() 
        return this.exited;
    

    /**
     * 累计已下载的大小
     * 使用同步锁来解决并发的访问问题
     */
    protected synchronized void append(int size) 
        //把实时下载的长度加入到总的下载长度中
        downloadedSize += size;
    

    /**
     * 更新指定线程最后下载的位置
     *
     * @param threadId 线程id
     * @param pos      最后下载的位置
     */
    protected synchronized void update(int threadId, int pos) 
        try 
            this.data.put(threadId, pos);
            //dbAdapter.open();
            DWManagerInfor dwInfor = new DWManagerInfor();
            dwInfor.setDownloadPath(this.downloadUrl);
            dwInfor.setThreadId(threadId);
            dwInfor.setDownloadLength(pos);
            //dbAdapter.updateItem(dwInfor);
            dbService.updateItem(dwInfor);
         catch (Exception e) 
            Log.e(TAG, ">>>>>>update error: " + e.getMessage(), e);
        
        //把指定线程id的线程赋予最新的下载长度,以前的值会被覆盖掉
        this.data.put(threadId, pos);
        //更新数据库中制定线程的下载长度

    

    private String generateFile(long fileLength, boolean generateFile) throws Exception 
        String end = downloadUrl.substring(downloadUrl.lastIndexOf("."));
        URL url = new URL(downloadUrl);
        //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.i(TAG, ">>>>>>需要操作的文件名为->" + fileName);
                Log.i(TAG,">>>>>>downloadedSize->"+downloadedSize+"  fileLength->"+fileLength);
                if (generateFile) 
                    if(downloadedSize==0||downloadedSize>=fileLength) 
                        Log.i(TAG,">>>>>>新建文件并设定长度->"+fileLength);
                        file = new RandomAccessFile(fileName, "rwd");
                        file.setLength(fileLength);
                        file.close();
                    else
                        Log.i(TAG,">>>>>>文件存在,返回文件名进行续传");
                    
                
                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) 
            
        

    

    public int getRemainDownloadLen(int threadCount, long fileLength) 
        int block = 0;
        try 
            block = (int) fileLength % threadCount == 0 ? (int) fileLength / threadCount :
                    (int) fileLength / threadCount + 1;
         catch (Exception e) 
            Log.e(TAG, ">>>>>>getRemainDownloadLen error: " + e.getMessage(), e);
        
        return block;
    

    public void download(boolean generateFile, DownloadProgressListener downloadProgressListener) throws Exception 
        try 
            fileSize = getDownloadFileSize(downloadUrl);
            //把所有的DB内已经存在的size放入全局的data中,以作缓存
            List<DWManagerInfor> dwInforList = new ArrayList<DWManagerInfor>();
            dwInforList = dbService.getData(downloadUrl);
            Log.i(TAG, ">>>>>>in download method the dwInforList size->" + dwInforList.size());
            if (dwInforList.size() > 0) 
                for (DWManagerInfor dwInfor : dwInforList) 
                    downloadedSize += dwInfor.getDownloadLength();
                    this.data.put(dwInfor.getThreadId(), dwInfor.getDownloadLength());
                
             else 
                for (int i = 0; i < threadCount; i++) 
                    this.data.put(i + 1, 0);
                
            
            this.block = getRemainDownloadLen(3, fileSize);
            Log.i(TAG,">>>>>>downloadSize->"+downloadedSize);
            String saveFileName = generateFile(this.fileSize, generateFile);//生成一个Random空文件并把文件长度设置好
            Log.i(TAG, ">>>>>>开始生成线程进行分: " + this.threadCount + " 条线程并行下载...每条线程的block->" + this.block);
            Log.i(TAG, ">>>>>>全局data size->" + data.size());
            for (int i = 0; i < this.threads.length; i++) //开启线程进行下载
                int downLength = 0;
                if (data.size() > 0) 
                    downLength = this.data.get(i + 1);
                
                Log.i(TAG, ">>>>>>开启前发觉当前下载进度为->" + downLength);
                //通过特定的线程id获取该线程已经下载的数据长度
                //判断线程是否已经完成下载,否则继续下载
                if (downLength < this.block && this.downloadedSize < this.fileSize) 
                    //初始化特定id的线程
                    //this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1),
                    // i+1);
                    this.threads[i] = new DownloadThread(this, downloadUrl, saveFileName, this.block,
                            this.data.get(i + 1), i + 1);
                    //设置线程优先级,Thread.NORM_PRIORITY = 5;
                    //Thread.MIN_PRIORITY = 1;Thread.MAX_PRIORITY = 10,数值越大优先级越高
                    this.threads[i].setPriority(7);
                    this.threads[i].start();    //启动线程
                 else 
                    Log.i(TAG, "当前线程不用下载,因为当前线程己下载长度downLength->" + downLength + " block->" + this.block);
                    this.threads[i] = null;   //表明线程已完成下载任务
                
            
            //dbAdapter.open();
            dbService.delete(downloadUrl);
            dbService.save(downloadUrl, this.data);
            //把下载的实时数据写入数据库中
            boolean notFinish = true;
            //下载未完成
            while (notFinish) 
                // 循环判断所有线程是否完成下载
                Thread.sleep(300);
                notFinish = false;
                for (int i = 0; i < threadCount; i++) 
                    if (this.threads[i] != null && !this.threads[i].isFinish()) 
                        //如果发现线程未完成下载
                        notFinish = true;
                        //设置标志为下载没有完成,以便于外层while循环不断check;
                    
                
                if (downloadProgressListener != null) 
                    downloadProgressListener.onDownloadSize(this.downloadedSize);
                
                //通知目前已经下载完成的数据长度
            
            if (downloadedSize == this.fileSize) 
                  dbService.delete(downloadUrl);
            
         catch (Exception e) 
            Log.e(TAG, ">>>>>>download error: " + e.getMessage(), e);
            throw new Exception(">>>>>>download error: " + e.getMessage(), e);
        

    

    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;
    


DownloadThread.java

这个类就是每一个子线程的实现了。在这个类里每一个子线程会启动OkHttp并使用http-header: Range去做断点下载。

值得注意的是,如果你的http-header带着Rnage去做请求,你得到的response code不是200还是206即:partial content。

package org.mk.android.demo.http;

import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class DownloadThread extends Thread 
    private static final String TAG = "DemoContinueDownload";
    private String downloadUrl;              //下载的URL
    private int block;                //每条线程下载的大小
    private int threadId = 1;            //初始化线程id设置
    private int downLength;             //该线程已下载的数据长度
    private boolean finish = false;         //该线程是否完成下载的标志
    private DownloadService downloader;
    private String saveFileName = "";

    //private FileDownloadered downloader;      //文件下载器
    public DownloadThread(DownloadService downloader, String downloadUrl, String saveFileName, int block,
            int downLength, int threadId) 
        this.downloader = downloader;
        this.downloadUrl = downloadUrl;
        this.saveFileName = saveFileName;
        this.block = block;
        this.downLength = downLength;
        this.threadId = threadId;
    

    @Override
    public void run() 
        Log.i(TAG, ">>>>>>downloadLength->" + downLength + " block->" + block);
        if (downLength < block) 
            int startPos = block * (threadId - 1) + downLength;//开始位置
            int endPos = block * threadId - 1;//结束位置
            OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)//设置连接超时时间
                    .readTimeout(10, TimeUnit.SECONDS)//设置读取超时时间
                    .build();
            Request request = new Request.Builder().get().url(downloadUrl)//请求接口,如果需要传参拼接到接口后面
                    .addHeader("Referer", downloadUrl)
                    .addHeader("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, " +
                            "application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, " +
                            "application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, " +
                            "application/vnd.ms-powerpoint, application/msword, */*")
                    .addHeader("connection", "keep-alive")
                    .addHeader("Range", "bytes=" + startPos + "-" + endPos)
                    .addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET " +
                            "CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET " +
                            "CLR 3.5.30729)")
                    .build(); //创建Request对象
            //Log.i(TAG, ">>>>>>线程" + threadId + "开始下载...Range: bytes=" + startPos + "-" + endPos);
            Call call = client.newCall(request);
            //异步请求
            call.enqueue(new Callback() 
                //失败的请求
                @Override
                public void onFailure(@NonNull Call call, @NonNull IOException e) 
                    Log.e(TAG, ">>>>>>下载进程加载->" + downloadUrl + " error:" + e.getMessage(), e);
                    finish = true;
                

                //结束的回调
                @Override
                public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException 
                    Log.i(TAG, ">>>>>>连接->" + downloadUrl + " 己经连接,进入下载...");
                    InputStream is = null;
                    Log.i(TAG, ">>>>>>当前:response code->" + response.code());
                    RandomAccessFile threadFile = null;
                    try 
                        if (response.code() == 200 || response.code() == 206) 
                            Log.i(TAG, ">>>>>>response.code()==" + response.code());
                            //Log.i(TAG, ">>>>>>response.message()==" + response.message());
                            is = response.body().byteStream();
                            byte[] buffer = new byte[1024];
                            int offset = 0;
                            int length = 0;
                            threadFile = new RandomAccessFile(saveFileName, "rwd");
                            threadFile.seek(startPos);
                            while (!downloader.getExited() && (offset = is.read(buffer, 0, 1024)) != -1) 
                                //Log.i(TAG,">>>>>>offset write->"+offset);
                                threadFile.write(buffer, 0, offset);
                                downLength += offset;
                                downloader.update(threadId, downLength);
                                downloader.append(offset);
                            
                            //Log.i(TAG,"current offset->"+offset);
                            //Log.i(TAG, ">>>>>>线程" + threadId  + "已下载完成");
                            finish = true;
                            threadFile.close();
                        
                     catch (Exception e) 
                        downLength = -1;               //设置该线程已经下载的长度为-1
                        Log.e(TAG, ">>>>>>线程:" + threadId + " 下载出错: " + e.getMessage(), e);
                        finish = true;
                     finally 
                        try 
                            threadFile.close();
                            ;
                         catch (Exception e) 
                        
                        try 
                            is.close();
                            ;
                         catch (Exception e) 
                        
                    

                
            );
        
    

    /**
     * 下载是否完成
     *
     * @return
     */
    public boolean isFinish() 
        return finish;
    

    /**
     * 已经下载的内容大小
     *
     * @return 如果返回值为-1,代表下载失败
     */
    public long getDownLength() 
        return downLength;
    

MainActivity.java

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.database.sqlite.SQLiteDatabase;
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 SQLiteDatabase db;
    private Context context;
    //private DBAdapter dbAdapter;
    private DBService dbService;
    private Button buttonDownload;
    private Button buttonStop;
    private DownloadTask downloadTask;
    private ProgressBar progressBarDownload;
    private static final String TAG = "DemoContinueDownload";
    //private static final String DOWNLOAD_URL = "http://www.jszjenergy.cn/data/upload/image/20191231/1577758425809614.jpg";
    private static final String DOWNLOAD_URL = "https://7-zip.org/a/7z2201.exe";

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = getApplicationContext();
        //dbAdapter = new DBAdapter(context);
        dbService=new DBService(context);
        progressBarDownload = (ProgressBar) findViewById(R.id.progressBarDownload);
        buttonDownload = (Button) findViewById(R.id.buttonDownload);
        buttonStop = (Button) findViewById(R.id.buttonStop);
        //progressBarDownload.setVisibility(View.GONE);
        buttonDownload.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                try 
                    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;
                        
                    
                    downloadTask = new DownloadTask();
                    downloadTask.start();
                 catch (Exception e) 
                    Log.e(TAG, ">>>>>>downloadTest error: " + e.getMessage(), e);
                
            
        );
        buttonStop.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                if (downloadTask != null) 
                    downloadTask.exit();
                
            
        );
    

    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:
                    progressBarDownload.setVisibility(View.VISIBLE);
                    //progressBarDownload.setProgress();
                    int inputNum = msg.getData().getInt("pgValue");
                    progressBarDownload.setProgress(inputNum);
                    if (inputNum >= 100) 
                        Toast.makeText(context, "下载完成", Toast.LENGTH_LONG).show();
                    
                    break;
            
            return false;
        
    );

    private class DownloadTask extends Thread 
        private DownloadService loader;

        /**
         * 退出下载
         */
        public void exit() 
            if (loader != null) 
                loader.exit();
            
        

        @Override
        public void run() 
            try 
                loader = new DownloadService(context, DOWNLOAD_URL);
                //dbAdapter = new DBAdapter(context);
                //dbAdapter.open();
                //dbAdapter.delete(DOWNLOAD_URL);
                loader.download(true, new DownloadProgressListener() 
                    @Override
                    public void onDownloadSize(int size) 
                        int fileSize=loader.getFileSize();
                        Log.i(TAG, ">>>>>>下载中,当前尺寸: " + size+" totalSize->"+fileSize);
                        float progress = ((float) size / (float) fileSize) * 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);
                    
                );
             catch (Exception e) 
                Log.e(TAG, ">>>>>>downloadTest error: " + e.getMessage(), e);
             finally 
                //dbAdapter.close();
            
        
    

为了正确运行上述内容你需要在gradle的build文件内加入OkHttp和commons-io的依赖包。

implementation 'com.squareup.okhttp3:okhttp:3.10.0'

implementation group: 'commons-io', name: 'commons-io', version: '2.6'

运行后的效果

当你无论如何stop再download再stop或者下载完后多次再download,那么当文件被成功下载后,会在Android的资源列表里此处显示下载的资源。

它位于data\\media\\0下。

为了验证你下载的正确性,你可以把这个资源右键->另存出去。然后双击这个安装程序,如果它可以正确安装那么说明你的断点下载是正确了。

结束今天的课程,不妨自己动一下手试试看吧。

附、AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <!-- 在SDCard中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
    <!-- 往SDCard写入数据权限 -->
    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
    <!--外部存储的写权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--外部存储的读权限-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:networkSecurityConfig="@xml/network_config"
        android:requestLegacyExternalStorage="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.DemoContinueDownloadProcess"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

android入门第55天-在android里使用okhttp组件访问网络资源(代码片段)

...的课程里我们会使用OKHttp组件来访问网络资源而不是使用Android自带的URLConnection。一个是OKHttp组件更方便二个是OKHttp组件本身就带有异步回调功能。下面就进入课程。课程目标我们的课程目标有4个点:使用OKHttp组件;使... 查看详情

android入门第59天-进入mvvm

什么是MVVM用“某大文豪亲”的话说:MVVM并不存在,只是xml里找控件找了太多了,自然而然就“找”出了一套共性。所以,MVVM只是包括了以下这些技术:DataBind;ViewModel双向绑定;Okhttp3+retrofit+rxja... 查看详情

android入门第37天-在子线程中调用handler(代码片段)

...on="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical" 查看详情

android入门第47天-fragment的基本使用(代码片段)

简介我们的Android入门一步步已经进入中级。我们讲完了所有的基本组件的基本使用、Activity、Service、BroadCast。今天我们来到了Fragment篇章。Fragment和Activity比到底是一个什么样的存在呢?我们以一个很小的例子来说通Fragment。F... 查看详情

android入门第12天-checkbox的使用(代码片段)

...三个组件即三列,因此第一行和第二行里的组件中使用:android:layout_span="3"做三列合并操作;UI端代码activity_main.xml<?xmlversion="1.0"encoding="utf-8"?>&l 查看详情

android入门第21天-android里textclock的使用(代码片段)

介绍TextClock是在Android4.2(API17)后推出的用来替代DigitalClock的一个控件。TextClock可以以字符串格式显示当前的日期和时间,因此推荐在Android4.2以后使用TextClock。这个控件推荐在24进制的android系统中使用,TextClock提供了两种... 查看详情

android入门第17天-android里的progressbar的使用(代码片段)

介绍Android里的ProgressBar默认为一个不断转圈的圆,它也可以自定义,但是如我在上一篇里所述我们不追求专业的beautiful。我们求的是Android这本知识我们可以完整的体系化的学会。因此在本篇里我们就使用ProgressBar的默认... 查看详情

android入门第16天-android里的switchbutton的使用(代码片段)

...们在手机应用中经常使用到的。我突然想到2012年我开发Android时,竟然使用了RadioButton来做开关这个梗。其实SwitchButton文如其名,它就是一个“开”和“关”两个状态事件存在时使用的,最典型的SwitchButton长下面这么... 查看详情

android入门第18天-android里的seekbar的使用(代码片段)

...来调节音量、模糊阀值等场景。SeekBar里怎么是通过:android:layout_weight="1"这个值设置大小的。其它我们都使用默认的就行。我们先来看UI界面的代码吧。activity_main.xml<?xmlversion=" 查看详情

android入门第22天-android里的计时器chronometer的使用(代码片段)

介绍非常简单的一个计时器,没有太多原理,我们直接上代码。先看课程目标课程目标就是一个简单的计时器,我们直接上使用示例吧界面里有一个计时器,4个按钮。开始计时,上面这个计时器就开始读秒... 查看详情

android入门第11天-android中radiobutton的使用(代码片段)

目标我们在开发中经常会面临:想做一下这样的一个功能。在本篇中我们就来实现RadioButton,当然,本篇中的控件是hardcode的,我们有时在开发中经常还会碰到需要通过后台service动态渲染一些组件。我们会在稍后的篇章中讲到如... 查看详情

android入门第10天-android访问远程springboot提供的restfulapi接口

需求在上一章中Android入门第9天-Android读本地JSON文件并显示中,我们使用本地的json文件来显示json数据。而实际在生产级开发中,我们都是通过远程的后台提供的RestfulService来接收json数据的返回。我们继着上一章在原有的界面中... 查看详情

android入门第44天-android里使用动态broadcast(代码片段)

BroadCast是什么BroadcastReceiver就是应用程序间的全局大喇叭,即通信的一个手段,系统自己在很多时候都会发送广播,比如电量低或者充足,刚启动完,插入耳机,你有一条新的微信消息。。。这种都是使用B... 查看详情

android入门第51天-使用android的sharedpreference存取信息(代码片段)

简介上一篇我们介绍了在android里如何读写本地文件。我们有一种场景,类似网页的cookie,要把用户的一些储如上一次登录、使用的痕迹等信息保存下来以便于每次不需要做重复“填表单”的操作,当在这种场景下我... 查看详情

android入门第19天-android里的ratingbar的使用(代码片段)

介绍我们先来看一下什么叫RatingBar长什么样的。我们很多时候订单评价给5星、打车评价都是用的这个RatingBar。本节我们学的这个RatingBar(星级评分条)其实在使用上是非常简单的,大家在某宝买过东西的对这个应该不陌生,... 查看详情

android入门第20天-android里的scrollview的使用(代码片段)

介绍ScrollView(滚动条),它有两种“滚动条”:竖直滚动条;水平方向上的滚动条:HorizontalScrollView;我们经常可以看到在手机里正在垂直加载一堆的数据,然后过一会加载得内容过多,到了手机的底部... 查看详情

android入门第49天-使用radiogroup+fragment来重构类首页底部4个按钮的界面(代码片段)

简介我们在:Android入门第47天-Fragment的基本使用 中使用Fragment制作了一个类首页底部含4个按钮的界面。今天的课程我们要做的是把第47天里的代码中一部分共用的东西抽象到res/values/themes.xml文件中。另外我们使用RadioGroup天... 查看详情

android入门第52天-在sharedpreference中使用加密(代码片段)

简介在上一篇中,我们讲了SharedPreference的使用。但是那不是一个生产场景。特别是我们举了一个例子,存放登录信息的例子。这个例子里用户的密码没有加密,比如说在真实的实际生产环境里用户的一些敏感信息或... 查看详情