美文网首页
OKHttp的断点续传下载实现

OKHttp的断点续传下载实现

作者: 饭勺 | 来源:发表于2018-09-28 15:55 被阅读0次

一、前言

下载的实现有很多,比较难的是多线程断点续传。本文讲解基于OKHttp,实现单线程的断点续传。

二、功能

1. 支持下载,暂停,删除;

2.支持断点续传;

3.支持全局设置存储路径;

4.支持单个任务指定存储路径;

5.支持自定义文件名;

6.支持任意地方添加下载进度的监听

7.兼容同一个下载链接,服务器内容改变的情况;

三、示意图

image

四、DownloadManager

DownloadManager是一个单例。

1. 提供初始化和全局存储路径设置

public void init(Context context) {
    if (context == null) {
        return;
    }
    mContext = context.getApplicationContext();
}

  /**
   * 设置下载保存的根路径(初始化时设置) APP通用下载位置
   * @param saveFile
   */
public void initSaveFile(String saveFile) {
    if (TextUtils.isEmpty(saveFile)) {
        return;
    }
    DownloadPathConfig.setDownloadPath(saveFile);
}

2. 提供start()开启下载

start(String downloadUrl)
start(String downloadUrl, IDownloadListener callBack)
startWithName(String downloadUrl, String fileName, IDownloadListener callBack)
startWithName(String downloadUrl, String fileName)
startWithPath(String downloadUrl, String savePath)
startWithPath(String downloadUrl, String savePath, IDownloadListener callBack)
start(String downloadUrl, String fileName, String savePath)
start(String downloadUrl, String fileName, String savePath, IDownloadListener callBack)

1) 每一个下载任务,对应一个唯一Key,Key值和url,savePath,fileName关联。

2) 任何一个因素改变都会视为不同的下载任务。

3) 开启下载以后,返回Key值。上层拿到Key值可以进行其他操作,比如暂停,删除和监听此任务的进度。

3.提供stop()暂停下载

stop(String url)
stop(String downloadUrl, String fileName, String savePath)
stop(int key)

4.提供delete()删除任务

delete(String url)
delete(String url, String fileName, String savePath)
delete(int key)

5.提供添加和取消进度监听

addDownloadListener(String downloadUrl, IDownloadListener processListener)
addDownloadListener(String downloadUrl, String fileName, String savePath, IDownloadListener processListener)
addDownloadListener(int key, IDownloadListener processListener)

removeDownloadListener(String downloadUrl, IDownloadListener processListener)
removeDownloadListener(String downloadUrl, String fileName, String savePath, IDownloadListener processListener)
removeDownloadListener(int key, IDownloadListener processListener)

6.构建DownloadInfo和DownloadTask,并且缓存DownloadTask

/**
 * @param downloadUrl
 * @param callBack
 * @return
 */
public int start(String downloadUrl, String fileName, String savePath,
                 IDownloadListener callBack) {
    //1.构建DownloadInfo
    DownloadInfo info = getDownloadInfo(downloadUrl, fileName, savePath);
    if (info == null) {
        return -1;
    }

    //2.创建DownloadTask
    DownloadTask task = getDownloadTask(mContext, info, callBack);
    if (task == null) {
        return -1;
    }
    //3.保存DownloadTask并开始下载
    int key = info.getKey();
    saveDownloadTask(key, task);
    task.start();
    return key;
}

1)构建fileName,上层可以传空,会根据url获取fileName

2)savePath,上层可以传空,会根据初始化的DownloadPathConfig获取存储路径

3)缓存DownloadTask,通过HaskMap缓存,键为该任务的Key值;

4)构建DownloadTask时,如果正在下载(即已经缓存),则整个方法返回-1;

五、DownloadObserver

DownloadObserver缓存了IDownloadListener,通过Rxjava,回调都执行在主线程。

六、DownloadTask

1.构造和init

public DownloadTask(Context context, DownloadInfo info, IDownloadListener callBack) {
    if (context == null || info == null) {
        return;
    }
    mContext = context.getApplicationContext();
    mDownloadInfo = info;
    init(callBack);
}

init中有两点操作:

1)根据key,构建DownloadObserver,同时添加IDwonloadListener。注意这里的IDownloadListener可以为null

2)构建OKHttpClient;

2. RXJava实现线程切换

 //下载
public void start() {

    Observable.just(mDownloadInfo).flatMap(new Function<DownloadInfo, Observable<DownloadInfo>>() {
        @Override
        public Observable<DownloadInfo> apply(DownloadInfo info) throws Exception {
            return Observable.just(createDownInfo(info));
        }
    }).flatMap(new Function<DownloadInfo, Observable<DownloadInfo>>() {
        @Override
        public Observable<DownloadInfo> apply(DownloadInfo downloadInfo) throws Exception {
            return Observable.create(new DownloadSubscribe(downloadInfo));
        }
    }).observeOn(AndroidSchedulers.mainThread())//在主线程回调
            .subscribeOn(Schedulers.io())//在子线程执行
            .subscribe(mDownloadObserver);
}

1)这里是结合数据库存储的信息再对DownloadInfo进行完善;执行在子线程;

2)如果数据库有记录,并且对应下载文件存在同时length不为0,则说明其已经有下载部分了。
此时,会在去服务器获取一次信息,拿到服务器的contentLength再和数据库记录的totalSize作对比,
如果不一致,则说明内容已变,需要清除数据删除文件,重新下载。获取服务器ContentLength逻辑如下:

/**
 * 请求完整数据长度
 * @param url
 */
private long getServerContentLength(String url) {
    Request request = new Request.Builder()
            .url(url)
            .build();
    try {
        okhttp3.Response response = mClient.newCall(request).execute();
        if (response != null && response.isSuccessful()) {
            long contentLength = response.body().contentLength();
            response.close();
            return contentLength == 0 ? -1 : contentLength;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return -1;
}

3. DownloadSubscribe

 //在subscribe()中真正的开启下载。
    @Override
    public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {

        String url = downloadInfo.getUrl();
        long downloadLength = downloadInfo.getDownloadPosition();//已经下载好的长度
        long responseLength = downloadInfo.getTotalSize();//文件的总长度, 注意此处可能为0
        String saveFilePath = downloadInfo.getSavePath();
        File saveFile = new File(saveFilePath);
        if (!saveFile.exists()) {
            saveFile.getParentFile().mkdirs();
        }
        if (!saveFile.exists()) {
            saveFile.createNewFile();
        }
        if (responseLength == -1) {
            e.onError(new SocketTimeoutException());
            return;
        }

        Request.Builder builder = new Request.Builder().url(url);
        if (responseLength != 0) {
            builder.addHeader("RANGE", "bytes=" +
                    downloadLength + "-" + responseLength);
        }
        Request request = builder.build();
        Call call = mClient.newCall(request);
        okhttp3.Response response = call.execute();

        if (responseLength == 0) {
            responseLength = response.body().contentLength();
        }
        //初始进度信息
        e.onNext(downloadInfo);
        if (downloadLength == 0) {
            downloadInfo.setTotalSize(responseLength);
            DownloadInfoDaoHelper.insertInfo(downloadInfo);
        }
        if (downloadLength >= responseLength) {
            //初始进度信息
            e.onNext(downloadInfo);
            e.onComplete();//完成
            return;
        }

        RandomAccessFile randomAccessFile = null;
        InputStream inputStream = null;
        try {
            randomAccessFile = new RandomAccessFile(saveFile, "rwd");
            randomAccessFile.seek(downloadLength);
            inputStream = response.body().byteStream();
            byte[] buffer = new byte[1024 * 16];//缓冲数组16kB
            int len;
            while (!mDelete && !mExit && (len = inputStream.read(buffer)) != -1) {
                randomAccessFile.write(buffer, 0, len);
                downloadLength += len;
                downloadInfo.setDownloadPosition(downloadLength);
                e.onNext(downloadInfo);
            }
        } catch (Exception t) {
            e.onError(t);
            return;
        } finally {
            //关闭IO流
            IOUtil.closeAll(inputStream, randomAccessFile);
        }
        if (mDelete) { //删除操作
            downloadInfo.setDownloadPosition(0);
            e.onNext(downloadInfo);
        }else if (mExit) { //stop操作
            e.onError(new Throwable(IDownloadListener.PAUSE_STATE));
        }else {
            e.onComplete();//完成
        }
    }

1)  mDelete和mExit都是从上层执行删除和暂停设置进来的,因为回调不一样,所以要区分标记。

2)  所有ObservableEmitter<DownloadInfo> e执行的操作最后都在DownloadObserver中执行,此时已是在主线程。

3) DownloadInfoDaoHelper.insertInfo(downloadInfo);在确定为第一次下载或者说还没开始下载一个字节时,记录一次数据库,数据库中存储Key值,url,fileName,savePath,totalSize等信息。

4) builder.addHeader("RANGE","bytes=" + downloadLength +"-" + responseLength);这里意味着请求服务器的部分数据,因为有了一部分下载,所以并不需要请求整段数据信息。

至此,整个下载流程便讲解完了。

七、总结

下载的要点,一是保存totalsize信息,二是断点续传时获取到上次已经下载的位置。

其余逻辑在实现上和扩展性上即可自由发挥。

Demo地址点这里

相关文章

网友评论

      本文标题:OKHttp的断点续传下载实现

      本文链接:https://www.haomeiwen.com/subject/fbdpoftx.html