Android网络请求库 - Say hello to OkHttp

rwkz2260 8年前
   <h2>引言</h2>    <p>网络请求是Android开发中必不可少的一块,通常我们可以用原生的HttpUrlConnection或者Apache封装的HttpClient来完成网络请求的实现。</p>    <p>如今来说,HttpClient在API23问世后也被废弃了。也就是说Google现在更推荐我们使用HttpUrlConnection来实现Http网络请求。</p>    <p>但以Http请求来说,大多数时候都是遵循一定的套路的,所以重复的写关于HttpUrlConnection的相关代码是一件很无趣并且浪费精力的事情。</p>    <p>于是,很多时候我们都会根据自己的实际需求去封装自己的网络请求框架。如果我们自己足够牛逼,那自己动手写框架会是一件很酷的事情。</p>    <p>但如果能力暂时还不够或者不想浪费精力去造轮子,那么当然也有很多现成的关于网络请求的第三方库供我们选择。</p>    <p>现在Android开发中,最常见的和出名的网络请求库大致有三种,分别是:Volley,OkHttp以及Retrofit。那么,我们做何选择呢?</p>    <p>关于这点我们可以参考这篇文章: <a href="/misc/goto?guid=4959677029621691180" rel="nofollow,noindex">Android开源项目推荐之「网络请求哪家强」</a> ,里面介绍了关于以上一种网络请求库的优劣对比。</p>    <h2>初次见面</h2>    <p>好的,接下来我们进入正题。今天我们要走进的是Android网络请求开源库这个系列里的第一个库: <strong>OkHttp</strong> 。</p>    <p>首先,我们可以进入关于 <strong>OkHttp</strong> 该库的官方介绍的网站: <a href="/misc/goto?guid=4958860115824511695" rel="nofollow,noindex">An HTTP & HTTP/2 client for Android and Java applications</a></p>    <p>好的,在开始了解如何使用这个库之前。我们打算先对这个库的概念有一个大致性的了解。我们先看到官方介绍中的Overview:</p>    <p>HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth. HTTP是现代应用进行网络通信的途径。这关乎于我们如何去交换数据&多媒体信息;如何通过HTTP更加有效的提升加载数据的速度以及节省带宽。</p>    <p>OkHttp is an HTTP client that’s efficient by default: OkHttp是一种HTTP的客户端,它默认为我们提供了以下有效的默认行为:</p>    <ul>     <li>HTTP/2 support allows all requests to the same host to share a socket.<br> HTTP 2.0的情况下,支持所有指向相同主机的请求共享同一个套接字</li>     <li>Connection pooling reduces request latency (if HTTP/2 isn’t available).<br> <strong>通过连接池减少请求的延迟(如果是HTTP 2.0则是无效的) </strong></li>     <li>Transparent GZIP shrinks download sizes.<br> 通过GZIP对数据进行压缩,减少数据体积(从而可以节省流量/带宽)</li>     <li>Response caching avoids the network completely for repeat requests.<br> 请求响应进行了缓存,从而避免重复请求(同样是节省流量)</li>    </ul>    <p>OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and for services hosted in redundant data centers. OkHttp initiates new connections with modern TLS features (SNI, ALPN), and falls back to TLS 1.0 if the handshake fails. bala bala….核心意思就是:如果你的服务器配置了多个地址,OkHttp会在第一个地址连接失败的时候,轮流的尝试连接其他备用地址。</p>    <p>Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks. OkHttp的使用很简单,因为其API被设计的很流畅和遵循一定套路。与此同时,同步阻塞和异步回调的方式哥都支持。</p>    <p>OkHttp supports Android 2.3 and above. For Java, the minimum requirement is 1.7. OkHttp支持Android 2.3以上版本;Java则是1.7以上版本。</p>    <h2>准备工作</h2>    <p>因为是第三方库,所以很显然的假设我们想要在自己的项目中使用它。自然第一步的准备工作就是将其捣鼓进我们自己的项目。我们有两种方式:</p>    <ul>     <li> <p>如果在AndroidStudio中进行开发(即gradle作为构建工具),那么因为OkHttp已经被托管在代码仓库jcenter当中,</p> <p>我们要做的就很简单了:仅仅只需要在app目录下的build.gralle文件中进行相关的依赖配置即可:</p> </li>    </ul>    <p>compile ‘com.squareup.okhttp3:okhttp:3.4.1’</p>    <p>compile ‘com.squareup.okio:okio:1.9.0’</p>    <ul>     <li>当然还有另一种熟悉的方式,那就是通过倒入JAR包的方式。这个就没什么值得说的了,只需要下载对应的JAR包倒入就行了。(JAR为okhttp以及其依赖的okio)</li>    </ul>    <h2>基本使用</h2>    <h2>HTTP GET</h2>    <p>显然,Http分为GET和POST两种请求方式,我们先来看比较简单的GET请求在OkHttp中怎么实现。在本地写一段简单的测试代码:</p>    <pre>  <code class="language-java">public class Test {      static String url = "http://localhost:8080/LocalServer/hello.api";        public static void main(String[] args) {          try {              testHttpGet();          } catch (IOException e) {              e.printStackTrace();          }      }        static void testHttpGet() throws IOException {          OkHttpClient client = new OkHttpClient();          Request request = new Request.Builder().url(url).build();          Response response = client.newCall(request).execute();            if(response.isSuccessful()){              String content = response.body().string();              System.out.println(content);          }      }  }</code></pre>    <p>我们通过tomcat在本地搭建了一个简单的servlet服务器,Get请求成功后,返回一段请求成功的文本信息。运行上面的代码会发现的确请求成功了。</p>    <p>可以看到对于OkHttp来说,基本的使用确实还是比较简单的。在上面的测试代码中,我们可以注意到三个比较关键的类:</p>    <ul>     <li>OkHttpClient 封装好的OkHttp客户端</li>     <li>Request 对应于Http的请求体</li>     <li>Response 对应于Http的响应体</li>    </ul>    <p>然后我们来解析一下这段代码发起请求的过程:</p>    <ul>     <li>首先,我们看到如果我们想发起一个请求,首先需要构建一个Request对象,而该对象是通过其辅助类Builder来构建的。<br> 同时因为其本身已经做了一定的封装,所以我们这里只需要通过url()方法传入我们自己的服务器url,并且调用build()方法完成构建就行了。</li>     <li>接着通过OkHttpClient的newCall方法将之前的Request对象传入,真正完成此次请求(Call)的封装。接着调用execute发出请求。</li>     <li>execute方法将返回一个Response对象,如它的命名一样,这个对象封装了此次请求响应的各种信息。</li>    </ul>    <h2>HTTP POST</h2>    <p>看完了GET的基本使用,我们接着就来看POST的基本使用是如何的。官方介绍中的Example是这样的:</p>    <pre>  <code class="language-java">public static final MediaType JSON          = MediaType.parse("application/json; charset=utf-8");        OkHttpClient client = new OkHttpClient();        String post(String url, String json) throws IOException {        RequestBody body = RequestBody.create(JSON, json);        Request request = new Request.Builder()            .url(url)            .post(body)            .build();        Response response = client.newCall(request).execute();        return response.body().string();      }</code></pre>    <p>我们可以看到,实际上上面的代码与之前使用GET时的代码是很相似的。不同点在于:</p>    <p>这里在通过Builder构建Request对象时,多调用了一个方法post()。而该方法接受的参数类型是一个叫做RequestBody的东西。</p>    <p>从命名就可以看出来这是对于请求体的一个封装。因为对于POST请求来说,通常我们都会在请求体中传入一些我们提交给服务器的信息。</p>    <p>public static final MediaType JSON=MediaType.parse(“application/json; charset=utf-8”);这行代码十分重要。</p>    <p>它做的工作实际是在设置请求的 <strong>Content-Type</strong> 。(更多的ContentType信息可以参考: <a href="/misc/goto?guid=4959677029735500473" rel="nofollow,noindex">常用Http Content-Type对照表</a> )</p>    <p>接着,我们通过源码看一看为什么post方法能够声明此次请求方法是POST。它在底层是怎么样与GET进行区分的呢?打开post的方法的源码:</p>    <pre>  <code class="language-java">public Request.Builder post(RequestBody body) {              return this.method("POST", body);          }            public Request.Builder method(String method, RequestBody body) {              // ......              } else {                  this.method = method;                  this.body = body;                  return this;              }          }</code></pre>    <p>看上去十分简洁易懂,那么为什么我们没有调用post方法的的时候,就代表是GET请求呢?打开builder默认的无参构造器就得到了答案:</p>    <pre>  <code class="language-java">public Builder() {              this.method = "GET";              this.headers = new okhttp3.Headers.Builder();          }</code></pre>    <h2>其它招式</h2>    <p>到了这里我们已经知道了对于OkHttp这个开源库来说,最基本的GET与POST请求的使用方式。但显然这还是不够,我们接着来看一些其它的常用的方式。</p>    <h2>不同的响应数据类型</h2>    <p>我们之前在HTTP GET的用例当中,通过response.body().string();的方式很容易的获取到了响应信息。但显然这里是指纯文本信息。</p>    <p>一个成熟的框架的封装度肯定远不如此,OkHttp还提供了另外几个常见的方法分别用于以byte数组、字节流与字符流的方式读取响应信息。它们分别是:</p>    <ul>     <li>bytes() // 返回类型为byte[]</li>     <li>byteStream() //返回类型为InputStream</li>     <li>charStream() //返回类型为Reader</li>    </ul>    <p>现在我们以一个简单的例子来加深这种印象。假设我们现在的需求是通过url去下载一张网络图片到本地,那么显然这个时候就需要以字节流的形式去读取数据:</p>    <pre>  <code class="language-java">if (response.isSuccessful()) {              InputStream in = response.body().byteStream();              FileOutputStream fos = new FileOutputStream(new File(                      "android.jpg"));                int length;              byte[] buf = new byte[1024];                while ((length = in.read(buf)) != -1) {                  System.out.println(length);                  fos.write(buf, 0, length);              }          }</code></pre>    <h2>添加请求头</h2>    <p>显然我们在实际开发时,很多时候还需要在请求头中去设置一些相关的信息。在OkHttp当中这个操作也容易实现:</p>    <pre>  <code class="language-java">Request request = new Request.Builder()                           .url(url)                           .header("headerKey1", "headerValue1")    // 设置请求头                           .addHeader("headerKey2", "headerValue2") // 追加请求头                           .build();</code></pre>    <h2>获取响应头</h2>    <p>同理来说,当然我们也可以从响应体中提取头信息。实现的方式同样很简单:</p>    <pre>  <code class="language-java">System.out.println(response.header("responseHeader"));              System.out.println(response.header("responseHeader", "默认值"));                Headers headers = response.headers();              for (int i = 0 ;i < headers.size();i++) {                  String name = headers.name(i);                  System.out.println(response.header(name));              }</code></pre>    <h2>以其它形式提交数据</h2>    <p>我们之前在说到POST的示例代码中,它示范了怎么样提交JSON形式的数据。但实际开发中,显然有很多其它类型的数据让我们POST。</p>    <p>举例来说,很多时候我们都会以表单形式去提交键值对上传到服务器,OkHttp也考虑到了这点,并做好了封装。</p>    <p>之前我们可以通过FormEncodingBuilder来实现,而OkHttp3之后这个类则已经被FormBody所代替了。</p>    <pre>  <code class="language-java">RequestBody formBody = new FormBody.Builder()          .add("platform", "android")          .add("name", "bug")          .add("subject", "XXXXXXXXXXXXXXX")          .build();            Request request = new Request.Builder().url(url).post(formBody).build();</code></pre>    <p>我们注意到这与之前上传JSON的示例当中,改变了的就仅仅是RequestBody的构建形式。抱着一探究竟的心态打开FormBody的源码,首先就会看到:</p>    <pre>  <code class="language-java">private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");</code></pre>    <p>我们恍然大悟,原来所谓的FormBody其实也就仅仅是OkHttp为我们做的一层封装而已。关键还是在于对于Content-Type的设置。</p>    <p>而一通则百通,了解了这个原理,对于其他更多数据类型的POST的使用,我们也就有底了。</p>    <h2>响应缓存</h2>    <p>在有些情况下,我们没有必要每次都去发起一次全新的请求去获取数据。可以通过设置缓存的方式来达到节约开销的目的。OkHttp当然也支持这样的操作。</p>    <pre>  <code class="language-java">// 设置缓存目录与缓存大小上限          Cache cache = new Cache(new File("D:\\cache"), 1024 * 1000);          // okhttp3不再以setCache设置缓存而是通过OkHttpClient.Builder来设置了          OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();          clientBuilder.cache(cache);          // 构建客户端对象          OkHttpClient client = clientBuilder.build();</code></pre>    <p>由此我们发现在OkHttp中使用缓存还是很简单的,关键需要注意一点:那就是我们发现这里的cache是与OkHttpClient对象关联的。也就是说:</p>    <p>我们最好在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,且可能导致程序崩溃。</p>    <p>同时,响应缓存使用HTTP头作为配置。对于客户端来说,我们可以在请求头中添加“Cache-Control: max-stale=3600”,OkHttp缓存会支持。而对于服务来说,则是通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=3600。</p>    <p>我们在这里可以简单的总结一下这两个东西,因为起初我一直对它们都感到有点迷惑:</p>    <ul>     <li>服务器在响应头中设置max-age的意义在于,将此次请求的响应进行缓存指定的秒数。当超出这个指定时间之后,这个缓存就会失效,那么再次获取数据就需要发起新的网络请求了。</li>     <li>客户端在请求头中设置max-stale,这个东西更有趣。起初一直没搞明白它究竟有什么用,直到看到Google在Android Developers一段相关的解析。</li>    </ul>    <p>This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the max-stale directive with the maximum staleness in seconds:</p>    <p>这段解释的关键在于: <strong>一个陈旧的响应总好过没有响应</strong> 。 一下就恍然大悟了,我们之前说缓存一旦超过max-age就会失效,需要获取新的响应。</p>    <p>但是也可能出现虽然缓存失效了,却新的请求因为某种原因失败获取不到响应,那么这时我们仍然从失效的缓存中获取响应总是要好过没有响应的。</p>    <p>而max-stale的意义就在于指定允许从失效多久的缓存中读取响应。例如我们将其设置为3600秒,即代表是否读取缓存取决于缓存是否已经失效超过1小时。</p>    <p>(另外,从代码注释中也可以看到,对于缓存的设置已经放在了OkHttpClient.Builder中。同理,对于其它的一些请求属性设置(如超时等)都已经转移到了这个类当中。)</p>    <p>做好了准备工作。我们就可以通过代码来判断是从缓存还是网络中读取响应了:</p>    <pre>  <code class="language-java">System.out.println("response:" + response);              System.out.println("cache response:"+ response.cacheResponse());              System.out.println("network response:"+ response.networkResponse());</code></pre>    <h2>取消请求</h2>    <p>有的时候,因为一些原因,我们也会想要去取消或者说中断一个请求。OkHttp当然也支持这种功能。</p>    <pre>  <code class="language-java">Call call = client.newCall(request);          call.enqueue(new Callback() {                @Override              public void onResponse(Call call, Response response) throws IOException {                  System.out.println("请求成功");                  call.cancel(); //可以在这里取消              }                @Override              public void onFailure(Call call, IOException e) {                  System.out.println("请求失败");                }          });          call.cancel(); // 可以在这里取消          client.dispatcher().cancelAll(); // 可以取消全部请求</code></pre>    <p>并且,同样的,如果取消一个正处于连接状态下的请求,是会抛出IO异常的。</p>    <h2>异步请求</h2>    <p>之前我们测试的代码当中,通过execute方法来执行请求,其内部都是以同步的形式实现的。而如果我们希望异步执行请求的话,则应该通过enqueue来实现:</p>    <pre>  <code class="language-java">client.newCall(request).enqueue(null);</code></pre>    <p>我们发现由同步执行改为异步执行很简单,只需要由execute方法改为调用enqueue就行了。但需要明白的一点是:</p>    <p>这里所谓的同步与异步并不是指多个请求的串行或者并行执行的区别。而是指线程是否因为此次请求而堵塞。</p>    <p>通过一段简单的代码,我们能够更加详细的理解这个概念:</p>    <pre>  <code class="language-java">Response response = client.newCall(request).execute();          System.out.println("lalalala");</code></pre>    <p>当我们采用同步的方式执行请求时,线程会进入堵塞,也就是说当此次请求完全执行完毕之前,之后的”lalalala”的打印语句都是无法执行并输出的。</p>    <p>而假设我们先将其该为用异步的方式执行请求,那么我们会发现之后的打印语句是不会被阻塞而无法执行的。</p>    <p>与此同时,我们看到enqueue方法接收一个Callback类型的参数,这个其实没什么好说的,一旦涉及异步,通常都离不开回调。这同样也是一个回调接口:</p>    <pre>  <code class="language-java">client.newCall(request).enqueue(new Callback() {                @Override              public void onResponse(Call arg0, Response arg1) throws IOException {                  System.out.println("请求成功");              }                @Override              public void onFailure(Call arg0, IOException arg1) {                  System.out.println("请求失败");                }          });</code></pre>    <h2>总结</h2>    <p>OK,到了这里,实际上当我们掌握了以上说到的点,对于日常的使用基本上是已经足够了。</p>    <p>更多的一些功能或者进阶的使用技巧,我们可以查看官方的资料或者在自己的使用工作逐渐摸索和总结。</p>    <p>总的来说,OkHttp是一个十分强大的网络请求库。但与此同时我们可以发现如果直接使用的话,在实际使用中还是会写到很多重复的代码的。</p>    <p>这也就是为什么很多人都建议能力足够的话,可以根据自己的需求来对OkHttp进行一次二次封装,从而让使用更加简洁。</p>    <p>总的来说就总结到这里吧,毕竟实践才能出真知。掌握了基本的知识点后,只有在实际的使用中不断碰坑,才能越发熟练的掌握一个东西。</p>    <p> </p>    <p>来自:http://blog.csdn.net/ghost_programmer/article/details/52253359</p>    <p> </p>