Android实现多线程下载文件,支持断点

本篇博客主要介绍多线程去下载文件,以下载多个app为例。不管去下在app,音视频等文件,实现起来都一样。篇幅有点长,先贴张美女图看看

这里写图片描述

正在下载的效果图

2018-04-25_13_36_47.gif

下载完成效果图

这里写图片描述
小编的下载路径是放在sd卡的绝对路径中,方便验证!

工程目录图

这里写图片描述

介绍下每个类是干什么的

    
DownloadCallback:下载完成回调接口,包含三个方法 void onSuccess(File file)、void onFailure(Exception e)、void onProgress(long progress,long currentLength);
DownloadDispatcher:负责创建线程池,连接下载的文件;
DownloadRunnable:每个线程的执行对应的任务;
DownloadTask:每个apk的下载,这个类需要复用的;
ircleProgressbar:自定义的圆形进度条;

具体思路:

1、首先自定义一个圆形进度条CircleProgressbar,实时更新进度
2、创建线程池,计算每个线程对应的不同的Range
3、每个线程下载完毕之后的回调,若出现了异常怎么处理

OkHttpManager类

public class OkHttpManager {
private static final OkHttpManager sOkHttpManager = new OkHttpManager();
private OkHttpClient okHttpClient;

private OkHttpManager() {
    okHttpClient = new OkHttpClient();
}

public static OkHttpManager getInstance() {
    return sOkHttpManager;
}

public Call asyncCall(String url) {

    Request request = new Request.Builder()
            .url(url)
            .build();
    return okHttpClient.newCall(request);
}

public Response syncResponse(String url, long start, long end) throws IOException {
    Request request = new Request.Builder()
            .url(url)
            //Range 请求头格式Range: bytes=start-end
            .addHeader("Range", "bytes=" + start + "-" + end)
            .build();
    return okHttpClient.newCall(request).execute();
}
}

大家可能会看到这个Range很懵,Range是啥?

什么是Range?
当用户在听一首歌的时候,如果听到一半(网络下载了一半),网络断掉了,用户需要继续听的时候,文件服务器不支持断点的话,则用户需要重新下载这个文件。而Range支持的话,客户端应该记录了之前已经读取的文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个 文件发送回客户端,以此节省网络带宽。

例如:
Range: bytes=10- :第10个字节及最后个字节的数据 。
Range: bytes=40-100 :第40个字节到第100个字节之间的数据。
注意,这个表示[start,end],即是包含请求头的start及end字节的,所以,下一个请求,应该是上一个请求的[end+1, nextEnd]

DownloadCallback类

public interface DownloadCallback {
    /**
     * 下载成功
     *
     * @param file
     */
    void onSuccess(File file);

    /**
     * 下载失败
     *
     * @param e
     */
    void onFailure(Exception e);

    /**
     * 下载进度
     *
     * @param progress
     */
    void onProgress(long progress,long currentLength);
}

DownloadCallback:下载完成回调接口,包含三个方法 void onSuccess(File file)下载文件成功回调、void onFailure(Exception e)下载文件失败回调、void onProgress(long progress,long currentLength) 下载文件实时更新下圆形进度条。

DownloadDispatcher 类

public class DownloadDispatcher {
    private static final String TAG = "DownloadDispatcher";
    private static volatile DownloadDispatcher sDownloadDispatcher;
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int THREAD_SIZE = Math.max(3, Math.min(CPU_COUNT - 1, 5));
    //核心线程数
    private static final int CORE_POOL_SIZE = THREAD_SIZE;
    //线程池
    private ExecutorService mExecutorService;
    //private final Deque<DownloadTask> readyTasks = new ArrayDeque<>();
    private final Deque<DownloadTask> runningTasks = new ArrayDeque<>();
    //private final Deque<DownloadTask> stopTasks = new ArrayDeque<>();


private DownloadDispatcher() {
}

public static DownloadDispatcher getInstance() {
    if (sDownloadDispatcher == null) {
        synchronized (DownloadDispatcher.class) {
            if (sDownloadDispatcher == null) {
                sDownloadDispatcher = new DownloadDispatcher();
            }
        }
    }
    return sDownloadDispatcher;
}

/**
 * 创建线程池
 *
 * @return mExecutorService
 */
public synchronized ExecutorService executorService() {
    if (mExecutorService == null) {
        mExecutorService = new ThreadPoolExecutor(CORE_POOL_SIZE, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), new ThreadFactory() {
            @Override
            public Thread newThread(@NonNull Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(false);
                return thread;
            }
        });
    }
    return mExecutorService;
}


/**
 * @param name     文件名
 * @param url      下载的地址
 * @param callBack 回调接口
 */
public void startDownload(final String name, final String url, final DownloadCallback callBack) {
    Call call = OkHttpManager.getInstance().asyncCall(url);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) {
            callBack.onFailure(e);
        }

        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) {
            //获取文件的大小
            long contentLength = response.body().contentLength();
            Log.i(TAG, "contentLength=" + contentLength);
            if (contentLength <= -1) {
                return;
            }
            DownloadTask downloadTask = new DownloadTask(name, url, THREAD_SIZE, contentLength, callBack);
            downloadTask.init();
            runningTasks.add(downloadTask);
        }
    });
}


/**
 * @param downLoadTask 下载任务
 */
public void recyclerTask(DownloadTask downLoadTask) {
    runningTasks.remove(downLoadTask);
    //参考OkHttp的Dispatcher()的源码
    //readyTasks.
}

public void stopDownLoad(String url) {
    //这个停止是不是这个正在下载的
}
}

DownloadDispatcher这个类主要负责创建线程池,连接下载的文件,如果你要控制下载文件的个数,比如3-5个,可以在这个类控制,比如你最大允许同时下载三个文件,每个文件有五个线程去下载,那么maxRequest只有15个线程,其余的可以放到readyTasks 中,有一个线程下载完毕了可以remove()掉,总结起来说一句话,去仿照okhttp的Dispatcher源码去写,runningTasks、readyTasks、stopTasks。

DownloadTask类

public class DownloadTask {
    private static final String TAG = "DownloadTask";
    //文件下载的url
    private String url;
    //文件的名称
    private String name;
    //文件的大小
    private long mContentLength;
    //下载文件的线程的个数
    private int mThreadSize;
    //线程下载成功的个数,变量加个volatile,多线程保证变量可见性
    private volatile int mSuccessNumber;
    //总进度=每个线程的进度的和
    private long mTotalProgress;
    private List<DownloadRunnable> mDownloadRunnables;
    private DownloadCallback mDownloadCallback;


public DownloadTask(String name, String url, int threadSize, long contentLength, DownloadCallback callBack) {
    this.name = name;
    this.url = url;
    this.mThreadSize = threadSize;
    this.mContentLength = contentLength;
    this.mDownloadRunnables = new ArrayList<>();
    this.mDownloadCallback = callBack;
}

public void init() {
    for (int i = 0; i < mThreadSize; i++) {
        //初始化的时候,需要读取数据库
        //每个线程的下载的大小threadSize
        long threadSize = mContentLength / mThreadSize;
        //开始下载的位置
        long start = i * threadSize;
        //结束下载的位置
        long end = start + threadSize - 1;
        if (i == mThreadSize - 1) {
            end = mContentLength - 1;
        }
        DownloadRunnable downloadRunnable = new DownloadRunnable(name, url, mContentLength, i, start, end, new DownloadCallback() {
            @Override
            public void onFailure(Exception e) {
                //有一个线程发生异常,下载失败,需要把其它线程停止掉
                mDownloadCallback.onFailure(e);
                stopDownload();
            }

            @Override
            public void onSuccess(File file) {
                mSuccessNumber = mSuccessNumber + 1;
                if (mSuccessNumber == mThreadSize) {
                    mDownloadCallback.onSuccess(file);
                    DownloadDispatcher.getInstance().recyclerTask(DownloadTask.this);
                    //如果下载完毕,清除数据库  todo
                }
            }

            @Override
            public void onProgress(long progress, long currentLength) {
                //叠加下progress,实时去更新进度条
                //这里需要synchronized下
                synchronized (DownloadTask.this) {
                    mTotalProgress = mTotalProgress + progress;
                    //Log.i(TAG, "mTotalProgress==" + mTotalProgress);
                    mDownloadCallback.onProgress(mTotalProgress, currentLength);
                }
            }
        });
        //通过线程池去执行
        DownloadDispatcher.getInstance().executorService().execute(downloadRunnable);
        mDownloadRunnables.add(downloadRunnable);
    }
}

/**
 * 停止下载
 */
public void stopDownload() {
    for (DownloadRunnable runnable : mDownloadRunnables) {
        runnable.stop();
    }
}

DownloadTask负责每个apk的下载,这个类需要复用的。计算每个线程下载范围的大小,具体的每个变量是啥?注释写的很清楚。注意的是这个变量mSuccessNumber,线程下载成功的个数,变量加个volatile,多线程保证变量可见性。还有的就是叠加下progress的时候mTotalProgress = mTotalProgress + progress,需要synchronized(DownloadTask.this)下,保证这个变量mTotalProgress内存可见,并同步下。

DownloadRunnable类

public class DownloadRunnable implements Runnable {
    private static final String TAG = "DownloadRunnable";
    private static final int STATUS_DOWNLOADING = 1;
    private static final int STATUS_STOP = 2;
    //线程的状态
    private int mStatus = STATUS_DOWNLOADING;
    //文件下载的url
    private String url;
    //文件的名称
    private String name;
    //线程id
    private int threadId;
    //每个线程下载开始的位置
    private long start;
    //每个线程下载结束的位置
    private long end;
    //每个线程的下载进度
    private long mProgress;
    //文件的总大小 content-length
    private long mCurrentLength;
    private DownloadCallback downloadCallback;

public DownloadRunnable(String name, String url, long currentLength, int threadId, long start, long end, DownloadCallback downloadCallback) {
    this.name = name;
    this.url = url;
    this.mCurrentLength = currentLength;
    this.threadId = threadId;
    this.start = start;
    this.end = end;
    this.downloadCallback = downloadCallback;
}

@Override
public void run() {
    InputStream inputStream = null;
    RandomAccessFile randomAccessFile = null;
    try {
        Response response = OkHttpManager.getInstance().syncResponse(url, start, end);
        Log.i(TAG, "fileName=" + name + " 每个线程负责下载文件大小contentLength=" + response.body().contentLength()
                + " 开始位置start=" + start + "结束位置end=" + end + " threadId=" + threadId);
        inputStream = response.body().byteStream();
        //保存文件的路径
        File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), name);
        randomAccessFile = new RandomAccessFile(file, "rwd");
        //seek从哪里开始
        randomAccessFile.seek(start);
        int length;
        byte[] bytes = new byte[10 * 1024];
        while ((length = inputStream.read(bytes)) != -1) {
            if (mStatus == STATUS_STOP)
                break;
            //写入
            randomAccessFile.write(bytes, 0, length);
            //保存下进度,做断点 todo
            mProgress = mProgress + length;
            //实时去更新下进度条,将每次写入的length传出去
            downloadCallback.onProgress(length, mCurrentLength);
        }
        downloadCallback.onSuccess(file);
    } catch (IOException e) {
        e.printStackTrace();
        downloadCallback.onFailure(e);
    } finally {
        Utils.close(inputStream);
        Utils.close(randomAccessFile);
        //保存到数据库 怎么存?? todo
    }
}

public void stop() {
    mStatus = STATUS_STOP;
}
}

DownloadRunnable负责每个线程的执行对应的任务,使用RandomAccessFile写入文件。最后看一张截图

这里写图片描述

哈,看到最后断点下载并没有实现,这个不急,小编还没写,但是实现多线程下载文件整体的思路和代码都已经出来了,至于断点怎么弄,其实具体的思路,在代码注解也已经写出来了,主要是DownloadRunnable 这个类,向数据库保存下文件的下载路径url,文件的大小currentLength,每个线程id,对应的每个线程的start位置和结束位置,以及每个线程的下载进度progress。用户下次进来可以读取数据库的内容,有网的情况下重新发请请求,对应Range: bytes=start-end;

整个项目粘贴了部分主要的代码。

项目完整代码https://github.com/StevenYan88/MultiThreadDownload

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现文件多线程下载需要以下步骤: 1. 创建一个下载任务队列,存放所有需要下载文件信息,包括文件名、文件大小、下载链接等。 2. 为每个下载任务创建一个下载线程,并将下载任务分配给下载线程。 3. 下载线程使用多线程的方式下载文件,可以根据文件大小划分多个线程,每个线程负责下载文件的一部分数据。 4. 在下载过程中,需要记录已经下载的字节数和下载进度,以便于断点续传。 5. 如果下载过程中出现网络异常或者用户暂停了下载,需要保存已下载的数据,以便于下次继续下载。 6. 如果用户需要暂停或取消下载,需要停止所有下载线程,并删除已下载的数据。 以下是一个简单的多文件多线程下载的示例代码: 1. 创建一个下载任务类,用于存储文件信息和下载状态 ```java public class DownloadTask { public String fileName; public String url; public long fileSize; public int status; public long progress; // ...其他属性和方法 } ``` 2. 创建一个下载线程类,用于下载指定的文件 ```java public class DownloadThread extends Thread { private String url; private long start; private long end; private RandomAccessFile raf; private DownloadListener listener; public DownloadThread(String url, long start, long end, RandomAccessFile raf, DownloadListener listener) { this.url = url; this.start = start; this.end = end; this.raf = raf; this.listener = listener; } @Override public void run() { HttpURLConnection conn = null; InputStream is = null; byte[] buffer = new byte[1024]; int len; try { URL url = new URL(this.url); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes=" + start + "-" + end); is = conn.getInputStream(); while ((len = is.read(buffer)) != -1) { raf.write(buffer, 0, len); listener.onProgress(len); } listener.onComplete(); } catch (Exception e) { listener.onError(e); } finally { try { if (is != null) { is.close(); } if (conn != null) { conn.disconnect(); } } catch (Exception e) { e.printStackTrace(); } } } } ``` 3. 创建一个下载管理类,用于管理所有下载任务和下载线程 ```java public class DownloadManager { private static final int MAX_THREAD_COUNT = 3; // 最大下载线程数 private static final String DOWNLOAD_DIR = "/sdcard/download/"; // 下载文件保存目录 private List<DownloadTask> taskList; private List<DownloadThread> threadList; private DownloadListener listener; public DownloadManager() { taskList = new ArrayList<>(); threadList = new ArrayList<>(); } // 添加下载任务 public void addTask(String url, String fileName, long fileSize) { DownloadTask task = new DownloadTask(); task.url = url; task.fileName = fileName; task.fileSize = fileSize; task.status = DownloadTask.STATUS_PENDING; taskList.add(task); if (listener != null) { listener.onTaskAdded(task); } } // 开始下载任务 public void startTask(DownloadTask task) { if (task.status == DownloadTask.STATUS_DOWNLOADING) { return; } task.status = DownloadTask.STATUS_DOWNLOADING; File file = new File(DOWNLOAD_DIR + task.fileName); long downloadedSize = file.exists() ? file.length() : 0; task.progress = downloadedSize; long blockSize = (task.fileSize + MAX_THREAD_COUNT - 1) / MAX_THREAD_COUNT; for (int i = 0; i < MAX_THREAD_COUNT; i++) { long start = i * blockSize + downloadedSize; long end = (i == MAX_THREAD_COUNT - 1) ? task.fileSize - 1 : (i + 1) * blockSize - 1; try { RandomAccessFile raf = new RandomAccessFile(file, "rw"); raf.seek(start); DownloadThread thread = new DownloadThread(task.url, start, end, raf, new DownloadListener() { @Override public void onProgress(int len) { synchronized (task) { task.progress += len; if (listener != null) { listener.onProgress(task); } } } @Override public void onComplete() { synchronized (task) { boolean allThreadsComplete = true; for (DownloadThread thread : threadList) { if (thread.getState() != State.TERMINATED) { allThreadsComplete = false; break; } } if (allThreadsComplete) { task.status = DownloadTask.STATUS_COMPLETE; if (listener != null) { listener.onComplete(task); } } } } @Override public void onError(Exception e) { synchronized (task) { task.status = DownloadTask.STATUS_ERROR; if (listener != null) { listener.onError(task, e); } } } }); threadList.add(thread); thread.start(); } catch (Exception e) { e.printStackTrace(); } } } // 暂停下载任务 public void pauseTask(DownloadTask task) { task.status = DownloadTask.STATUS_PAUSED; for (DownloadThread thread : threadList) { thread.interrupt(); } threadList.clear(); if (listener != null) { listener.onPause(task); } } // 取消下载任务 public void cancelTask(DownloadTask task) { task.status = DownloadTask.STATUS_CANCELED; File file = new File(DOWNLOAD_DIR + task.fileName); if (file.exists()) { file.delete(); } for (DownloadThread thread : threadList) { thread.interrupt(); } threadList.clear(); if (listener != null) { listener.onCancel(task); } } // 设置下载监听器 public void setDownloadListener(DownloadListener listener) { this.listener = listener; } public interface DownloadListener { void onTaskAdded(DownloadTask task); void onProgress(DownloadTask task); void onComplete(DownloadTask task); void onPause(DownloadTask task); void onCancel(DownloadTask task); void onError(DownloadTask task, Exception e); } } ``` 4. 在Activity中调用下载管理类,实现文件下载功能 ```java public class DownloadActivity extends AppCompatActivity implements DownloadManager.DownloadListener { private DownloadManager downloadManager; private ListView listView; private DownloadListAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_download); downloadManager = new DownloadManager(); downloadManager.setDownloadListener(this); listView = (ListView) findViewById(R.id.list_view); adapter = new DownloadListAdapter(this, downloadManager.getTaskList()); listView.setAdapter(adapter); } // 添加新的下载任务 public void addTask(String url, String fileName, long fileSize) { downloadManager.addTask(url, fileName, fileSize); } // 开始下载任务 public void startTask(DownloadTask task) { downloadManager.startTask(task); } // 暂停下载任务 public void pauseTask(DownloadTask task) { downloadManager.pauseTask(task); } // 取消下载任务 public void cancelTask(DownloadTask task) { downloadManager.cancelTask(task); } @Override public void onTaskAdded(DownloadTask task) { adapter.notifyDataSetChanged(); } @Override public void onProgress(DownloadTask task) { adapter.notifyDataSetChanged(); } @Override public void onComplete(DownloadTask task) { adapter.notifyDataSetChanged(); } @Override public void onPause(DownloadTask task) { adapter.notifyDataSetChanged(); } @Override public void onCancel(DownloadTask task) { adapter.notifyDataSetChanged(); } @Override public void onError(DownloadTask task, Exception e) { adapter.notifyDataSetChanged(); } } ``` 以上代码仅作为示例,实际项目中还需要考虑一些异常情况,例如网络异常、存储空间不足等。同时应该注意线程同步和异常处理,以确保下载任务的正确性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值