Skip to content

Instantly share code, notes, and snippets.

@SmartDengg
Last active October 20, 2016 06:33
Show Gist options
  • Save SmartDengg/dafddcc5e990a62a96a0f077860d4340 to your computer and use it in GitHub Desktop.
Save SmartDengg/dafddcc5e990a62a96a0f077860d4340 to your computer and use it in GitHub Desktop.
NetWorkingPractice
**Android生态圈发展步伐之快,网络框架更是层出不穷,本次主讲人将带你回顾网络框架的变迁史,并对比时下流行网络框架,阐述他的经验,以及这些演变背后的原因。**
Android开发生态圈的节奏非常之快。每周都会有新的工具诞生,类库的更新,博客的发表以及技术探讨。
如果你外出度假,当你回来的时候可能已经发布了新版本的
网络框架不同于其他,因为它在一定程度上与业务耦合度极高,甚至从代码的角度来说会遍布于工程的各个角落,很难集中管理,一旦替换甚至还要经过漫长的调试过程。
不同与图像加载,日志打印等框架那样,在短时间内就能够进行重构,而且一直沿用多个迭代版本也是正常的。
这一次我会跟大家一起回顾Android开发中那些常见的网络加载类,还有库。
阐述我的观点,分享我的经验,希望大家能够喜欢。
@SmartDengg
Copy link
Author

SmartDengg commented Oct 16, 2016

HTTP, GET VS POST

HTTP(超文本传输协议),设计目的是保证客户端与服务器之间的进行通信。
HTTP 的工作方式是客户端与服务器之间的请求-应答协议。

总的来说,客户端(浏览器)向服务器提交 HTTP 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。

  • GET - 从指定的资源请求数据。

  • POST - 向指定的资源提交要被处理的数据(请求体)。

    GET POST
    缓存 请求会被browser主动缓存 不会被browser主动缓存,除非手动配置
    编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded , multipart/form-data
    对数据长度的限制 直接向 URL 添加数据,长度受限(limit:2048) 无限制
    对数据类型的限制 只允许 ASCII 字符 没有限制
    安全性 较差,发送的数据是URL的一部分 只能说安全性比GTE好,数据不会暴露在URL中
    可见性 数据在 URL 中对所有人都是可见的 数据不会显示在 URL 中,而是在request body(请求体)中

除了GET和POST外,还有其他几种HTTP请求方式

方法 描述
HEAD 与 GET 相同,但只返回 HTTP 报头,不返回文档主体。
PUT 上传指定的 URI 表示。
DELETE 删除指定资源。
OPTIONS 返回服务器支持的 HTTP 方法。
CONNECT 把请求连接转换到透明的 TCP/IP 通道。

URLConnection

URLConnection是所有代表应用与URL建立连接进行通信的父类。这个类的实例,可以读取和写入之前指定的URL所引用的资源。

HttpURLConnection

使用这个类的基本模式如下

  1. 通过调用URL.openConnection()获得HttpURLConnection实例,注意类型转换
  2. 准备请求。最重要的属性就是URI地址,在请求头中添加请求所必须的参数
  3. 上传额外的请求体。如果请求包含一个请求体,必须设置setDoOutput(true),传输的数据写入通过调用getOutputStream()返回的stream中。
  4. 读取响应。响应头中通常包含一些重要的数据,如响应体的长度,session或者cookies等重要信息。可以通过getInputStream()来获取响应体数据流,如果返回一个空的流,则代表没有响应体。
  5. 断开连接。一旦读完响应体,HttpURLConnection应该通过调用disconnect()来关闭。释放资源,交回连接池中以便被再次复用。

先看一个最简单的例子,比如说要访问这个网站http://www.android.com/

   URL url = new URL("http://www.android.com/");
   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     readStream(in);
   } finally {
     urlConnection.disconnect();
   }

Response Handling

如果HTTP响应表明它发生了一个错误,getInputStream()将会抛出一个IOException,这个时候应该使用getErrorStream()来读取错误的响应。可以通过getHeaderFields()来获读取所有响应头信息,当然他还有很多重载方法,可以用来获取指定的响应头。

  • getContentEncoding:获取 content-encoding 响应头字段的值。
  • getContentLength:获取 content-length 响应头字段的值。
  • getContentType:获取 content-type 响应头字段的值。
  • getDate():获取date响应头字段的值。
  • getExpiration():获取expires响应头字段的值。
  • getLastModified():获取 last-modified 响应头字段的值。

Posting Content

想服务器上传数据之前,应该设置setDoOutput(true)。为了提高效率,如果数据的大小是已知的,应该setFixedLengthStreamingMode(int),如果是未知的应该调用setChunkedStreamingMode(int),值得一提的是在早期的Android版本中,前者被限定在2GB内。否则HttpURLConnection在传输前会将整个请求体读进内存,不仅浪费了堆内存,甚至可能引起崩溃,这显然是应该避免的。

比如下面这个例子,我们不清楚上传数据的大小

   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     urlConnection.setDoOutput(true);
     //"0"代表使用默认的分块长度“1024” 
     urlConnection.setChunkedStreamingMode(0);

     OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
     writeStream(out);

     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     readStream(in);
   } finally {
     urlConnection.disconnect();
   }

HTTP Methods

HttpURLConnection默认使用GET请求,如果设置了setDoOutput(true),那么请求将变成POST,或者手动更改请求方式setRequestMethod("POST"),其他类型的请求(OPTIONS, HEAD, PUT, DELETE and TRACE)则必须通过setRequestMethod(String)来设置

Performance

The input and output streams returned by this class are not buffered. Most callers should wrap the returned streams with BufferedInputStream or BufferedOutputStream. Callers that do only bulk reads or writes may omit buffering.
When transferring large amounts of data to or from a server, use streams to limit how much data is in memory at once. Unless you need the entire body to be in memory at once, process it as a stream (rather than storing the complete body as a single byte array or string).

To reduce latency, this class may reuse the same underlying Socket for multiple request/response pairs. As a result, HTTP connections may be held open longer than necessary. Calls to disconnect() may return the socket to a pool of connected sockets. This behavior can be disabled by setting the http.keepAlive system property to false before issuing any HTTP requests. The http.maxConnections property may be used to control how many idle connections to each server will be held.

By default, this implementation of HttpURLConnection requests that servers use gzip compression and it automatically decompresses the data for callers of getInputStream(). The Content-Encoding and Content-Length response headers are cleared in this case. Gzip compression can be disabled by setting the acceptable encodings in the request header:

   urlConnection.setRequestProperty("Accept-Encoding", "identity");

Setting the Accept-Encoding request header explicitly disables automatic decompression and leaves the response headers intact; callers must handle decompression as needed, according to the Content-Encoding header of the response.

getContentLength() returns the number of bytes transmitted and cannot be used to predict how many bytes can be read from getInputStream() for compressed streams. Instead, read that stream until it is exhausted, i.e. when read() returns -1.

但是如果启动了响应压缩的功能,HTTP响应头里的Content-Length就会代表着压缩后的长度,这时再使用getContentLength()方法来取出解压后的数据就是错误的了。正确的做法应该是一直调用InputStream.read()方法来读取响应数据,一直到出现-1为止。

HTTP Authentication

HttpURLConnection支持基本HTTP验证。

   Authenticator.setDefault(new Authenticator() {
     protected PasswordAuthentication getPasswordAuthentication() {
       return new PasswordAuthentication(username, password.toCharArray());
     }
   });

尤其要指出的是,username,password,请求体,响应体,在网络传输过程不经过任何的加密,一定涵义上,这种验证方式并不属于安全机制,因此它至少应该和HTTPS搭配使用才能发挥作用。

Proxies

默认情况下,这个类的连接将会直接连接到原始服务器。可以通过HTTP或者SOCKS代理,来更改连接方式。只需要在创建连接的时候使用重载函数URL.openConnection(Proxy),传入指定的Proxy即可。

IPv6 Support

支持IPV6的数据传输,如果一个主机既有IPV4又有IPV6,那么它会试图链接每一个地址,直到有一个地址建立了连接。

Response Caching

Android 4.0 (Ice Cream Sandwich, API level 15) 加入了响应缓存机制。感兴趣的话可以通过深入android.net.http.HttpResponseCache
来了解具体的实现细节,来为你的应用开启HTTP缓存。

Avoiding Bugs In Earlier Releases

从性能的角度考虑,为每一次请求都创建一个新的连接的代价是非常大的,而且也会让请求的过程变慢,因此HttpURLConnection使用了连接池的概念(connection pool),当新的请求来到的时候,会服用之前已经被关闭的连接,而且Android默认为所有连接都设置了KeepAlive = true

在早期的Android版本中 Android 2.2 (Froyo) ,HttpURLConnection有一个烦人的bug,当在一个可读的InputStream上调用close(),会污染连接池。这句话很多人都听过,但是什么时候会出现这种隐患呢。我来举例说明,当我们试图关闭一个没有读完的,数据量很大的inputstream时,如果这个流被连接池回收了,那么下一次仍然会读到上一次所没有读完的数据。

为了解决这个bug,有些变通的方法,比如官方建议的,禁用应用中的所有连接的“http.keepAlive”。

   private void disableConnectionReuseIfNecessary() {
   // Work around pre-Froyo bugs in HTTP connection reuse.
   if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
     System.setProperty("http.keepAlive", "false");
   }
 }

或者在发起请求前设置conn.setRequestProperty("connection", "close");如果在请求前忘记设置这个属性,依然会给bug留可趁之机。

使用这些办法,所有的连接都不会被复用了,每次请求都会创建新的连接,因此样做的后果是会让连接的过程变慢,对性能造成损耗。很难两全。

应该为每一个HttpURLConnection实例,使用一对request/response,另外需要注意的是,这个类不是线程安全的。

GZIP

在Android 2.3版本的时候,加入了更加透明化的响应压缩。Accept-Encoding不存在的情况下,HttpURLConnection会自动在每个发出的请求中加入消息头Accept-Encoding: gzip,并处理相应的返回结果。

权限

<!-- 在SD卡中创建与删除文件权限 -->  
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  
<!-- 向SD卡写入数据权限 -->  
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  
<!-- 授权访问网络 -->  
<uses-permission android:name="android.permission.INTERNET"/>  

GET

  public String get(String urlPath) {
    HttpURLConnection connection = null;
    InputStream is = null;
    try {
      //获得URL对实例
      URL url = new URL(urlPath);
      //获得HttpURLConnection实例
      connection = (HttpURLConnection) url.openConnection();
      // 默认为GET
      connection.setRequestMethod("GET");
      //不使用缓存
      connection.setUseCaches(false);
      //设置超时时间
      connection.setConnectTimeout(10000);
      //设置读取超时时间
      connection.setReadTimeout(10000);
      //设置是否从httpUrlConnection读入,默认情况下是true;
      connection.setDoInput(true);
      if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {//相应码是否为200
        //获得输入流
        is = new BufferedInputStream(connection.getInputStream());
        //包装字节流为字符流
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
          response.append(line);
        }
        return response.toString();
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (connection != null) {
        connection.disconnect();
        connection = null;
      }
      if (is != null) {
        try {
          is.close();
          is = null;
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    return null;
  }

POST

private String post(String urlPath, Map<String, String> params) {
    if (params == null || params.isEmpty()) {
      return get(urlPath);
    }
    OutputStream os = null;
    InputStream is = null;
    HttpURLConnection connection = null;
    StringBuffer body = getParamString(params);
    byte[] data = body.toString().getBytes();
    try {
      //获得URL实例
      URL url = new URL(urlPath);
      //获得HttpURLConnection实例
      connection = (HttpURLConnection) url.openConnection();
      // 设置请求方法为post
      connection.setRequestMethod("POST");
      //不使用缓存
      connection.setUseCaches(false);
      //设置超时时间
      connection.setConnectTimeout(10000);
      //设置读取超时时间
      connection.setReadTimeout(10000);
      //设置是否从httpUrlConnection读入,默认情况下是true;
      connection.setDoInput(true);
      //设置为true后才能写入参数
      connection.setDoOutput(true);
      connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
      connection.setRequestProperty("Content-Length", String.valueOf(data.length));
      os = connection.getOutputStream();
      //写入参数
      os.write(data);
      if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {//相应码是否为200
        //获得输入流
        is = new BufferedInputStream(connection.getInputStream());
        //包装字节流为字符流
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
          response.append(line);
        }
        return response.toString();
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      //关闭
      if (os != null) {
        try {
          os.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
      if (connection != null) {
        connection.disconnect();
        connection = null;
      }
    }
    return null;
  }

  private StringBuffer getParamString(Map<String, String> params) {
    StringBuffer result = new StringBuffer();
    Iterator<Map.Entry<String, String>> iterator = params.entrySet().iterator();
    while (iterator.hasNext()) {
      Map.Entry<String, String> param = iterator.next();
      String key = param.getKey();
      String value = param.getValue();
      result.append(key).append('=').append(value);
      if (iterator.hasNext()) {
        result.append('&');
      }
    }
    return result;
  }

注意事项:

  • HttpURLConnection的connect()函数,实际上只是建立了一个与服务器的tcp连接,并没有实际发送http请求。
  • 无论是post还是get,http请求实际上直到调用HttpURLConnection的getInputStream()后才正式发送出去。
  • 对HttpURLConnection对象的一切配置都必须要在connect()函数执行之前完成。而对outputStream的写操作,又必须要在inputStream的读操作之前。这些顺序实际上是由HTTP请求的格式决定的。

@SmartDengg
Copy link
Author

SmartDengg commented Oct 16, 2016

HttpClient

HttpClient是Apache开源组织提供的一个Http客户端,HttpClient封装了Session、Cookie等细节问题的处理。
简单来说,HttpClient就是一个增强版的HttpURLConnection ,HttpURLConnection可以做的事情HttpClient全部可以做;HttpURLConnection没有提供的有些功能,HttpClient也提供了,但它只是关注于如何发送请求、接收响应,以及管理HTTP连接。

高效稳定,但是维护成本高昂,故android 开发团队不愿意在维护该库而是转投更为轻便的HttpUrlConnection用法

  1. HttpClient是一个接口,因此无法直接创建它的实例,一般都是创建一个DefaultHttpClient实例
  2. 如果要发起Get请求,需要创建一个HttpGet对象,并传入请求地址
  3. 如果要发起Post请求,需要创建一个HttpPost对象。并传入请求地址,通过setEntity函数设置请求参数
  4. 调用execute方法,传入HttpGet或者HttpPost实例,执行后返回HttpResponse对象,判断响应状态码
    解析响应结果,通过调用getEntity函数获得一个HttpEntity对象,之后可以通过EntityUtils.toString方法将其转换为字符串。

自定义配置HttpClient

    //创建HttpClient
    private HttpClient createHttpClient() {
        HttpParams defaultHttpParams = new BasicHttpParams();
        //设置连接超时
        HttpConnectionParams.setConnectionTimeout(defaultHttpParams, 15000);
        //设置请求超时
        HttpConnectionParams.setSoTimeout(defaultHttpParams, 15000);
        HttpConnectionParams.setTcpNoDelay(defaultHttpParams, true);
        HttpProtocolParams.setVersion(defaultHttpParams, HttpVersion.HTTP_1_1);
        HttpProtocolParams.setContentCharset(defaultHttpParams, HTTP.UTF_8);
        //持续握手
        HttpProtocolParams.setUseExpectContinue(defaultHttpParams, true);
        HttpClient httpClient = new DefaultHttpClient(defaultHttpParams);
        return httpClient;
    }

GET

  private String get(String url) {
    HttpClient client = null;
    HttpGet request = null;
    try {
      client = new DefaultHttpClient();
      request = new HttpGet(url);
      HttpResponse response = client.execute(request);
      if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
        String result = EntityUtils.toString(response.getEntity(), "UTF-8");
        return result;
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    return null;
  }

POST

  private String post(String url, List<NameValuePair> params) {
    HttpClient client = null;
    HttpPost request = null;
    try {
      client = new DefaultHttpClient();
      request = new HttpPost(url);
      request.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
      HttpResponse response = client.execute(request);
      if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
        String result = EntityUtils.toString(response.getEntity(), "UTF-8");
        return result;
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    return null;
  }

读取相应流

private String converStreamToString(InputStream is) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuffer sb = new StringBuffer();
        String line = null;
        while ((line = reader.readLine()) != null) {
            sb.append(line + "\n");
        }
        String respose = sb.toString();
        return respose;
    }

@SmartDengg
Copy link
Author

SmartDengg commented Oct 17, 2016

HttpUrlConnection VS HttpClient

HttpURLConnection和HttpClient 都支持HTTPS协议、IPv6、以流的形式进行上传和下载、配置超时时间、以及连接池等功能。

Most network-connected Android apps will use HTTP to send and receive data. Android includes two HTTP clients: HttpURLConnection and Apache HTTP Client. Both support HTTPS, streaming uploads and downloads, configurable timeouts, IPv6 and connection pooling.

But the large size of this API makes it difficult for us to improve it without breaking compatibility. The Android team is not actively working on Apache HTTP Client.

不过HttpURLConnection和HttpClient的用法还是稍微有些复杂的,如果不进行适当封装的话,很容易就会写出不少重复代码,甚至错误代码。

Bug

HttpClient: 高效稳定,无明显bug,至少在我使用的时候是这样的。

HttpUrlConnection: 在早期的2.2版本(Froyo)之前,不包括2.2,有污染连接池的风险,这个bug再后来被修复。如果在低版本中使用,应该通过调用System.setProperty("http.keepAlive", "false");来避免这个bug的发生。

GIZP

HttpUrlConnection: Android 2.3 之后,添加了透明的响应压缩,如果请求头信息中不包含Accept-Encoding字段,HttpURLConnection自动添加Accept-Encoding: gzip并负责对应的响应解压缩,如果之前的请求中存在,则不做任何操作。

HttpClinet: 默认不带gzip压缩,需要手动配置,并对response解压缩。

Response Caching

HttpUrlConnection: Android 4.0(Ice Cream Sandwich, API level 15)开始加入了响应缓存,可以对响应缓存相关类android.net.http.HttpResponseCache进行深入了解,了解使用方式和缓存背后的秘密,从而降低不必要的网络请求,省电,省流量,提高用户体验。

HttpClinet: 手动配置响应缓存,样板代码。

API

HttpUrlConnection: 灵活轻便,易于扩展,3.0后以及4.0中都进行了改善,如加入响应缓存和支持HTTPS

HttpClient: 高效稳定,但维护成本高昂,bug数量较少,API的数量很多,Android团队不愿意投入精力去维护 :(

Install:

android 5.1 里HttpClient已经被deprecated了:(
在Android6.0则直接删除了HttpClient类库。不过可以再一下目录中找到相关类,以及使用方法:
**sdk\platforms\android-23\optional目录中

 android {
       useLibrary 'org.apache.http.legacy'
        }

@SmartDengg
Copy link
Author

SmartDengg commented Oct 17, 2016

android-async-http

Author: James Smith

avator

简介

官方介绍:

An asynchronous callback-based Http client for Android built on top of Apache’s HttpClient libraries. All requests are made outside of your app’s main UI thread, but any callback logic will be executed on the same thread as the callback was created using Android’s Handler message passing. You can also use it in Service or background thread, library will automatically recognize in which context is ran.

一个构建于HttpClient基础上,基于异步回调的Android网络请求库。所有的请求都在主线程之外,而所有的回调逻辑则通过Android的Handler消息发送机制,在创建它的线程中执行。也就是说无论是在Service还是后台线程中使用,它都能够意识到所运行的上下文环境。

功能与特性

  • 兼容23甚至更高版本
  • 异步HTTP请求,在匿名回调中处理响应结果
  • 在主线程之外执行网络请求
  • 使用线程池处理并发请求
  • 使用RequestParams类来构造GET/POST请求参数
  • 不需要额外添加任何三方库即可轻松实现文件上传
  • 对你的应用来说,它非常轻巧,只有大约90KB大小
  • 自动的请求重试,优化了参差不齐的移动连接
  • 优化连接速度,支持对响应的自动gzip解码
  • 使用BinaryHttpResponseHandler处理二进制协议通信,下载文件或图片等二进制资源
  • 内置Json解析器JsonHttpResponseHandler,对响应进行Json转换
  • 使用FileAsyncHttpResponseHandler,将响应直接保存到本地临时文件
  • 使用SharedPreferences序列化cookie
  • 通过BaseJsonHttpResponseHandler,可集成其他Json序列化/反序列化类库,如Jackson,Gson等
  • 支持SAX解析的SaxAsyncHttpResponseHandler
  • 支持其他语言编码,而不仅仅是UTF-8

Maven

Add maven dependency using Gradle buildscript in format

dependencies {
  compile 'com.loopj.android:android-async-http:1.4.9'
}

核心处理类

  • AsyncHttpClient:发起异步请求的HttpClient封装类
  • RequestParams:HTTP请求参数集合,支持String,InputStream和File等
  • AsyncHttpRequest:一个Runnable实现类,执行异步Http请求。
  • AsyncHttpResponseHandler:抽象类,用来处理响应结果。300以下的状态码onSuccess(),300以上包括300的状态码onFailure()
  • RequestHandle:异步请求的句柄,可以用来取消请求

Sample

那么,我们现在已经对它有了初步的认识。

AsyncHttpClient client = new AsyncHttpClient();
client.get("https://www.google.com", new AsyncHttpResponseHandler() {

    @Override
    public void onStart() {
        // called before request is started
    }

    @Override
    public void onSuccess(int statusCode, Header[] headers, byte[] response) {
        // called when response HTTP status is "200 OK"
    }

    @Override
    public void onFailure(int statusCode, Header[] headers, byte[] errorResponse, Throwable e) {
        // called when response HTTP status is "4XX" (eg. 401, 403, 404)
    }

    @Override
    public void onRetry(int retryNo) {
        // called when request is retried
    }
});

POST

    AsyncHttpClient client = new AsyncHttpClient();
    RequestParams params = new RequestParams();
    params.put("key", "value");
    params.put("more", "data");
    RequestHandle requestHandle =
        client.get("http://www.google.com", params, new TextHttpResponseHandler() {
          @Override public void onSuccess(int statusCode, Header[] headers, String res) {
            // called when response HTTP status is "200 OK"
          }

          @Override
          public void onFailure(int statusCode, Header[] headers, String res, Throwable t) {
            // called when response HTTP status is "4XX" (eg. 401, 403, 404)
          }
        });

    requestHandle.cancel(true);

JSONObject and JSONArray

import org.json.*;
import com.loopj.android.http.*;

class TwitterRestClientUsage {
    public void getPublicTimeline() throws JSONException {
        TwitterRestClient.get("statuses/public_timeline.json", null, new JsonHttpResponseHandler() {
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
                // If the response is JSONObject instead of expected JSONArray
            }

            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONArray timeline) {
                // Pull out the first event on the public timeline
                JSONObject firstEvent = timeline.get(0);
                String tweetText = firstEvent.getString("text");

                // Do something with the response
                System.out.println(tweetText);
            }
        });
    }
}

然而我们都知道AsyncTask的内部封装了future-task,而future-task默认只能执行一次,除非手动清除状态,然后AsyncTask并没有清除状态的API提供给我们:(
因此,没必要针对每次每次请求都创建一个AsyncHttpClient实例,可以使用一个静态访问器,来进行网络访问。

官方实例:

public class TwitterRestClient {
  private static final String BASE_URL = "https://api.twitter.com/1/";

  private static AsyncHttpClient client = new AsyncHttpClient();

  public static void get(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
      client.get(getAbsoluteUrl(url), params, responseHandler);
  }

  public static void post(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) {
      client.post(getAbsoluteUrl(url), params, responseHandler);
  }

  private static String getAbsoluteUrl(String relativeUrl) {
      return BASE_URL + relativeUrl;
  }
}
First release Last release Release Contributors Pull request Issues
on 5 Jun 2011 on 20 Sep 2015 11 73 261 777(closed) 176(open)

image

如果你是从先前的版本升级上来的,并且使用到了网络请求中的Header,那么需要导入一下包:

import org.apache.http.Header;

Replaced with this line:

import cz.msebera.android.httpclient.Header;

同样的,如果有使用到org.apache.http包下的类,也要修改为cz.msebera.android.httpclient.

这里提供了一个快速的修改方式,Mac用户可以试试,代码片段如下:

brew install gnu-sed
find . -name '*.java' -exec gsed -i 's/org.apache.http/cz.msebera.android.httpclient/g' \{\} +

总结

AsyncHttp基于异步回调,封装了HttpClient,简化调用步骤,使用方便,支持Gizp自动解压缩,丰富的ResponseHandler,方便的处理响应结果,默认重试5次,每次间隔1.5秒的重试策略。

BinaryHttpResponseHandler是继承AsyncHttpResponseHandler的子类,这是一个字节流返回处理的类,用于处理图片等类。

JsonHttpResponseHandler是继承AsyncHttpResponseHandler的子类,这是一个json请求返回处理服务器与客户端用json交流时使用的类。

AsyncHttpRequest继承自Runnable,是基于线程的子类,用于异步请求类, 通过AsyncHttpResponseHandler回调。

PersistentCookieStore继承自CookieStore,是一个基于CookieStore的子类, 使用HttpClient处理数据,并且使用cookie持久性存储接口。

虽然AsyncHttp帮我们做了非常优雅的封装,而且功能丰富,不仅简化了网络请求的步骤,还针对不同类型的响应提供了回调接口,这大大减少了我们的代码量。让我们专心写业务代码,而不用花费精力去关注网络连接的细节。

虽然功能丰富,但没有实现有效的响应缓存,配置请求优先级的API也没有,甚至我也没有找到自定义线程池的函数等,无法同步执行网络的处理,当然,你可能会说,这些功能函数我们可以自己添加,但这一切都是建立在打破原有结构的基础上才能实施的,另外它依然没有统一的错误集中处理机制,可怕的回调地狱仍可能发生。而且最重要的一点是HttpClient已经被Android官方标记为过期,甚至被删除,如果继续使用,这无疑会增加我们应用的体积,以至于后面很难维护,甚至无法移除陈旧代码。

Knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 17, 2016

afinal

算得上是一个快速集成框架,不是一个单纯的网络请求库 :(

Author:

简介

Afinal是一个android的ioc,orm框架,内置了四大模块功能:FinalAcitivity,FinalBitmap,FinalDb,FinalHttp。通过finalActivity,我们可以通过注解的方式进行绑定ui和事件。通过finalBitmap,我们可以方便的加载bitmap图片,而无需考虑oom等问题。通过finalDB模块,我们一行代码就可以对android的sqlite数据库进行增删改查。通过FinalHttp模块,我们可以以ajax形式请求http数据

Maven

NO MAVEN ! IT'S A JAR !

功能特性

因为是一个功能继承框架,因此由四个重要的功能模块组成:

  • FinalDB模块:android中的orm框架,一行代码就可以进行增删改查。支持一对多,多对一等查询。
    更优秀的,专注于数据库操作的greendao。
  • FinalActivity模块:android中的ioc框架,完全注解方式就可以进行UI绑定和事件绑定。无需findViewById和setClickListener等。
    编译时生成辅助类的butterknife
  • FinalBitmap模块:通过FinalBitmap,imageview加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象。FinalBitmap可以配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等。FinalBitmap的内存管理使用lru算法,没有使用弱引用(android2.3以后google已经不建议使用弱引用,android2.3后强行回收软引用和弱引用,详情查看android官方文档),更好的管理bitmap内存。FinalBitmap可以自定义下载器,用来扩展其他协议显示网络图片,比如ftp等。同时可以自定义bitmap显示器,在imageview显示图片的时候播放动画等(默认是渐变动画显示)。

几乎所有都可配置的UIL
小巧轻便,基于OKhttp的Picasso
舒服的流式调用,拥有比Picasso更优雅的回收策略的Glide
目前最厉害的图片记载库Fresco

  • FinalHttp模块:通过httpclient进行封装http数据请求,支持ajax方式加载。

GET

单一的回调接口,在不破坏源码的结构的基础上很难进行扩展

FinalHttp fh = new FinalHttp();
fh.get("http://www.yangfuhai.com", new AjaxCallBack(){

    @Override
    public void onLoading(long count, long current) { //每1秒钟自动被回调一次
            textView.setText(current+"/"+count);
    }

    @Override
    public void onSuccess(String t) {
            textView.setText(t==null?"null":t);
    }

    @Override
    public void onStart() {
        //开始http请求的时候回调
    }

    @Override
    public void onFailure(Throwable t, String strMsg) {
        //加载失败的时候回调
    }
});

使用POST上传文件

  AjaxParams params = new AjaxParams();
  params.put("username", "michael yang");
  params.put("password", "123456");
  params.put("email", "test@tsz.net");
  params.put("profile_picture", new File("/mnt/sdcard/pic.jpg")); // 上传文件
  params.put("profile_picture2", inputStream); // 上传数据流
  params.put("profile_picture3", new ByteArrayInputStream(bytes)); // 提交字节流

  FinalHttp fh = new FinalHttp();
  fh.post("http://www.yangfuhai.com", params, new AjaxCallBack(){
        @Override
        public void onLoading(long count, long current) {
                textView.setText(current+"/"+count);
        }

        @Override
        public void onSuccess(String t) {
            textView.setText(t==null?"null":t);
        }
  });

让人感到困惑的AjaxParams,以及AjaxCallBack回调。

文件下载

FinalHttp fh = new FinalHttp();  
    //调用download方法开始下载
    HttpHandler handler = fh.download("http://www.xxx.com/下载路径/xxx.apk", //这里是下载的路径
    true,//true:断点续传 false:不断点续传(全新下载)
    "/mnt/sdcard/testapk.apk", //这是保存到本地的路径
    new AjaxCallBack() {  
                @Override  
                public void onLoading(long count, long current) {  
                     textView.setText("下载进度:"+current+"/"+count);  
                }  

                @Override  
                public void onSuccess(File t) {  
                    textView.setText(t==null?"null":t.getAbsoluteFile().toString());  
                }  

            });  


   //调用stop()方法停止下载
   handler.stop();

支持断点续传,随时停止下载任务或者开始任务。然而这个并不能翻盘我对他的不够专一所表达出的失望 :(

总结

First release Last release Release Contributors Pull request Issues
on Nov 2012 (not sure) on Feb 28 2014 0.2.0 - 0.5.1 6 31 29(closed) 51(open)

除了githubpage上少量的文档说明外,没有提供任何示例。阅读无序的javadoc以及那些高级语法,是件令人头疼的事情,薄弱的社区力量。甚至在我着手写这份调研报告的时候,他还没有发布1.0版本。

@SmartDengg
Copy link
Author

SmartDengg commented Oct 17, 2016

xUtils

与afinal类似,是一个用于快速开发的框架,功能丰富。

Author

简介

xUtils3 api变化较多, 已转至 https://github.com/wyouflf/xUtils3
xUtils 2.x对Android 6.0兼容不是很好, 请尽快升级至xUtils3.
xUtils 包含了很多实用的android工具。
xUtils 支持大文件上传,更全面的http请求协议支持(10种谓词),拥有更加灵活的ORM,更多的事件注解支持且不受混淆影响...

Maven

没有maven仓库,不过作者会将最新的jar包上传至项目的根目录上。

特性

与afinal一样,由四个功能模块组成:

  • DbUtils模块:
    android中的orm框架,一行代码就可以进行增删改查;
    支持事务,默认关闭;
    可通过注解自定义表名,列名,外键,唯一性约束,NOT NULL约束,CHECK约束等(需要混淆的时候请注解表名和列名);
    支持绑定外键,保存实体时外键关联实体自动保存或更新;
    自动加载外键关联实体,支持延时加载;
    支持链式表达查询,更直观的查询语义,参考下面的介绍或sample中的例子。
  • ViewUtils模块:
    android中的ioc框架,完全注解方式就可以进行UI,资源和事件绑定;
    新的事件绑定方式,使用混淆工具混淆后仍可正常工作;
    目前支持常用的20种事件绑定,参见ViewCommonEventListener类和包com.lidroid.xutils.view.annotation.event。
  • BitmapUtils模块:
    加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象;
    支持加载网络图片和本地图片;
    内存管理使用lru算法,更好的管理bitmap内存;
    可配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等...
  • HttpUtils模块:
    支持同步,异步方式的请求;
    支持大文件上传,上传大文件不会oom;
    支持GET,POST,PUT,MOVE,COPY,DELETE,HEAD,OPTIONS,TRACE,CONNECT请求;
    下载支持301/302重定向,支持设置是否根据Content-Disposition重命名下载的文件;
    返回文本内容的请求(默认只启用了GET请求)支持缓存,可设置默认过期时间和针对当前请求的过期时间。

普通GET方法

HttpUtils http = new HttpUtils();
http.send(HttpRequest.HttpMethod.GET,
    "http://www.lidroid.com",
    new RequestCallBack<String>(){
        @Override
        public void onLoading(long total, long current, boolean isUploading) {
            testTextView.setText(current + "/" + total);
        }

        @Override
        public void onSuccess(ResponseInfo<String> responseInfo) {
            textView.setText(responseInfo.result);
        }

        @Override
        public void onStart() {
        }

        @Override
        public void onFailure(HttpException error, String msg) {
        }
});

使用POST上传文件

RequestParams params = new RequestParams();
params.addHeader("name", "value");
params.addQueryStringParameter("name", "value");

// 只包含字符串参数时默认使用BodyParamsEntity,
// 类似于UrlEncodedFormEntity("application/x-www-form-urlencoded")。
params.addBodyParameter("name", "value");

// 加入文件参数后默认使用MultipartEntity("multipart/form-data"),
// 如需"multipart/related",xUtils中提供的MultipartEntity支持设置subType为"related"。
// 使用params.setBodyEntity(httpEntity)可设置更多类型的HttpEntity(如:
// MultipartEntity,BodyParamsEntity,FileUploadEntity,InputStreamUploadEntity,StringEntity)。
// 例如发送json参数:params.setBodyEntity(new StringEntity(jsonStr,charset));
params.addBodyParameter("file", new File("path"));
...

HttpUtils http = new HttpUtils();
http.send(HttpRequest.HttpMethod.POST,
    "uploadUrl....",
    params,
    new RequestCallBack<String>() {

        @Override
        public void onStart() {
            testTextView.setText("conn...");
        }

        @Override
        public void onLoading(long total, long current, boolean isUploading) {
            if (isUploading) {
                testTextView.setText("upload: " + current + "/" + total);
            } else {
                testTextView.setText("reply: " + current + "/" + total);
            }
        }

        @Override
        public void onSuccess(ResponseInfo<String> responseInfo) {
            testTextView.setText("reply: " + responseInfo.result);
        }

        @Override
        public void onFailure(HttpException error, String msg) {
            testTextView.setText(error.getExceptionCode() + ":" + msg);
        }
});

下载文件

HttpUtils http = new HttpUtils();
HttpHandler handler = http.download("http://apache.dataguru.cn/httpcomponents/httpclient/source/httpcomponents-client-4.2.5-src.zip",
    "/sdcard/httpcomponents-client-4.2.5-src.zip",
    true, // 如果目标文件存在,接着未完成的部分继续下载。服务器不支持RANGE时将从新下载。
    true, // 如果从请求返回信息中获取到文件名,下载完成后自动重命名。
    new RequestCallBack<File>() {

        @Override
        public void onStart() {
            testTextView.setText("conn...");
        }

        @Override
        public void onLoading(long total, long current, boolean isUploading) {
            testTextView.setText(current + "/" + total);
        }

        @Override
        public void onSuccess(ResponseInfo<File> responseInfo) {
            testTextView.setText("downloaded:" + responseInfo.result.getPath());
        }


        @Override
        public void onFailure(HttpException error, String msg) {
            testTextView.setText(msg);
        }
});

...
//调用cancel()方法停止下载
handler.cancel();

用作已经不再维护这个库,转而创建了另一个仓库xUtils3

  • xUtils 最低兼容Android 4.0 (api level 14).
  • HTTP实现替换HttpClient为UrlConnection, 自动解析回调泛型, 更安全的断点续传策略.
  • compile 'org.xutils:xutils:3.3.36'

repository First release Last release Release Contributors Pull request Issues size
xUtils 三年前 两年前 2.6.14 2 25 62(closed) 171(open) 284KB(jar)
xUtils3 一年前 两年前 3.3.36 3 10 144(closed) 274(open) 855(jar) 239(dex)
afinal on Nov 2012 (not sure) on Feb 28 2014 0.5.1 6 31 29(closed) 51(open) 157(jar)

相对于afinal,丰富的文档和完整的示例,供开发者参考,但由于其过于丰富的API,导致包的尺寸较大,虽然xutils3在xutils的基础,修改了网络请求的实现细节,而且支持网络相应的泛型解析,但灵活度仍让人担忧,如整合Gson,Jackson这样的json解析库仍是个问题,而且只能在现有接口的基础上对其封装,而不是自定义它们。而且烦人的请求嵌套依然会发生。

@SmartDengg
Copy link
Author

SmartDengg commented Oct 17, 2016

Volley

The core Volley library is developed in the open AOSP repository at frameworks/volley
android / platform / frameworks / volley

没有托管在github上,如果你是第一次点开这个项目的地址,你一会感到很困惑,甚至不知所措,因为它看起来就像这样

Volley是Google 推出的 Android 异步网络请求框架和图片加载框架。在Google I/O 2013 大会上发布。除了简单易用之外,Volley在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。

发布演讲时的配图:

a burst or emission of many things or a large amount at once

Maven

dependencies {
    ...
    compile 'com.android.volley:volley:1.0.0'
}

功能特性

  • 自动调度的网络请求
  • 支持多并发的网络请求
  • 使用标准HTTP缓存一致性,透明的磁盘和内存响应缓存
  • 支持分配请求的优先级
  • 提供了取消网络请求的API
  • 基于接口设计,可配置性强,如重试和退避策略等
  • 完全分离网络加载与UI更新
  • 支持图像加载

GET

使用volley时,必须要创建一个请求队列RequestQueue。当然也可以使用官方推荐的做法,创建一个全局单例,见仁见智。

    // Instantiate the RequestQueue. 
    RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
    String url = "http://www.google.com";

    // Request a string response from the provided URL. 
    StringRequest stringRequest = new StringRequest(url, new Response.Listener<String>() {
      @Override public void onResponse(String response) {
        //update ui

      }
    }, new Response.ErrorListener() {
      @Override public void onErrorResponse(VolleyError error) {
        //handle error

      }
    });
    // Add the request to the RequestQueue.
    queue.add(stringRequest);

POST

     // Instantiate the RequestQueue. 
    RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
    String url = "http://www.google.com";

     // Request a string response from the provided URL. 
    StringRequest stringRequest =
        new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
          @Override public void onResponse(String response) {

          }
        }, new Response.ErrorListener() {
          @Override public void onErrorResponse(VolleyError error) {

          }
        }) {
          @Override protected Map<String, String> getParams() throws AuthFailureError {
            Map<String, String> params = new HashMap<String, String>();
            params.put("user", "deng");
            params.put("pass", "123"); 
            return params;
          }
        };

    // Add the request to the RequestQueue. 
    queue.add(stringRequest);

重载Request抽象类中的getParams()返回POST请求所需要的参数集合,同样的,也可以重载getHeaders()来设置本次的请求头信息,或者getPriority()函数来设置请求的优先级等。

只需要调用RequestQueueadd(),即可发送一个请求,如下图所示,一旦添加到请求队列中,那么它将通过网络管道,连接服务,对相应进行解析,最终返回给调用者。

通过图表可以了解到,一旦将请求通过add()添加到请求队列中,volley首先通过缓存调度线程从缓存中查询本次请求,如果存在满足条件的缓存,则表示命中缓存,同时读取缓存并进行解析,然后传递给主线程回调。如果缓存未命中,则通过网络调度线程,执行HTTP请求,解析响应,写入缓存(如果需要的话),最后将结果传递给主线程的回调函数。值得一提的是每个请求队列,内部会构造五个线程,其中一个处理缓存线程,四个网络线程。

设置请求的TAG

//为请求标记TAG
stringRequest.setTag(TAG); 
//将设置TAG的请求添加到请求队列中
mRequestQueue.add(stringRequest); 

//通过请求队列取消相同TAG的请求
mRequestQueue.cancelAll(TAG);

请求Josn

  • JsonArrayRequest—A request for retrieving a JSONArray response body at a given URL.
    JsonArrayRequest通过给定的URL地址,检索JSONArray格式的响应体
  • JsonObjectRequest—A request for retrieving a JSONObject response body at a given URL, allowing for an optional JSONObject to be passed in as part of the request body.
    与之相对的可以使用JsonObjectRequest来构造一个JSONObject格式的响应体
// Instantiate the RequestQueue. 
RequestQueue queue = Volley.newRequestQueue(getApplicationContext());
String url = "http://my-json-feed";

JsonObjectRequest jsObjRequest = new JsonObjectRequest
        (Request.Method.GET, url, null, new Response.Listener<JSONObject>() {
    @Override
    public void onResponse(JSONObject response) {

    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {

    }
});

// Add the request to the RequestQueue. 
queue.add(request);

Cache

volley自带的cache管理非常的强大,能够缓存所有的HTTP请求,有效的减少网络请求次数,从而避免流量和电量的浪费。

那么它的缓存策略是这样的,通过读取响应头信息,来判断是否需要缓存当前请求。

如果Header的Cache-Control字段含有no-cacheno-store则表示不缓存。
或者根据Cache-Control和Expires首部,计算出缓存的过期时间和缓存的新鲜度时间
如果两个首部都存在情况下,以Cache-Control为准。

默认的缓存实现,将缓存以文件的形式存储在Disk,保证程序退出后不会丢失。

从cache中加载请求

Cache cache = queue.getCache();
Entry entry = cache.get(url);
if(entry != null){
    try {
        String data = new String(entry.data, "UTF-8");
        // handle data, like converting it to xml, json, bitmap etc.,
    } catch (UnsupportedEncodingException e) {      
        e.printStackTrace();
        }
    }
}else{
    // Cached response doesn't exists. Make network call here
}

禁用缓存

stringRequest.setShouldCache(false);

删除指定URL的缓存

queue.getCache().remove(url);

清空缓存

queue.getCache().clear();

从效率和电量的角度来考虑,有时需要做批量请求,而不是零星的分别执行。这种场景下,我们可能需要获取并展示数据,但是可能还没有重要到必须立即更新这些数据的程度。那么比较好的办法就是等待下一次重要更新后,去覆盖那些已经被标记了”缓存失效“的数据,而在这之前我们可以一直使用这些缓存数据。我们只需要对这些特殊的响应缓存,标记失效即可,而不需要删除它们。这就允许我们一直使用这些已经存在的缓存数据,直到新的响应到来,继而将它们覆盖。

不得不再说一遍:使指定URL的缓存失效,但并不会删除该条缓存,接收到新的响应结果后将自动覆盖这条缓存数据。换句话说就是,虽然本条缓存数据被标记为失效状态,但仍然处于可用状态,直到接收到新的响应结果。

queue.getCache().invalidate(url,true);

Volley框架默认在Android Gingerbread(API 9)及以上都是用HttpURLConnection,9以下用HttpClient。

knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 18, 2016

okhttp

An HTTP+HTTP/2 client for Android and Java applications

简介

HTTP是一种现代网络应用层的交互方式。通过它交互数据,媒体等信息。做HTTP优化不仅可以使你的数据加载更快,还能节省你的带宽资源。

OkHttp是一个高效的HTTP客户端:

  • HTTP 2.0支持允许连接到统一主机的所有请求共享一个socket
  • 连接池降低了请求等待(HTTP/2不可用的情况下)
  • GZIP缩小了下载的大小
  • 响应缓存完全避免了重复的网络请求

OKHttp简单易用。它的request/response API被设计成链式调用。它支持同步调用和异步回调调用,两种调用方式。

Install

Maven

在我写这份调研报告的时候,okhttp的最新版本是3.4.1

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.4.1</version>
</dependency>

Gradle

compile 'com.squareup.okhttp3:okhttp:3.4.1'

功能特性

  • 重写请求

在发送请求之前OkHttp会重写你的请求。
比如OkHttp可以为原始的请求添加缺失的消息头信息,包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, and Content-Type。除非Accept-Encoding消息头信息已经存在,否则将会对response进行透明压缩(gzip)。
如果你获取到cookies,OkHttp将添加一个Cookie到其中去。
一些请求会有Response的缓存。当Response的缓存不是最新的,OkHttp会做条件判断,如果缓存不是最新的,则下载最新的的Response。

  • 重写响应

如果透明压缩(gzip)被使用,OkHttp将删除对应的content-Encoding和Content-Length响应头信息,因为它们并不适用于解压缩的response body。

  • 请求重试

如果请求失败:连接池过时或者断开连接,或者服务器本身无法访问。OKHttp将重试有效的请求到不同的路由。

  • 调用
    • 同步调用,你的线程将会阻塞直到响应返回
    • 异步调用, 无论在任何线程中调用,都会被加入请求队列,需要注意的是响应结果并不是在主线程中通过回调返回,而是在其它线程中
  • 任务调度

对于同步调用,需要自己调度线程,并管理并发请求。太多的并发连接浪费资源,尤其会导致请求延迟。

对于异步调用,Dispatcher实现大数量并发请求策略。可以为每个host主机,配置最大并发访问数,默认是5;或者设置应用的最大并发访问数,默认是64

GET

例如,下载一个URL资源,并将内容打印成字符串。

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

POST

例如,利用POST向服务器发送数据。

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();
}

Interceptors

在okhttp中Interceptor扮演者相当重要的角色,比如说追踪,重写或者重试,当然也可以做一些日志打印,统计,或者埋点操作。按顺序添加则按顺序调用。

Application Interceptors

  1. 不用担心中间过程的响应,例如重定向和重试。
  2. 始终调用一次,即使HTTP响应来自于缓存。

Network Interceptors

  1. 能操作中间响应,例如重定向和重试。
  2. 发生网络短路的缓存响应时,不被调用。
  3. 观察将通过网络传输的数据。

BridgeInterceptor

public final class BridgeInterceptor implements Interceptor {

  @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    ...

    // 1.判断请求头信息"Accept-Encoding"是否存在
    // 2.如果是okhttp添加的"Accept-Encoding: gzip",那么也将负责解压缩
    // 3.如果原始请求头中存在"Accept-Encoding: gzip",那么okhttp不负责对响应进行解压缩,需要手动解压缩
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    ...

    // 4.发生网络传输,并获取响应
    Response networkResponse = chain.proceed(requestBuilder.build());

    Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);

    // 5.判断响应头是否包含"Content-Encoding: gzip"字段,并且由okhttp负责解压缩
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {

      // 6.对响应进行解压缩
      GzipSource responseBody = new GzipSource(networkResponse.body().source());

      Headers strippedHeaders = networkResponse.headers()
          .newBuilder()
          .removeAll("Content-Encoding") // 7.移除"Content-Encoding"响应头字段
          .removeAll("Content-Length")   // 8.移除"Content-Length"响应头字段
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
}

更多的调用案例,请参考Recipes所展示的示例。

同步GET - SynchronousGet

异步GET - AsynchronousGet

比如上传文件 - PostFile

缓存响应 - CacheResponse

取消请求 - CancelCall

不在此一一举例,只给出固定链接,因为官方示例太多了,而且很完整。

Knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 18, 2016

retrofit

Type-safe HTTP client for Android and Java by Square

为Android和java而生的类型安全的HTTP客户端。

简介

基于动态代理,运行时反射,在okhttp的基础上构建的,高效,便捷,类型安全的HTTP客户端。

Retrofit requires at minimum Java 7 or Android 2.3

install

截至我在写这份调研报告的时候,retrofit最新版本是2.1.0

Maven

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>2.1.0</version>
</dependency>

Gradle

compile 'com.squareup.retrofit2:retrofit:2.1.0'

基本使用

  1. 将要请求的HTTP API定义到java接口中
public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}
  1. 使用Retrofit实例,生成GitHubService接口的实现类,值得注意的是这个生成由动态代理完成
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);
  1. 通过GitHubService所创建的Call,执行同步或异步的HTTP请求。
Call<List<Repo>> repos = service.listRepos("octocat");

synchronous

List<Repo> repositories = repos.execute().body();

asynchronous

repos.enqueue(new Callback<List<Repo>>() {
      @Override public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {

      }

      @Override public void onFailure(Call<List<Repo>> call, Throwable t) {

      }
    });

一句话来说就是,使用注解来描述HTTP请求。

不仅支持URL参数的替换,还支持query参数的添加

@GET("/list")
Call<ResponseBody> list(@Query("page") int page);

.list(1) ===> /list?page=1

@GET("/list")
Call<ResponseBody> list(@Query("category") String category);

.list(null) ===> /list

@GET("/list")
Call<ResponseBody> list(@Query("category") String... categories);

.list("bar", "baz") ===> /list?category=bar&category=baz

另外值得一提是,这些参数,无论是name还是value,在网络传输过程中都会默认进行URL编码,也就是说一些特殊字符比如“+”也会被编码,这可能不是我们想要的结果,可以通过encoded=true来改变这个行为:

 @GET("/search")
 Call<ResponseBody> list(@Query(value="foo", encoded=true) String foo);

.list() ===> .list("foo+bar")) yields /search?foo=foo+bar

当然还有更多,更丰富,更灵活的使用方式,在次不一一介绍,具体使用方法,请参考官方示例。

适当总结

美中不足,不支持请求优先级。

但几乎所有东西都可以自定义配置,替换原有的回调接口,而不是在他们的基础上进行二次封装,提供了对象转换的工厂类Converter.Factory,无论是json,protocol buffer还是xml结构的响应,添加想要的任何的序列化/反序列化解析器,同时也支持自定的的CallAdapter轻松支持RxJava,Java8, Guava等。

Knowledge

@SmartDengg
Copy link
Author

SmartDengg commented Oct 18, 2016

volley vs okhttp vs retrofit

repository First release Last release Now Release Contributors Pull request Issues
okhttp on 6 May 2013 on 10 Jul 2016 3.4.1 43 121 1448 1302(closed) 108(open)
retrofit on 14 Jun 2012 on 15 Jun 2016 2.1.0 38 103 687 1323(closed) 41(open)

最显而易见的区别,

volley需要指定完整路径的访问地址,并且是在调用的时候以参数的的形式动态添加,可返回JSONObject或者JSONArray,不过这都依赖于所实现的接口类型,默认没有处理反序列化操作。

retrofit可以指定一个base URL,每一个请求只需要提供相对访问路径即可,请求参数均通过java注解来描述,支持动态添加和修改,让我们不必关注请求项的拼接,支持泛型的返回结果,让开发者不必再手动反序列化响应。

我们终于能够站在云端思考业务,而不必蹲在泥里写实现了。

Volley

  1. Volley的优势在于处理小文件的http请求;
  2. 在Volley中也是可以使用Okhttp作为传输层
  3. Volley在处理高分辨率的图像压缩上有很好的支持;
  4. NetworkImageView在GC的使用模式上更加保守,在请求清理上也更加积极,networkimageview仅仅依赖于强大的内存引用,并当一个新请求是来自ImageView或ImageView离开屏幕时 会清理掉所有的请求数据。

OKHttp

  1. OKHttp是Android版Http客户端。非常高效,支持SPDY、连接池、GZIP和 HTTP 缓存。
  2. 默认情况下,OKHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题。
  3. 如果你的应用程序中集成了OKHttp,Retrofit默认会使用OKHttp处理其他网络层请求。

Retrofit

  1. 性能最好,处理最快
  2. 使用REST API时简单方便
  3. 传输层默认就使用OkHttp
  4. 支持NIO,对IO流的操作更高效
  5. 拥有出色的API文档和社区支持
  6. 速度上比volley更快
  7. 默认使用Gson

@SmartDengg
Copy link
Author

SmartDengg commented Oct 19, 2016

接下来的这一段,我想跟大家分享一下,我在日常开发中,尤其是处理网络操作的时候,遇到的那些棘手问题,并且经常让我感到困惑的事情。

这一段应该插在httpurlconnection和httpclient的对比之后。文字有限,不再赘述,大部分都会来自于口述。

那么现在假设,我们已经有了封装性极好的http客户端,我们不关心它的内部实现,也就是说它可以由HttpUrlconnection实现,也可以由
httpclient实现,只不过这些细节,我们不再关注。假设它的健壮性非常好,合理的线程池调度,超时时间,甚至网络缓存等。

OK,我们有了一个配置如此强大的客户端后,那就开始使用吧,但是需要注意的是,我们还没有处理响应的回调接口,没关系,我们还有一个老朋友AsyncTask,他可以帮助我们来处理这些异步任务,而且我们自己封装的HTTP客户端在绝大多数工程中都是跟AsyncTask搭配使用的。不过AsyncTask并不属于本次演讲的内容,因此不做过多赘述,你只需要记住,它是一个在future-task的基础上,封装了Handler消息机制,通过线程池调度处理异步任务的类

比如,我要加载一些URL,以字符串的形式展示这些地址的返回结果。

public class DownloadWebPageTask extends AsyncTask<String, Voice, String> {

   ...

  @Override protected String doInBackground(String... urls) {

    String response = "";

    for (String url : urls) {

      try {
        InputStream inputStream = EffectiveHttpClient.retrieveService(url);
        BufferedReader buffered = new BufferedReader(new InputStreamReader(inputStream));

        String s = "";
        while ((s = buffered.readLine()) != null) {
          response += s;
        }
      } catch (IOException e) {
        //handle IOException

        //handle JSONException ?

        //handle inner Exception ?

        //handle Exception when nest call ?
      }
    }

    return response;
  }

  @Override protected void onPostExecute(String result) {
    mTextView.setText(result);
  }

  @Override protected void onCancelled(String result) {
    // handle cancel with result may be null
  }
}

假设EffectiveHttpClient就是我在上面提到的,自定义的高效HTTP客户端,再所一遍,我们不考虑它的内部细节实现。

那么回到这个例子中,看一看,哪些地方使我们不能接受的,或者说,这样会带来什么隐患。

我们先从代码的角度来考虑问题:

  1. 这种写法,迫使我们不得不添加丑陋的try-catch代码块,捕获异常并处理,这虽然看起来还不错。但是,当我们需要调用多个请求,后一个请求依赖于前一个请求的结果的时候,我们可能要分别处理这些异常,甚至还要校验返回的数据,判断它们的合法性,很难抽离归纳出一个健全的error handler。而且嵌套调用通常很难调试,ugly code!
  2. 虽然我们能够提供“key-value”的请求参数的拼接,但是我们很难搞定参数替换,为特殊的请求设定特殊的请求头信息,也是个麻烦事,很难动态添加和修改。没有人愿意写重复,枯燥的代码。
  3. 解析数据POJO?过滤?缓存?序列化数据至preference或者SQLite?这都是难题
  4. 重试机制和退避策略?
  5. 很难在Android开发中控制线程,进行优雅的线程切换。
  6. AsyncTask自身的设计缺陷,如果开启AsyncTask的ACT/Fragment已经被销毁了,也就是说执行到了其生命周期的onDestroy()。而AsyncTask运行,由于非静态内部类会持有外部类的引用,这就造成了ACT/Fragment的泄漏,如果继续在onPostExecute()中更新UI,可能引起NPE,或者其他崩溃隐患,而且我们很难真正取消一个正在运行的线程。
  7. CALL BACK HELL!

@SmartDengg
Copy link
Author

SmartDengg commented Oct 19, 2016

ALL RXJAVA WITH RETROFIT

优雅线程切换,统一的错误处理,回调天堂,统统给你。

比如说现在有一个需求,通过webservice获取城市列表 -> 根据返回的结果,通过城市ID获取电影列表 -> 根绝返回的结果,通过电影ID获取电影详情 -> 进行展示。

首先定义java接口

interface Service {

    //获取城市列表集合
    @GET("movie/citys") Observable<List<CityListResponse>> getCityList();

    //根据城市ID获取电影列表集合
    @GET("movie/movies.today") Observable<List<MovieListResponse>> getMovies(
        @Query("cityid") String cityId);

    //根据电影ID获取电影详情
    @GET("movie/query") Observable<MovieDetailResponse> getMovieDetail(
        @Query("movieid") String movieId);
  }

处理网络操作的逻辑如下:

private void retrieveMovies() {

    final Service service = ServiceGenerator.createService(Service.class);

    //1. 获取城市列表(网络操作)
    service.getCityList()
        .concatMap(new Func1<List<CityListResponse>, Observable<CityListResponse>>() {
          @Override
          public Observable<CityListResponse> call(List<CityListResponse> cityListResponses) {
            //2.依次取出"城市列表"集合中的元素
            return Observable.from(cityListResponses);
          }
        })
        .concatMap(new Func1<CityListResponse, Observable<List<MovieListResponse>>>() {
          @Override
          public Observable<List<MovieListResponse>> call(CityListResponse cityListResponse) {
            //3.根据每一个城市的id,获取该城市的电影列表(网络操作)
            return service.getMovies(cityListResponse.cityId);
          }
        })
        .concatMap(new Func1<List<MovieListResponse>, Observable<MovieListResponse>>() {
          @Override
          public Observable<MovieListResponse> call(List<MovieListResponse> movieListResponses) {
            //4.依次取出"电影列表"集合中的元素
            return Observable.from(movieListResponses);
          }
        })
        .concatMap(new Func1<MovieListResponse, Observable<MovieDetailResponse>>() {
          @Override
          public Observable<MovieDetailResponse> call(MovieListResponse movieListResponse) {
            //5.根据每一个电影的ID,获取该电影详情(网络操作)
            return service.getMovieDetail(movieListResponse.movieId);
          }
        })
        .subscribeOn(Schedulers.newThread())// 6.线程切换,上游的所有操作执行在工作线程
        .observeOn(AndroidSchedulers.mainThread())//7.线程切换,下游的所有将操作执行在UI线程
        .subscribe(new Subscriber<MovieDetailResponse>() {
          @Override public void onCompleted() {
          // finishCompletion
          }

          @Override public void onError(Throwable e) {
          //error handler
          }

          @Override public void onNext(MovieDetailResponse movieDetailResponse) {
            //8.根据电影ID所获取到的电影详情,该函数会被调用多次。
            textView.append(movieDetailResponse.movieName);
          }
        });
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment