App网络请求实战三:下载文件以及断点续载

App网络请求实战三:下载文件以及断点续载

瓜皮已上线,嘿嘿。想反杀,不存在的!

小老板,多捞哦。

还是原来的配方,无图言diao

本篇记录的是如何将下载功能集成到网络框架中。

解题步骤

1.大的方面来了解okhttp、retrofit、rxjava三者之间的关系

a.首先okhttp是一个Http底层请求库,square公司开发已被谷歌Android采用。和okhttp对应的有:httpurlconnection和httpClient。其中httpClient已经被弃用了。

b.然后retrofit是一个Http高层请求库,按理说它应该不关心底层的http请求库是什么。不信你可以注意到我们每次写retrofit有这么一句

new Retrofit.Builder()
        .client(client);

按照扩展性原则来说,这个client可以是httpurlconnection或者是okhttp或者是httpClient。但是retrofit2.0之后貌似直接将其定位okhttp了,也就是说retrofit只能配置okhttp了。也可以从retrofit源码中看出的:

/**
 * The HTTP client used for requests.
 * <p>
 * This is a convenience method for calling {@link #callFactory}.
 */
public Builder client(OkHttpClient client) {
  return callFactory(checkNotNull(client, "client == null"));
}

/**
 * Specify a custom call factory for creating {@link Call} instances.
 * <p>
 * Note: Calling {@link #client} automatically sets this value.
 */
public Builder callFactory(okhttp3.Call.Factory factory) {
  this.callFactory = checkNotNull(factory, "factory == null");
  return this;
}

和retrofit对应的框架有volley、async-http-client。

c.最后rxjava其实跟网络没什么关系,rxjava在这里面其的作用就是响应式编程和链式编程范式,以及更加轻易的处理异步问题。在项目里,凡是涉及到异步的操作都可以使用rxjava来处理,不一定是网络请求。

2.retrofit下载文件

明白了上面的关系后,讲道理我们可以知道最终实现下载的还是okhttp,只是说现在套了一层retrofit。所以呢,胸低们,我们首先必须要了解retrofit下载文件的方式。两种方式如下:

//方式一
//下载文件
//如果文件非常大,必须要使用Streaming注解。否则retrofit会默认将整个文件读取到内存中
//造成OOM
@Streaming
@GET
Observable<ResponseBody> downLoadFile(@Url String fileUrl);

//方式二
@Streaming
@GET("tools/test.apk")
Observable<ResponseBody> downLoadFile();

从上往下看,注意下载文件必须要添加Steaming注解。因为如果不添加这个注解,如果文件非常大,retrofit会默认将整个文件读取到内存中,造成OOM。两种方式的区别:方式一可以更灵活的更改下载的url,方式二只能下载baseUrl下的文件。懂了不,一个灵活,一个死板,所以果断选择灵活的那个。其次期望返回的是okhttp的ResponseBody对象,这个对象有个方法:

public final InputStream byteStream() {
  return source().inputStream();
}

兄弟们,懂了不,拿到输入流了,至此我们可以为所欲为了。后面的操作和httpurlConnection、httpClient甚至是利用okhttp下载是一样一样儿滴~~下面赶紧试验下,不会打我脸吧,擦,好怕怕哦。

String url = "http://imtt.dd.qq.com/16891/8EE1D586937A31F6E0B14DA48F8D362E.apk?fsname=com.dewmobile.kuaiya_5.4.2(CN)_216.apk&csr=1bbd";
Observable<ResponseBody> observable = apiService.downLoadFile(url)
        .compose(RxSchedulers.<ResponseBody>io_main());
observable.subscribe(new DownLoadObserver()
{
    @Override
    public void onNext(ResponseBody responseBody)
    {
        //拿到输入流后,我们就可以终极为所欲为至为所欲为!
        InputStream inputStream = responseBody.byteStream();
    }

    @Override
    public void onError(Throwable e)
    {
        Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show();
    }
});

看我多贴心的给大伙儿找到一个下载apk的链接地址。注意,这里就没必要用map来转换返回类型了哦,只需要添加线程切换转换即可。这里再次强调下什么时候用map,咳咳,注意了大胸弟:

首先

@Streaming
@GET
Observable<ResponseBody> downLoadFile(@Url String fileUrl);

表示apiService调用这个downLoadFile方法,我们期望返回的是一个ResponseBody对象对吧?对呀,我们本来就是要这个对象来生成输入流,所以还转个屁呀。之前要map转换类型是因为:

//获取token过期的http
@GET("tools/mockapi/440/token_expired")
Observable<BaseResponse<ResEntity1.DataBean>> getExpiredHttp();

这里我们期望的是BaseResponse对象,而我们实际要的是ResEntity1.DataBean对象,所以需要转换。擦,好累,懂得都懂,还不懂别问,往下看吧。

其实到这里只是剩下java的文件io读写了,只是要注意的是io操作是需要开启子线程的,可以说已经完成了下载功能了。但是有个大胸弟不乐意地说:特么你这里既不能显示下载进度,又不能控制开始下载、暂停下载、取消下载啊。我特么跳起来打你膝盖哦:老铁,能别bb了不?

3.显示下载进度、控制开始、暂停、取消下载

其实下载进度这个功能还算好实现,只需要用已经下载文件的长度比上总文件的长度即可

int progress = (int) (downloadedLength * 100 / contentLength);

但是我们假设啊,只是假设,现在有个老铁在外面用着4g网络,不小心点击了下载。完犊子了,不能暂停、取消下载,大骂是那个**开发的程序。

胸低们我怕被跨省千里追sa,只有想想办法怎么实现暂停下载呢,想想还有点小委屈呢。首先分析上面的需求就是:假设下载文件10M,我能不能先下载1M,然后点击暂停,然后出去high了,high完再继续从1M那里继续把流写入文件。那java里有木有这样一个类,可以控制文件指针长度偏移量的类呢,就是说可以跳到这个文件1M的那个地方的类。是有的,是RandomAccessFile类。这个类就不详细介绍了,反正他有实现我们这个需求的功能。其次是下载一半5M后,发现擦下错文件了,就要取消下载,注意哦,取消下载伴随这有个隐藏操作就是删除掉已经下好的5M文件。切记切记,不然也会被跨省的(这里其实也可以用FileOutputStream流来操作)。

但是但是,重点来了,貌似rxjava和retrofit配合无法像下面这样同步返回:

Response<BaseResponse<ResEntity1.DataBean>> tokenRes = call.execute();

(我实在不知道怎么用rxjava做出这样的,有木有大佬知道的哦)

而异步回调又做不到既能提供下载进度,又能暂停下载,取消下载。所以必须得想另一个方法。提供一种okhttpi下载的代码(引自郭霖第一行代码):

try
{
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
            .addHeader("RANGE", "bytes=" + downloadedLength + "-")
            .url(downloadUrl)
            .build();
    Response response = client.newCall(request).execute();
    if (response != null)
    {
        Log.e("DownLoadTask", "(DownLoadTask.java:78)" + response.body().contentLength());
        is = response.body().byteStream();
        savedFile = new RandomAccessFile(file, "rw");
        savedFile.seek(downloadedLength);
        byte[] bytes = new byte[1024];
        int total = 0;
        int len;
        while ((len = is.read(bytes)) != -1)
        {
            if (isCanceled)
            {
                return TYPE_CANCELED;
            } else if (isPaused)
            {
                return TYPE_PAUSE;
            } else
            {
                total += len;
                savedFile.write(bytes, 0, len);
                int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                publishProgress(progress);
            }
        }
    }
    response.body().close();
    return TYPE_SUCCESS;
} catch (IOException e)
{
    e.printStackTrace();
} finally
{
    try
    {
        if (is != null)
        {
            is.close();
        }
        if (savedFile != null)
        {
            savedFile.close();
        }
        if (isCanceled && file != null)
        {
            file.delete();
        }
    } catch (IOException e)
    {
        e.printStackTrace();
    }
}

其实这里涉及到两个问题:

  • 下载进度
  • 断点续载

4.下载进度

关键在于自定义ResponseBody,注意里面的source方法,我们可以在这里面对source进行提前操作,感觉跟okhttp的拦截器的思想很像。

首先定义一个下载接口:

/**
 * <pre>
 *     作者   : 肖坤
 *     时间   : 2018/04/20
 *     描述   :
 *     版本   : 1.0
 * </pre>
 */
public interface DownLoadListener
{
    void onProgress(int progress, boolean downSuc, boolean downFailed);
}

3个参数分别是下载进度,是否下载完成,是否下载失败;

其次看下自定义的ResponseBody代码如下:

@Override
public BufferedSource source()
{
    if (bufferedSource == null)
    {
        bufferedSource = Okio.buffer(source(mResponseBody.source()));
    }
    return bufferedSource;
}

private Source source(Source source)
{
    return new ForwardingSource(source)
    {
        @Override
        public long read(Buffer sink, long byteCount) throws IOException
        {
            long bytesRead = super.read(sink, byteCount);
            totalBytesRead += bytesRead != -1 ? bytesRead : 0;
            if (bytesRead == -1)
            {
                //下载完成
                sHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        mListener.onProgress(100, true, false);
                    }
                });
            } else
            {
                //正在下载中,更新进度
                sHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        int progress = (int) (totalBytesRead * 100 / contentLength);
                        mListener.onProgress(progress, false, false);
                    }
                });
            }
            saveToFile(sink);
            return bytesRead;
        }
    };
}

//写入文件
private void saveToFile(Buffer buffer)
    {
        InputStream inputStream = buffer.inputStream();
        RandomAccessFile saveFile = null;
        try
        {
            saveFile = new RandomAccessFile(file, "rw");
            saveFile.seek(getDownloadedLength());
            byte[] bytes = new byte[1024];
            int len;
            while ((len = inputStream.read(bytes)) != -1)
            {
                saveFile.write(bytes, 0, len);
            }
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e)
        {
            e.printStackTrace();
        } finally
        {
            try
            {
                if (inputStream != null)
                {
                    inputStream.close();
                }
                if (saveFile != null)
                {
                    saveFile.close();
                }
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

就是那个ForwardingSource类,真的感觉这个东西有点像okhttp中的拦截器,就是把那个source拦下来,然后就可以做一些不可描述的事情,嘿嘿嘿。这里为了后面的断点续载功能,用到了RandomAccessFile。其实也是可以用FileOutputStream的两个参数的构造函数,如下

//if <code>true</code>, then bytes will be written to the end of the file rather than the //beginning
public FileOutputStream(String name, boolean append)
    throws FileNotFoundException
{
    this(name != null ? new File(name) : null, append);
}

第二个参数true也是可以继续写入的。总体来说下载进度还是比较简单的。

5.断点续载

其实关键就是,玛德文字说不清楚。看图:

这里写图片描述

Http请求头中有一个属性如下:

request = request.newBuilder()
        .header("RANGE", "bytes=" + downloadedLength + "-")
        .build();

这个属性的作用就是可以更改请求域,比如说我想从第500个字节处开始下载。那么如何动态修改它呢,没错还是用到Interceptor拦截器:

//下载文件拦截器
static Interceptor downloadInterceptor = new Interceptor()
{
    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Request request = chain.request();
        //记录已写入的文件的长度
        long downloadedLength = DownloadManager.dSp.getLong(downloadEntity.getFileName(), 0);

        Response proceed = chain.proceed(request);
        //保存源文件的总长度(关键)
        DownloadManager.dSp.edit().putLong(downloadEntity.getFileName() + "content_length", proceed.body().contentLength()).commit();
        request = request.newBuilder()
                .header("RANGE", "bytes=" + downloadedLength + "-")
                .build();
        Response response = chain.proceed(request);
        response = response.newBuilder()
                .body(new ProgressResponseBody(response.body(), downloadEntity))
                .build();
        return response;
    }
};

所有进度progress = (已写入文件的长度+正在下载的长度)/源文件的总长度,代码在项目中有我就不贴了。还有一个要注意就是暂停下载和取消下载,我利用的是rxjava中的Disposable,我特意写了一个下载管理类,如下所示:

/**
 * Created by 肖坤 on 2018/4/22.
 *
 * @author 肖坤
 * @date 2018/4/22
 */

public class DownloadManager
{
    public static SharedPreferences dSp;

    /**
     * 初始化DownloadManager
     *
     * @param context
     */
    public static void initDownManager(Context context)
    {
        dSp = context.getSharedPreferences("download_file", Context.MODE_PRIVATE);
    }

    /**
     * 暂停下载
     *
     * @param disposable 控制rxjava的开关
     * @param fileName   下载的文件名,必须包含后缀
     */
    public static void pauseDownload(Disposable disposable, String fileName)
    {
        if (disposable == null || TextUtils.isEmpty(fileName))
        {
            return;
        }
        if (!disposable.isDisposed())
        {
            disposable.dispose();
        }
        if (dSp == null)
        {
            throw new NullPointerException("必须首先初始化DownloadManager");
        }
        File file = initFile(fileName);
        if (file.exists() && dSp != null)
        {
            dSp.edit().putLong(file.getName(), file.length()).commit();
        }
    }

    /**
     * 取消下载
     *
     * @param disposable 控制rxjava的开关
     * @param fileName   下载的文件名,必须包含后缀
     */
    public static void cancelDownload(Disposable disposable, String fileName)
    {
        if (disposable == null || TextUtils.isEmpty(fileName))
        {
            return;
        }
        if (!disposable.isDisposed())
        {
            disposable.dispose();
        }
        File file = initFile(fileName);
        if (file.exists() && dSp != null)
        {
            dSp.edit().putLong(file.getName(), 0).commit();
        }
        //取消下载,最后一步记得删除掉已经下载的文件
        if (file.exists())
        {
            file.delete();
        }
    }

    public static File initFile(String fileName)
    {
        String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
        File file = new File(directory + File.separator + fileName);
        return file;
    }
}

注意哦,我在取消下载的时候删除了写入的文件,没毛病,老铁。整的明明白白的,德云社有牌面!
最后我们在项目里下载也相当简单,代码这样写:

//测试下载文件哦
    private void downloadFile()
    {
        fileName = "httpTest.apk";
        downloadEntity = new DownloadEntity(loadListener, fileName);
        ApiService apiService = RetrofitHelper.createService(ApiService.class,
                RetrofitHelper.getDownloadRetrofit(downloadEntity));

        Observable<ResponseBody> observable = apiService.downLoadFile(url)
                .subscribeOn(Schedulers.io());
        observable.subscribe(new DownLoadObserver()
        {
            @Override
            public void onSubscribe(Disposable d)
            {
                disposable = d;
            }
        });
    }

然后暂停:

//暂停下载
DownloadManager.pauseDownload(disposable, fileName);

然后取消下载:

//取消下载
DownloadManager.cancelDownload(disposable, fileName);

github链接:Demo

以上。

猜你喜欢

转载自blog.csdn.net/qq_34184412/article/details/80045637