文章

OkHttp基础

OkHttp基础

OkHttp 基本用法

基本的 GET&POST 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "tag";
    private TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = (TextView) findViewById(R.id.tv);

        //        get();
        //        get1();

        //        post();
        String json = "";
        post1(json);

    }

    public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    /**
     * POST提交Json数据
     */
    private void post1(String json) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(5, TimeUnit.SECONDS)
                .connectTimeout(5, TimeUnit.SECONDS)
                .build();

        String url = "";

        RequestBody requestBody = RequestBody.create(JSON, json);

        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();

        Call call = okHttpClient.newCall(request);
        try {
            Response response = call.execute();
            if (response.isSuccessful()) {
                String body = response.body().string();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * POST提交键值对
     * <p/>
     * 通过POST方式把键值对数据传送到服务器
     */
    private void post() {
        String url = "http://172.18.188.37:8080/Demo/newsDetailPageBottomData";

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .readTimeout(5, TimeUnit.SECONDS)
                .connectTimeout(5, TimeUnit.SECONDS)
                .build();

        RequestBody requestBody = new FormBody.Builder()
                .add("username", "hacket")
                .add("password", "123456")
                .build();

        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                        //  .addHeader() 添加
                        //  .header() 设置
                .build();

        final Call call = okHttpClient.newCall(request);

        new Thread() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    if (response.isSuccessful()) {
                        ResponseBody responseBody = response.body();
                        final String body = responseBody.string();
                        Log.d(TAG, "body:" + body);

                        // 回调在工作线程
                        MainActivity.this.runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tv.setText(body);
                            }
                        });
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    private void get1() {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(5, TimeUnit.SECONDS)
                .readTimeout(5, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url("http://www.csdn.net/")
                .build();
        final Call call = okHttpClient.newCall(request);
        new Thread() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    if (response.isSuccessful()) {
                        ResponseBody responseBody = response.body();
                        final String body = responseBody.string();
                        Log.d(TAG, "body:" + body);

                        // 回调在工作线程
                        MainActivity.this.runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tv.setText(body);
                            }
                        });
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    /**
     * HTTP GET
     */
    private void get() {
        String url = "http://gold.xitu.io/entry/56cb3259d342d3005457d151";

        // 默认构造
        // OkHttpClient client = new OkHttpClient();

        // 配置构造
        OkHttpClient client = new OkHttpClient.Builder()
                .readTimeout(30, TimeUnit.SECONDS)
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();

        //        Request
        Request request = new Request.Builder()
                .url(url)
                .build();

        Call call = client.newCall(request);

        //        取消请求,已经完成的请求不能被取消
        //        call.cancel();

        // enqueue()为OkHttp提供的异步方法
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "onFailure: " + call.toString() + "---" + e.getMessage());
            }

            @Override
            public void onResponse(Call call, final Response response) throws IOException {
                // 回调在子线程

                final ResponseBody body = response.body();

                final String str = body.string();

                // 回调在工作线程
                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText(str);
                    }
                });

            }
        });

    }

}

同步 GET/异步 GET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/***
 * 同步/异步get
 */
public void syncGet() {

    final OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .readTimeout(5, TimeUnit.SECONDS)
            .connectTimeout(5, TimeUnit.SECONDS)
            .build();

    final Request request = new Request.Builder()
            .url("http://publicobject.com/helloworld.txt")
            .build();

    new Thread() {
        @Override
        public void run() {

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

                Headers responseHeaders = response.headers();
                for (int i = 0; i < responseHeaders.size(); i++) {

                    Log.d(TAG, "syncGet: " + responseHeaders.name(i) + ": " + responseHeaders.value(i));
                }

                Log.d(TAG, "syncGet: " + response.body().string());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }.start();

    // 异步get
    // okHttpClient.newCall(request).enqueue(callback);
}

提取响应头

典型的 HTTP 头 像是一个 Map<String, String> : 每个字段都有一个或没有值。但是一些头允许多个值,像 Guava 的 Multimap。例如:HTTP 响应里面提供的 Vary 响应头,就是多值的。OkHttp 的 api 试图让这些情况都适用。
当写请求头的时候,使用 header(name, value) 可以设置唯一的 name、value。如果已经有值,旧的将被移除,然后添加新的。使用 addHeader(name, value) 可以添加多值(添加,不移除已有的)。
当读取响应头时,使用 header(name) 返回最后出现的 name、value。通常情况这也是唯一的 name、value。如果没有值,那么 header(name) 将返回 null。如果想读取字段对应的所有值,使用 headers(name) 会返回一个 list。
为了获取所有的 Header,Headers 类支持按 index 访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final OkHttpClient client = new OkHttpClient();
 
public void run() throws Exception {
    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();
 
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
 
    System.out.println("Server: " + response.header("Server"));
    System.out.println("Date: " + response.header("Date"));
    System.out.println("Vary: " + response.headers("Vary"));
}

Post 方式提交 String

使用 HTTP POST 提交请求到服务。这个例子提交了一个 markdown 文档到 web 服务,以 HTML 方式渲染 markdown。因为整个请求体都在内存中,因此避免使用此 api 提交大文档(大于 1MB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static final MediaType MEDIA_TYPE_MARKDOWN
        = MediaType.parse("text/x-markdown; charset=utf-8");

private void postString() {
    final OkHttpClient client = new OkHttpClient();
    String postBody = ""
            + "Releases\n"
            + "--------\n"
            + "\n"
            + " * _1.0_ May 6, 2013\n"
            + " * _1.1_ June 15, 2013\n"
            + " * _1.2_ August 11, 2013\n";

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

    new Thread() {
        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    Log.e(TAG, response.body().string());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

Post 方式提交流

以流的方式 POST 提交请求体。请求体的内容由流写入产生。这个例子是流直接写入 Okio 的 BufferedSink。你的程序可能会使用 OutputStream,你可以使用 BufferedSink.outputStream() 来获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public static final MediaType MEDIA_TYPE_MARKDOWN
        = MediaType.parse("text/x-markdown; charset=utf-8");

private void postStream() {
    final OkHttpClient client = new OkHttpClient();
    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);
        }
    };
     final Request request = new Request.Builder()
            .url("https://api.github.com/markdown/raw")
            .post(requestBody)
            .build();

    new Thread() {
        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    Log.e(TAG, response.body().string());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

Post 方式提交文件

以文件作为请求体是十分简单的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static final MediaType MEDIA_TYPE_MARKDOWN
        = MediaType.parse("text/x-markdown; charset=utf-8");

/**
 * Post方式提交文件
 */
private void postFile() {
    final OkHttpClient client = new OkHttpClient();

    File file = new File(Environment.getExternalStorageDirectory(), "note.md");

    RequestBody requestBody = RequestBody.create(MEDIA_TYPE_MARKDOWN, file);

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

    new Thread() {
        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    final String body = response.body().string();
                    Log.e(TAG, body);
                    MainActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tv.setText(body);
                        }
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

Post 方式提交表单

使用 FormBody 来构建和 HTML<form> 标签相同效果的请求体。键值对将使用一种 HTML 兼容形式的 URL 编码来进行编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void postForm() {
    final OkHttpClient client = new OkHttpClient();

    RequestBody formBody = new FormBody.Builder()
            .add("search", "Jurassic Park")
            .build();

    final Request request = new Request.Builder()
            .url("https://en.wikipedia.org/w/index.php")
            .post(formBody)
            .build();

    new Thread() {
        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    final String body = response.body().string();
                    Log.e(TAG, body);
                    MainActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tv.setText(body);
                        }
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

Post 方式提交分块请求

MultipartBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

private void postPart() {
    final OkHttpClient client = new OkHttpClient();

    MultipartBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addPart(
                    Headers.of("Content-Disposition", "form-data; name=\"title\""),
                    RequestBody.create(null, "Square Logo"))
            .addPart(
                    Headers.of("Content-Disposition", "form-data; name=\"image\""),
                    RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
            .build();

    final Request request = new Request.Builder()
            .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
            .url("https://api.imgur.com/3/image")
            .post(requestBody)
            .build();
    new Thread() {
        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    final String body = response.body().string();
                    Log.e(TAG, body);
                    MainActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tv.setText(body);
                        }
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

使用 Gson 来解析 JSON 响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void postGson() {
    final OkHttpClient client = new OkHttpClient();
    final Gson gson = new Gson();
    final Request request = new Request.Builder()
            .url("https://api.github.com/gists/c2a7c39532239ff261be")
            .build();
    new Thread() {
        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
                    for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
                        System.out.println(entry.getKey());
                        System.out.println(entry.getValue().content);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

static class Gist {
    Map<String, GistFile> files;
}

static class GistFile {
    String content;
}

响应缓存

为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。
一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次 new OkHttpClient(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。
响应缓存使用 HTTP 头作为配置。你可以在请求头中添加 Cache-Control: max-stale=3600 ,OkHttp 缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用 Cache-Control: max-age=9600

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void cache() {

    File cacheDir = new File(getCacheDir(), "bb");
    long cacheSize = 10 * 1024 * 1024; // 10M
    Cache cache = new Cache(cacheDir, cacheSize);
    final OkHttpClient client = new OkHttpClient.Builder()
            .cache(cache)
            .build();

    final Request request = new Request.Builder()
            .url("http://publicobject.com/helloworld.txt")
            .build();

    new Thread() {

        @Override
        public void run() {
            try {
                Response response = client.newCall(request).execute();
                String string1 = "";
                if (response.isSuccessful()) {
                    string1 = response.body().string();
                    Log.d(TAG, "response string:" + string1);
                    Log.d(TAG, "response:" + response);
                    Log.d(TAG, "response cacheResponse:" + response.cacheResponse());
                    Log.d(TAG, "response networkResponse:" + response.networkResponse());
                }

                Response response2 = client.newCall(request).execute();
                String string2 = "  ";
                if (response2.isSuccessful()) {
                    string2 = response2.body().string();
                    Log.d(TAG, "response2 string:" + string2);
                    Log.d(TAG, "response2:" + response2);
                    Log.d(TAG, "response2 cacheResponse:" + response2.cacheResponse());
                    Log.d(TAG, "response2 networkResponse:" + response2.networkResponse());
                }

                Log.d(TAG, "Response 2 equals Response?" + string2.equals(string2));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}

取消一个 Call

使用 Call.cancel() 可以立即停止掉一个正在执行的 call。如果一个线程正在写请求或者读响应,将会引发 IOException。当 call 没有必要的时候,使用这个 api 可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的 call 都可以取消。你可以通过 tags 来同时取消多个请求。当你构建一请求时,使用 Request.Builder.tag(tag) 来分配一个标签。之后你就可以用 OkHttpClient.cancel(tag) 来取消所有带有这个 tag 的 call。okHttp3 用 runningCalls() andqueuedCalls() 取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void cacelCall() {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    final OkHttpClient client = new OkHttpClient();

    final Request request = new Request.Builder()
            .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
            .tag("hacket")
            .build();

    final long startTimeMillis = System.currentTimeMillis();
    final Call call = client.newCall(request);

    executorService.schedule(new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "【Canceling call】" + (System.currentTimeMillis() - startTimeMillis));
            call.cancel();
            Log.d(TAG, "【 Canceled call】" + (System.currentTimeMillis() - startTimeMillis));
        }
    }, 1, TimeUnit.SECONDS);

    Executors.newSingleThreadExecutor().execute(new Runnable() {
        @Override
        public void run() {
            try {
                Log.d(TAG, "【Executing call】" + (System.currentTimeMillis() - startTimeMillis));
                Response response = call.execute();
                Log.d(TAG, "【Call was expected to fail, but completed】" + (System.currentTimeMillis() -
                                                                                   startTimeMillis) + ":"
                        + response);
            } catch (IOException e) {
                Log.d(TAG, "【Call failed as expected】" + (System.currentTimeMillis() - startTimeMillis) + ":" + e);
            }
        }
    });

}

超时

没有响应时使用超时结束 call。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp 支持连接,读取和写入超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final OkHttpClient client;
 
public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient();
    client.setConnectTimeout(10, TimeUnit.SECONDS);
    client.setWriteTimeout(10, TimeUnit.SECONDS);
    client.setReadTimeout(30, TimeUnit.SECONDS);
}
 
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();
    Response response = client.newCall(request).execute();
    System.out.println("Response completed: " + response);
}

每个 call 的配置

使用 OkHttpClient,所有的 HTTP Client 配置包括代理设置、超时设置、缓存设置。当你需要为单个 call 改变配置的时候,clone 一个 OkHttpClient。这个 api 将会返回一个浅拷贝(shallow copy),你可以用来单独自定义。下面的例子中,我们让一个请求是 500ms 的超时、另一个是 3000ms 的超时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private final OkHttpClient client = new OkHttpClient();
 
public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
        .build();
 
    try {
      Response response = client.clone() // Clone to make a customized OkHttp for this request.
          .setReadTimeout(500, TimeUnit.MILLISECONDS)
          .newCall(request)
          .execute();
      System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }
 
    try {
      Response response = client.clone() // Clone to make a customized OkHttp for this request.
          .setReadTimeout(3000, TimeUnit.MILLISECONDS)
          .newCall(request)
          .execute();
      System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
}

OkHttp Proxy

OkHttp 中配置 Proxy

1
2
3
4
5
6
7
8
9
10
OkHttpClient.Builder builder = new OkHttpClient.Builder();

// 设置代理地址
SocketAddress sa = new InetSocketAddress("代理服地址", 代理端口);
builder.proxy(new Proxy(Proxy.Type.HTTP, sa));

OkHttpClient client = builder.build();
Request.Builder requestBuilder = new Request.Builder();
requestBuilder.url("目标服务器地址");
client.newCall(requestBuilder.build());

Proxy 基本原理

正向代理(forward proxy)

正向代理是一个位于客户端和目标服务器之间的服务器 (代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端。

正向代理有个明显的特点:代理服务器是在客户端设置的

案例:Shadowsocks 和 Astrill

反向代理(reverse proxy)

反向代理是指 “ 服务器端 “ 主动部署 “ 代理服务器 “ 来接受互联网上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个反向代理服务器。

反向代理有个明显的特点:代理服务器是部署在服务器端的

总结

用一句话总结正向代理和反向代理的区别就是:正向代理隐藏真实客户端,反向代理隐藏真实服务端。

OkHttp3 缓存

使用缓存

使用缓存可以让我们的 app 不用长时间地显示令人厌烦的加载框,提高了用户体验,而且节省了流量。在数据更新不是很频繁的地方使用缓存就非常有必要了。OkHttp 已经内置了缓存,默认是不使用的,需要我们手动开启

Android 中的 Http 缓存

OkHttp 只对 Get 请求进行缓存,并不会对 Post 请求缓存,因为 Post 请求多是数据交互,没有多少缓存的意义。

服务器支持缓存,配置 OkHttp 缓存目录和大小
1
2
3
4
5
6
7
8
public void btn_okhttp_cache() {
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .cache(provideCache())
            .build();
}
private Cache provideCache() {
    return new Cache(mContext.getCacheDir(), 10 * 1024 * 1024);
}
服务器不支持缓存,用 interceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ResponseCacheInterceptor implements Interceptor {

    private static final String RESPONSE_HEADER_PRAGMA = "Pragma";
    private static final String RESPONSE_HEADER_CACHE_CONTROL = "Cache-Control";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        Response newResponse = response.newBuilder()
                .removeHeader(RESPONSE_HEADER_PRAGMA)
                .removeHeader(RESPONSE_HEADER_CACHE_CONTROL)
                .header(RESPONSE_HEADER_CACHE_CONTROL, "max-age=" + 30 * 24 * 3600) // cache for 30 days
                .build();
        return newResponse;
    }

}

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

Reference

服务器禁用客户端缓存,添加响应头

1
2
3
Expires: -1
Cache-Control: no-cache
Pragma: no-cache

OkHttp 缓存原理

缓存拦截器 CacheInterceptor

在 Interceptor 的链中,在建立连接、和服务器通讯之前,就是 CacheInterceptor,检查响应是否已经被缓存、缓存是否可用,如果是则直接返回缓存的数据,否则就进行后面的流程,并在返回之前,把网络的数据写入缓存。

CacheInterceptor 功能就是服务 request 从缓存中取数据和获取到服务器 response 写入到缓存中去。

OkHttp 如何添加缓存的?
在 RealCall 的 getResponseWithInterceptorChain() 构建了很多系统的 Interceptor,其中就有 CacheInterceptor:

1
2
// Real#getResponseWithInterceptorChain()
interceptors.add(new CacheInterceptor(client.internalCache()));

而 OKHttpClient 设置 cache 的:

1
2
3
4
5
6
7
8
final @Nullable Cache cache;
final @Nullable InternalCache internalCache
public Cache cache() {
    return cache;
}
InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
}

如果有 Cache,就取 Cache 的 internalCache;如果没有,就取 internalCache 值。

具体的缓存逻辑 OkHttp 内置封装了一个 Cache 类,它利用 DiskLruCache,用磁盘的有限大小空间进行缓存,根据 LRU 算法进行缓存淘汰。

对 OkHttp 内置的 Cache 类不满意,我们可以自行实现 InternalCache 接口,这样就可以在 OKHttpClient 设置自定义的缓存策略了。

Cache 类主要封装了 HTTP 协议缓存细节的实现

OkHttp 注意

OkHttp encode 注意

post 参数,如果已经 decode 了,包含\n 特殊字符的话,会被过滤掉,导致比如聊天室公告,需要换行,

1
FormBody.Builder.addEncoded()

具体看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static void canonicalize(Buffer out, String input, int pos, int limit, String encodeSet,
      boolean alreadyEncoded, boolean strict, boolean plusIsSpace, boolean asciiOnly,
      Charset charset) {
Buffer encodedCharBuffer = null; // Lazily allocated.
int codePoint;
for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
  codePoint = input.codePointAt(i);
  if (alreadyEncoded
      && (codePoint == '\t' || codePoint == '\n' || codePoint == '\f' || codePoint == '\r')) {
    // Skip this character.
  } else if (codePoint == '+' && plusIsSpace) {
    // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'.
    out.writeUtf8(alreadyEncoded ? "+" : "%2B");
  } else if (codePoint < 0x20
      || codePoint == 0x7f
      || codePoint >= 0x80 && asciiOnly
      || encodeSet.indexOf(codePoint) != -1
      || codePoint == '%' && (!alreadyEncoded || strict && !percentEncoded(input, i, limit))) {
    // Percent encode this character.
    if (encodedCharBuffer == null) {
      encodedCharBuffer = new Buffer();
    }

    if (charset == null || charset.equals(Util.UTF_8)) {
      encodedCharBuffer.writeUtf8CodePoint(codePoint);
    } else {
      encodedCharBuffer.writeString(input, i, i + Character.charCount(codePoint), charset);
    }

    while (!encodedCharBuffer.exhausted()) {
      int b = encodedCharBuffer.readByte() & 0xff;
      out.writeByte('%');
      out.writeByte(HEX_DIGITS[(b >> 4) & 0xf]);
      out.writeByte(HEX_DIGITS[b & 0xf]);
    }
  } else {
    // This character doesn't need encoding. Just copy it over.
    out.writeUtf8CodePoint(codePoint);
  }
}
}

解决:

1
FormBody.Builder.add()

Header 不允许特殊字符

OkHttp 不的 header 的 name 和 value 默认是不允许有特殊字符(非 ASCII 码字符),否则会抛异常,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Headers.Builder
class Builder {
    public Builder add(String name, String value) {
      checkName(name);
      checkValue(value, name);
      return addLenient(name, value);
    }
}
static void checkName(String name) {
    if (name == null) throw new NullPointerException("name == null");
    if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
    for (int i = 0, length = name.length(); i < length; i++) {
      char c = name.charAt(i);
      if (c <= '\u0020' || c >= '\u007f') {
        throw new IllegalArgumentException(Util.format(
            "Unexpected char %#04x at %d in header name: %s", (int) c, i, name));
      }
    }
}

static void checkValue(String value, String name) {
    if (value == null) throw new NullPointerException("value for name " + name + " == null");
    for (int i = 0, length = value.length(); i < length; i++) {
      char c = value.charAt(i);
      if ((c <= '\u001f' && c != '\t') || c >= '\u007f') {
        throw new IllegalArgumentException(Util.format(
            "Unexpected char %#04x at %d in %s value: %s", (int) c, i, name, value));
      }
    }
}

如果允许非 ASCII 码的值作为 value,调用 addUnsafeNonAscii(name, value)

1
2
3
4
5
6
7
8
/**
 * Add a header with the specified name and value. Does validation of header names, allowing
 * non-ASCII values.
 */
public Builder addUnsafeNonAscii(String name, String value) {
  checkName(name);
  return addLenient(name, value);
}

重定向

followRedirects

默认开启,设置是否禁止 OkHttp 的重定向操作,我们自己处理重定向

1
2
3
public Builder followRedirects(boolean followRedirects) {
    
}

followSslRedirects

默认开启,设置是否 https 的重定向也自己处理

1
2
3
public Builder followSslRedirects(boolean followProtocolRedirects) {
    
}
本文由作者按照 CC BY 4.0 进行授权