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

本文的分析基于OkHttp3.4,不展示完整的代码示例,具体可以查看这个官方例子或者项目中的samples

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

1 OkHttp的优点

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

  • 支持HTTP/2 协议,允许连接到同一个主机地址的所有请求共享Socket。这必然会提高请求效率。
  • 在HTTP/2协议不可用的情况下,通过连接池减少请求的延迟。
  • GZip透明压缩减少传输的数据包大小。
  • 响应缓存,避免同一个重复的网络请求。

2 网络处理3要素

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

  • Request类封装客户端发送的请求,包括请求的url,请求方法method(主要是GET和POST方法)、请求头header以及请求体requestBody;
  • Response类封装了服务器响应的数据,包括code、message、body、header等。
  • OkHttpClient负责发送请求Request并通过同步或者异步的方式返回服务器的响应Response,就好比是一个浏览器。

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

2.1 请求Request

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

  • 设置url。可以是String类型、URL类型和HttpUrl类型。最终都是用到HttpUrl类型。
  • 设置method,包含get、post方法等。默认的是get方法。post方法要传RequestBody,类似的还有delete、put、patch。
  • 设置header,方法有addHeader(String name, String value)、 removeHeader(String name)、header(String name, String value)、headers(Headers headers)。headers(Headers headers)调用之后其它的header都会被移除,只添加这一个header。而header(String name, String value)方法调用之后,其它与这个name同名的header都会被移除,只保留这一个header。
  • 设置tag,设置tag可以用来取消这一请求。如果未指定tag或者tag为null,那么这个request本身就会当做是一个tag用来被取消请求。
  • 设置cacheControl,这个是设置到请求头中。用来替换其它name是"Cache-Control"的header。如果cacheControl是空的话就会移除请求头中name是"Cache-Control"的header。
Request.png

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

  • String类型
  • Stream流类型
  • File文件类型
  • Form表单形式的key-value类型
  • 类似Html文件上传表单的复杂请求体类型(多块请求)。

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

//创建String类型的请求体
public static RequestBody create(MediaType contentType, String content)

//创建文件类型的请求体
 public static RequestBody create(final MediaType contentType, final File file) 

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

  //对应的是name为Content-Type的header
  public abstract MediaType contentType();

  //这个BufferedSink位于Okio包下,提供高效的写入。
  public abstract void writeTo(BufferedSink sink) throws IOException;

  //在写入的时候可以传递内容的大小,如果不知道就返回-1即可。
  public long contentLength() throws IOException {  return -1;}

例如,我们提交一个String:

String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

提交File:

File file = new File("README.md");

Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

提交流:

RequestBody requestBody = new RequestBody() {
       @Override
        public MediaType contentType() {
             return MEDIA_TYPE_MARKDOWN;
        }

        @Override
         public void writeTo(BufferedSink sink) throws IOException {
              sink.writeUtf8("Numbers\n");
              sink.writeUtf8("-------\n");
              for (int i = 2; i <= 997; i++) {
                     sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
               }
           }

          private String factor(int n) {
                for (int i = 2; i < n; i++) {
                     int x = n / i;
                     if (x * i == n) return factor(x) + " × " + i;
                 }
                return Integer.toString(n);
                }
};

Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

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

2.1.1 表单FormBody

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

//采用OkHttp默认的编码
public Builder add(String name, String value) 

//采用用户要求的编码
public Builder addEncoded(String name, String value)

例如:

  RequestBody formBody = new FormBody.Builder()
                        .add("search", "Jurassic Park")
                        .build();
  Request request = new Request.Builder()
                        .url("https://en.wikipedia.org/w/index.php")
                        .post(formBody)
                        .build();

2.1.2 分块MultipartBody

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

MIME类型有:

  public static final MediaType MIXED = MediaType.parse("multipart/mixed");

  public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative");

  public static final MediaType DIGEST = MediaType.parse("multipart/digest");

  public static final MediaType PARALLEL = MediaType.parse("multipart/parallel");

有几个主要的方法:


  //设置MIME类型,如MIXED(默认的)
    public Builder setType(MediaType type) {}

  //添加请求体
    public Builder addPart(RequestBody body) {
      return addPart(Part.create(body));
    }

  //添加包含header的请求体
    public Builder addPart(Headers headers, RequestBody body) {
      return addPart(Part.create(headers, body));
    }

    //请求体添加表单
    public Builder addFormDataPart(String name, String value) {
      return addPart(Part.createFormData(name, value));
    }

    //请求体中包含文件
    public Builder addFormDataPart(String name, String filename, RequestBody body) {
      return addPart(Part.createFormData(name, filename, body));
    }

    //添加自己定义的part
    public Builder addPart(Part part) {
      if (part == null) throw new NullPointerException("part == null");
      parts.add(part);
      return this;
    }

例如提交一个图片文件:

RequestBody requestBody = new MultipartBody.Builder()
         .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
         RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
        .build();

 Request request = new Request.Builder()
         .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
         .url("https://api.imgur.com/3/image")
         .post(requestBody)
         .build();

2.2 客户端OkHttpClient

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

OkHttpClient okHttpClient=new OkHttpClient.Builder().build();

//如果不需要我们额外配置,可以使用默认的配置
OkHttpClient okHttpClient1 = new OkHttpClient();

一个例子:

int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheDirectory = new File(getCacheDir(), "OkHttpCache");
Cache cache = new Cache(cacheDirectory, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(60, TimeUnit.SECONDS)//连接超时时间
        .readTimeout(60, TimeUnit.SECONDS)//读的时间
        .writeTimeout(60, TimeUnit.SECONDS)//写的时间
        .cache(cache)//配置缓存
        .build();

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

OkHttpClient client = new OkHttpClient();

//读的时间设置为500ms
OkHttpClient copy = client.newBuilder()
                          .readTimeout(500, TimeUnit.MILLISECONDS)
                          .build();

//读的时间设置为3000ms
OkHttpClient copy = client.newBuilder()
                          .readTimeout(3000, TimeUnit.MILLISECONDS)
                          .build();

3 同步请求和异步请求

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

#OkHttpClient
public Call newCall(Request request)

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

response = client.newCall(request).execute();

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

client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
              //处理错误的回调
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
              //处理正确的回调
            }
        });

4 其他

4.1 配置响应缓存

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

注意:如果使用FORCE_CACHE禁止使用网络,而响应又没有缓存存在,OkHttp会报**504 Unsatisfiable Request **响应错误。

4.2 取消请求

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

4.3 Response读取响应结果

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

需要说明的是:

  • 如果ResponseBody的内容不读取的话,不会触发IO流的读取操作
  • 内容读取之后,这个body需要关闭。

5 总结

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

  • OkHttpClient.Builder
  • Request.Builder
  • FormBody.Builder
  • MultipartBody.Builder
  • Response.Builder

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

参考

OkHttp官方Wiki文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容