重识OkHttp——更深入了解如何使用

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重识OkHttp——更深入了解如何使用相关的知识,希望对你有一定的参考价值。

参考技术A

OkHttp作为square公司出品的一个网络请求框架,应该算是目前android端最火爆的网络框架了。我公司目前的项目中采用的都是Rxjava结合Retrofit进行网络请求的处理,对于底层真正实现网络请求的OkHttp关注的不是很多。最近探究了一下OkHttp的源码,对OkHttp的使用有了一些新的认识,在此做一下总结。

OkHttp作为当前Android端最火热的网络请求框架,必然有很多的优点。

对于客户端来讲,我们关注的就是把正确的请求发送到服务端并拿到结果来进行处理。在OkHttp中,我认为可以分为3个部分:

OkHttp中通过建造者模式来构建OkHttpClient、Request和Response。对于客户端来讲,我们不需要过多关注Response是如何构建的,因为这个是OkHttp对响应结果进行了封装处理。我们只关注请求Request和客户端OkHttpClient如何构建即可。

Request采用建造者模式来配置url,请求方法method、header、tag和cacheControl。

OkHttp采用POST方法向服务器发送一个请求体,在OkHttp中这个请求体是RequestBody。这个请求体可以是:

RequestBody有几个静态方法用于创建不同类型的请求体:

最终都是相当于重写了RequestBody的两个抽象方法来写入流,如果传递流类型的参数,只要重写这两个抽象方法即可。

例如,我们提交一个String:

提交File:

提交流:

对于提交表单和分块请求,OkHttp提供了两个RequestBody的子类, FormBody MultipartBody

FormBody也是采用建造者模式, 这个很简单,添加key-value形式的键值对即可。
添加键值对有两个方法:

例如:

MultipartBody也是采用建造者模式,MultipartBody.Builder可以构建兼容html文件上传表单的复杂请求体。每一部分的多块请求体都是它自身的请求体,并且可以定义它自己的请求头。如果存在的话,这些请求头用来描述这部分的请求体。例如Content-Disposition、Content-Length 和 Content-Type如果可用就会被自动添加到头。

MIME类型有:

有几个主要的方法:

例如提交一个图片文件:

OkHttpClient采用建造者模式,通过Builder可以配置连接超时时间、读写时间,是否缓存、是否重连,还可以设置各种拦截器interceptor等。
建议在一个App中,OkHttpClient保持一个实例。一个OkHttpClient支持一定数量的并发,请求同一个主机最大并发是5,所有的并发最大是64。这个与OkHttp中的调度器Dispatcher有关,可以设置并发数。本文不对Dispatcher进行讨论。

一个例子:

OkHttpClient支持单独配置,例如原来设置不同的请求时间,可以通过OkHttpClient的newBuilder()方法来重新构造一个OkHttpClient。例如:

上面已经讲了如何创建Request和OkHttpClient,剩下的就是发送请求并得到服务器的响应了。OkHttp发送请求可分为同步和异步。OkHttpClient首先通过Request构建一个Call,通过这个Call去执行同步或者异步请求。

同步方式,调用Call的execute()方法,返回Response,会阻塞当前线程:

异步方式,调用Call的enqueue(CallBack callBack)方法,会在另一个线程中返回结果。

为了缓存响应,需要一个可读写并且设置大小Size的缓存目录。缓存目录需要私有,其它不信任的应用不能访问这个文件。
如果同时有多个缓存访问同一个缓存目录会报错。所以最好只在App中初始化一次OkHttpClient,给这个实例配置缓存,在整个App生命周期内都用这一个缓存。否则几个缓存会相互影响,导致缓存出错,引起程序崩溃。
响应缓存采用Http头来配置,你可以添加这样的请求头 Cache-Control: max-stale=3600 max-age 指的是客户端可以接收生存期不大于指定时间(以 为单位)的响应。
为了防止响应使用缓存,可以用 CacheControl.FORCE_NETWORK 。为了防止使用网络,采用 CacheControl.FORCE_CACHE

调用Call.cancel()方法可以立即取消一个网络请求。如果当前线程正在写request或者读response会报IO异常。如果不再需要网络请求,采用这种方法是比较方便的。例如在App中返回了上一页。无论是同步还是异步的请求都可以被取消。

可以通过Response的code来判断请求是否成功,如果服务器返回的有数据,可以通过Response的body得到一个ResponseBody读取。
如果采用ResponseBody的string()方法会一次性把数据读取到内存中,如果数据超过1MB可能会报内存溢出,所以对于超过1MB的数据,建议采用流的方式去读取,如ResponseBody的byteStream()方法。

需要说明的是:

OkHttp中的很多类都用到了建造者模式,可以根据需要灵活配置。采用建造者模式的有:

如果单独使用OkHttp进行网络请求,通常需要开发者自己再封装一下,如果不想重复造轮子,Github上面的有一些优秀开源库可以拿来使用(本文只列出star较多的几个):

OkHttp官方Wiki文档

你必须学会的okhttp——进阶篇

今天上一篇博客刚好在郭神公众号出现了。也有一个多月没写点什么了。今天就继上一次的okhttp继续深入了解把。在你必须学会的okhttp——入门篇中我简单介绍了okhttp的使用方法。不了解可以看完在回来看这篇文章。
好了。话不多说。这次我主要介绍下okhttp如何实现多文件断点下载。
参考博客:

http://blog.csdn.net/KevinsCSDN/article/details/51934274

之前对如何使用okhttp上传与下载我们已经知道该怎么做了。但是如何实现多文件的操作呢?首先,在这边阐述下我做的过程中所遇到的问题。

  • 如何存储url对应的当前长度以及总长度
  • 如何实现暂停以及续传操作
  • 如何用一个info对象实现多文件的下载
  • response.body.contentlength与实际长度不一样。(例如我获取的长度是5.5M但他的实际长度是6.7M)

如何存储当前长度以及总长度

我在网上看到很多demo对于这块是用SQLite实现,我觉得完全可以Shareperference来存储,通过他的url来存储对应的当前长度和总长度,有人回说Shareperference不是只能一个建对应一个值么,两个怎么解决。我们可以通过MD5加密的url来存储当前进度。通过MD2加密来存储总进度。

如何实现暂停以及续传操作

我们可以通过okhttp自带的拦截器来实现其效果,具体代码如下:

  private Call newCall(long current_length ) {
        Request request = new Request.Builder()
                .url(url)
                .header("RANGE", "bytes=" + current_length + "-")
                .build();
        return client.newCall(request);
    }

    public OkHttpClient getProgressClient() {
        Interceptor interceptor = new Interceptor() {
            public Response intercept(Chain chain) throws IOException {
                Response originalResponse = chain.proceed(chain.request());
                return originalResponse.newBuilder()
                        .body(originalResponse.body())
                        .build();
            }
        };

        return new OkHttpClient.Builder()
                .addNetworkInterceptor(interceptor)
                .build();
    }

如何用一个info对象实现多文件的下载

其实在前面我也说了。用Shareperference来实现,具体怎么说的。你的url,存储路径每次都是需要传的,但是为了防止进度冲突(例:明明的A的进度,下载B的时候却用的A的进度)。所以通过传入的url来用Shareperference得到他存储的当前长度与总长度来解决。

response.body.contentlength与实际长度不一样。

其实。。。我也不知道。。百度了好久。得到的答案是在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。还有种说法还有种说法是服务器限制问题。不解。总之会导致获取进度的时候,进度值是大于100的。。。


大致的问题和解决方法已经说明了。首先,我们先来看下效果图。
这里写图片描述

最后上源码,相信你看懂了上面的思路。对于源码的理解就不是很难了。

public class DownLoadSupport {

    private OkHttpClient okHttpClient;
    private Call call;
    @Bean
    FileSupport fileSupport;
    @Bean
    ByteUtils byteUtils;
    @Bean
    SharePreferencesUtils sharePreferencesUtils;
    private MD5Utils md5Utils;

    public DownLoadSupport() {
        md5Utils = new MD5Utils();
        okHttpClient = getProgressClient();
    }

    public OkHttpClient getProgressClient() {
        Interceptor interceptor = new Interceptor() {
            public Response intercept(Chain chain) throws IOException {
                Response originalResponse = chain.proceed(chain.request());
                return originalResponse.newBuilder()
                        .body(originalResponse.body())
                        .build();
            }
        };
        return new OkHttpClient.Builder().addNetworkInterceptor(interceptor).build();
    }

    private Call newCall(HttpDownloadBean httpDownloadBean) {
        Request request = new Request.Builder().tag(md5Utils.md5(httpDownloadBean.getUrl()))
                .url(httpDownloadBean.getUrl()).addHeader("Accept-Encoding", "identity")
                .header("RANGE", "bytes=" + sharePreferencesUtils.get(md5Utils.md5(httpDownloadBean.getUrl()), (long) 0) + "-")
                .build();
        return okHttpClient.newCall(request);
    }

    public void download(final HttpDownloadBean httpDownloadBean, final DownloadCallBack callBack) {
        if (!sharePreferencesUtils.contains(md5Utils.md5(httpDownloadBean.getUrl()))) {
            sharePreferencesUtils.put(md5Utils.md5(httpDownloadBean.getUrl()), (long) 0);
        }
        if (!sharePreferencesUtils.contains(md5Utils.md2(httpDownloadBean.getUrl()))) {
            sharePreferencesUtils.put(md5Utils.md2(httpDownloadBean.getUrl()), (long) 0);
        }
        call = newCall(httpDownloadBean);
        call.enqueue(new Callback() {
            public void onFailure(Call call, IOException e) {

            }

            public void onResponse(Call call, Response response) throws IOException {
                writeToSDCard(response, httpDownloadBean, callBack);
            }
        });
    }

    public void pause(HttpDownloadBean httpDownloadBean) {
        for (Call call : okHttpClient.dispatcher().queuedCalls()) {
            if (call.request().tag().equals(md5Utils.md5(httpDownloadBean.getUrl())))
                call.cancel();
        }
        for (Call call : okHttpClient.dispatcher().runningCalls()) {
            if (call.request().tag().equals(md5Utils.md5(httpDownloadBean.getUrl())))
                call.cancel();
        }
    }

    private void writeToSDCard(Response response, HttpDownloadBean httpDownloadBean, DownloadCallBack callBack) {
        ResponseBody body = response.body();
        InputStream input = body.byteStream();
        FileChannel channelOut = null;
        // 随机访问文件,可以指定断点续传的起始位置
        RandomAccessFile randomAccessFile = null;
        long current = 0;
        long total = 0;
        current = (long) sharePreferencesUtils.get(md5Utils.md5(httpDownloadBean.getUrl()), (long) 0);
        total = (long) sharePreferencesUtils.get(md5Utils.md2(httpDownloadBean.getUrl()), (long) 0);
        if (total == 0) {
            total = body.contentLength();
            httpDownloadBean.setTotal_length(body.contentLength());
            sharePreferencesUtils.put(md5Utils.md2(httpDownloadBean.getUrl()), httpDownloadBean.getTotal_length());
        }
        try {
            randomAccessFile = new RandomAccessFile(fileSupport.createStorgeFile(httpDownloadBean.getStoragepath(), httpDownloadBean.getFilepath()), "rwd");
            //Chanel NIO中的用法,由于RandomAccessFile没有使用缓存策略,直接使用会使得下载速度变慢,亲测缓存下载3.3秒的文件,用普通的RandomAccessFile需要20多秒。
            channelOut = randomAccessFile.getChannel();
            // 内存映射,直接使用RandomAccessFile,是用其seek方法指定下载的起始位置,使用缓存下载,在这里指定下载位置。
            MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE, current, total);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = input.read(buffer)) != -1) {
                current += len;
                if (callBack != null) {
                    callBack.download(byteUtils.getSize(current) + byteUtils.getByte(current), byteUtils.getSize(total) + byteUtils.getByte(total));
                    callBack.downloadprogress((int) (current * 1.0f / total * 100));
                }
                httpDownloadBean.setCurrent_length(current);
                if (current >= total) {
                    sharePreferencesUtils.remove(md5Utils.md5(httpDownloadBean.getUrl()));
                    sharePreferencesUtils.remove(md5Utils.md2(httpDownloadBean.getUrl()));
                } else {
                    sharePreferencesUtils.put(md5Utils.md5(httpDownloadBean.getUrl()), httpDownloadBean.getCurrent_length());
                }
                mappedBuffer.put(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                input.close();
                if (channelOut != null) {
                    channelOut.close();
                }
                if (randomAccessFile != null) {
                    randomAccessFile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public interface DownloadCallBack {

        void download(String current_progress, String total_progress);

        void downloadprogress(int progress);
    }

}

关于httpdownloadbean:


`
public class HttpDownloadBean {

    private String url = null;
    private String storagepath = null;
    private String filepath = null;
    private long current_length = 0L;
    private long total_length = 0L;

    public long getTotal_length() {
        return total_length;
    }

    public void setTotal_length(long total_length) {
        this.total_length = total_length;
    }

    public long getCurrent_length() {
        return current_length;
    }

    public void setCurrent_length(long current_length) {
        this.current_length = current_length;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getStoragepath() {
        return storagepath;
    }

    public void setStoragepath(String storagepath) {
        this.storagepath = storagepath;
    }

    public String getFilepath() {
        return filepath;
    }

    public void setFilepath(String path) {
        this.filepath = path;
    }
}

就是这几个数据,通过set和get来设置和获取。
ShareperferenceUtils是关于Shareperference的工具类。filesupport是用来创建文件的。byteutils可以不必理会。

有什么问题可以提出来一起讨论。这应该是年前最后一篇技术博文了。我也该为年后找工作的事件而忙碌了。

以上是关于重识OkHttp——更深入了解如何使用的主要内容,如果未能解决你的问题,请参考以下文章

你必须学会的okhttp——进阶篇

深入理解OkHttp源码——网络操作

重识Nginx - 系列导读

RxJava+Retrofit+OkHttp深入浅出-mvp(使用篇)

重识maven

一起了解Android开源框架&&解析okhttp框架任务核心类dispatcher