Bootstrap

okhttp3使用总结

1. 简介

okhttp是我们在Android开发中十分常用的一个网络请求框架。

2. 常用API总结

下面将介绍okhttp3中的一些常用的api(okhttp3完整文档如下,可自行参阅:https://square.github.io/okhttp/3.x/okhttp/)。

2.1 OkHttpClient

OkHttpClient可以理解为是一个构造Call对象的“工厂”。OkHttpClient实例创建方式有两种:

(1)直接通过new的方式实例化:

// The singleton HTTP client.
public final OkHttpClient client = new OkHttpClient();

 (2)通过OkHttpClient.Builder构造实例,此种方式支持自定义部分行为:

// The singleton HTTP client.
public final OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor())
    .cache(new Cache(cacheDir, cacheSize))
    .build();

 我们在使用OkHttpClient实例时,最好确保它在整个应用中都是唯一的单例。client实例会持有它自己的连接池和线程池,确保client的全局唯一可以让我们在所有的请求中复用这些连接和线程,这有助于减少内存资源的消耗。

为此,官方还提供了newBuilder()的api来帮助我们创建支持自定义参数的可全局共享的OkHttpClient实例:

   OkHttpClient eagerClient = client.newBuilder()
       .readTimeout(500, TimeUnit.MILLISECONDS)
       .build();
   Response response = eagerClient.newCall(request).execute();

 之后,官方文档中还提到,OkHttp中被挂起的线程和连接都将会在保持空闲时自动回收。如果我们想要主动释放资源,可以使用如下方式,之后所有的Call对象执行请求都将被拒绝。

 client.dispatcher().executorService().shutdown();

 清理连接池可以使用如下方式(连接池的守护线程可能不会立即退出):

 client.connectionPool().evictAll();

 如果希望关闭缓存,可以使用如下方式:

client.cache().close();

2.2 Request

Request对象是http请求的抽象(包括请求地址,请求方法,请求头,请求体等内容)。通常情况下,我们可以通过Request.Builder来创建该对象:

    Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .build();

 我们通过request对象的url,method,headers,body等方法能够获取到请求的对应信息。

2.3 Response

Response对象是http响应的抽象(包括响应头,响应体等内容)。

2.4 ResponseBody

ResponseBody抽象的是响应体(包括服务端响应的原始字节信息),我们可以通过response.body()来获取该对象。

需要注意的是,ResponseBody对象必须在我们使用完后关闭。因为每一个ResponseBody都是由有限的资源提供支持,例如套接字(实时网络响应)和打开的文件(用于缓存响应体)。如果没有关闭ResponseBody,将会导致资源的泄露,严重时可能会使得应用变得缓慢或者是崩溃。ResponseBody和Response均实现了closeable接口,我们可以通过如下方法来关闭:

Response.close();
Response.body().close();
Response.body().source().close();
Response.body().charStream().close();
Response.body().byteStream().close();

对于同步调用,我们可以使用try代码块,编译器会帮我们在隐含的finally代码块中调用close方法。例如:

Call call = client.newCall(request);
try (Response response = call.execute()) {
    ... // Use the response.
}

 异步调用中也是类似的:

Call call = client.newCall(request);
call.enqueue(new Callback() {
     public void onResponse(Call call, Response response) throws IOException {
       try (ResponseBody responseBody = response.body()) {
         ... // Use the response.
       }
     }

     public void onFailure(Call call, IOException e) {
       ... // Handle the failure.
     }
});

2.5 Call

Call对象抽象的是一个正在准备执行的请求。它代表着一次完整的请求和响应,可以被取消,但是不能被执行两次。

3. 使用案例

以下例子均来源于官网

3.1 同步Get请求

通过Get请求去服务器上获取helloworld.txt的文本内容:

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    Request request = new Request.Builder()
                        .url("https://publicobject.com/helloworld.txt")
                        .build();

    // client.newCall(request)会返回一个Call对象,通过调用Call对象的execute方法就会执行同步请求,并返回一个Response对象
    try (Response response = client.newCall(request).execute()) {
        if (!response.isSuccessful()) {
            throw new IOException("Unexpected code: " + response);
        }

        // 获取响应头的集合
        Headers responseHeaders = reponse.headers();
        // 遍历并打印响应头的内容
        for (int i = 0; i < responseHeaders.size(); i++) {
            System.out.println(responseHeaders.name(i) + " : " + responseHeaders.value(i));
        }

        // 打印响应体
        System.out.println(response.body().string());
    }
}

在上面的代码中,我们通过response.body().string()获取到了响应体的完整内容。但是如果在响应体的内容(字节数)比较大的情况下,就不建议采用这种方法来获取,因为response.body().string()会默认将整个响应体内容加载到内存中,此时我们最好使用字节流的形式来读取。

3.2 异步Get请求

在3.1中,我们学会了如何通过okhttp来发送同步请求,不过大家应该都知道,Android中的UI线程是不允许发送同步请求的,因为这会阻塞主线程,从而导致ANR。下面将介绍如何在okhttp中通过异步的方式来发送请求:

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    Request request = new Request.Builder()
                        .url("http://publicobject.com/helloworld.txt")
                        .build();

    // 通过调用Call对象的enqueue方法就会执行异步请求,并且会根据请求成功与否执行相应的回调
    client.newCall(request).enqueue(new Callback() {
        @Override 
        public void onFailure(Call call, IOException e) {
            // 非主线程
            e.printStackTrace();
        }

        @Override 
        public void onResponse(Call call, Response response) throws IOException {
            // 非主线程,如果要执行UI操作,需要使用runOnUiThread
            try (ResponseBody responseBody = response.body()) {
                if (!response.isSuccessful()) {
                    throw new IOException("Unexpected code " + response);
                }

                Headers responseHeaders = response.headers();
                for (int i = 0, size = responseHeaders.size(); i < size; i++) {
                    System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
                }

                System.out.println(responseBody.string());
            }
        }
    });
}

3.3 请求头和响应头

下面的例子展示了如何设置请求头以及获取响应头:

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // 请求头的设置
    // header()会覆盖之前已设置的同名请求头,而addHeader()不会
    Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      // response.header(headerName)获取的是响应头对应字段的最新值
      System.out.println("Server: " + response.header("Server"));
      System.out.println("Date: " + response.header("Date"));

      // 获取响应头集合
      System.out.println("Vary: " + response.headers("Vary"));
    }
  }

3.4 Post请求(提交字符串)

下面的例子中通过Post请求提交了一串字符串到服务端:

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    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();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

需要注意的是,整个请求体对象Request都是在内存中的,所以要避免提交的字符串内容过多(不要超过1MB)。

3.5 Post请求(以字节流的形式提交请求体)

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    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();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

3.6 Post请求(提交文件)

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    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();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

3.7 Post请求(以表单形式提交参数)

通过FormBody.Builder我们可以构建表单参数形式的请求体(类似于HTML中的<form>标签),键值对将以兼容HTML表单的方式进行编码。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    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();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

此种方式Post的请求头中Content-Type字段的值是:application/x-www-form-urlencoded。

3.8 Post请求(以MultiPart的格式提交文件,分块请求)

 /**
   * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
   * these examples, please request your own client ID! https://api.imgur.com/oauth2
   */
  private static final String IMGUR_CLIENT_ID = "...";
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    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();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

此种方式Post的请求头中Content-Type字段的值是:multipart/form-data,常用于以表单形式上传文件。详细区别可以参见这篇博客:深入解析 multipart/form-data

3.9 取消一个请求

通过调用Call对象的cancel方法可以立刻停止一个进行中的请求,如果某个线程当前正在执行请求或读取响应,它将会收到一个IOException的异常。

无论是同步请求还是异步请求都能够被取消,在恰当的时候取消请求可以让我们节省网络资源。

  private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
    try (Response response = call.execute()) {
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e);
    }
  }

3.10 超时设置

okhttp请求超时设置方法如下:

  private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    try (Response response = client.newCall(request).execute()) {
      System.out.println("Response completed: " + response);
    }
  }

4. 参考文章

官方文档:https://square.github.io/okhttp/

OkHttp使用完全教程:https://www.jianshu.com/p/ca8a982a116b

Okhttp3基本使用:https://www.jianshu.com/p/da4a806e599b

;