1 簡介
OkHttp是一個用於Android網絡請求的第三方開源的輕量級框架。該框架由移動支付Square公司貢獻,其優勢有支持HTTP/2,允許連接到同一個主機地址的所有請求共享一個Socket連接;若HTTP/2不可用情況下,還可通過連接池的設計減少請求延遲;自動處理GZip壓縮節省響應數據大小;支持緩存響應請求數據避免重複請求等。
其實我們在上一篇文章《Android網絡編程(八) 之 HttpURLConnection原理分析》中就已經暴光過okhttp框架了。因爲HttpURLConnection裏針對http和https協議的處理底層就是通過OkHttp來完成的,當時提到其源碼下載地址可訪問:https://android.googlesource.com/platform/external/okhttp 進行下載。實際上,OkHttp也有自己的官網:https://square.github.io/okhttp。OkHttp第3個版本,也就是我們常提到的OkHttp3是一個里程碑版本,儘管目前最新版本已升級至4.X,但其內部還是保持着與OkHttp3.X的嚴格兼容,甚至包名仍然是okhttp3。還值得一提的,在OkHttp 4.0.0 RC 3版本後它的實現語言從Java變成了Kotlin來實現。
2 快速上手
開始使用前,請在你工程gradle中配置好OkHttp的依賴,文章寫作時官網最新版本是4.2.1,配置代碼如下:
implementation("com.squareup.okhttp3:okhttp:4.2.1")
以及在AndroidManifest.xml中添加訪問網絡權限
<uses-permissionandroid:name="android.permission.INTERNET" />
2.1 同步Get請求
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void syncGet() throws Exception {
Request request = new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call = mOkHttpClient.newCall(request);
Response response = call.execute();
if (response.isSuccessful()) {
Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(response.body().string());
}
}
2.2 異步Get請求
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void asyncGet() {
Request request = new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
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(response.code());
System.out.println(response.message());
System.out.println(response.body().string());
}
}
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
});
}
2.3 Post提交字符串
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void postString() throws Exception {
String postBody = "Hello world!";
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(RequestBody.create(MediaType.parse("text/x-markdown; charset=utf-8"), postBody))
.build();
Response response = mOkHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
2.4 Post提交流
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void PostStreaming() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MediaType.parse("text/x-markdown; charset=utf-8");
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Hello \n");
sink.writeUtf8("world");
}
};
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBody)
.build();
Response response = mOkHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
2.5 Post提交文件
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void PostFile() throws Exception {
File file = new File("README.md");
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(RequestBody.create(MediaType.parse("text/x-markdown; charset=utf-8"), file))
.build();
Response response = mOkHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
2.6 Post提交表單
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void PostForm() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("name", "zyx")
.build();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(formBody)
.build();
Response response = mOkHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
2.7 Post複雜請求體
MultipartBody就是可以構建與HTML文件上傳表單形式兼容的複雜的請求體。multipart請求體的每一部分本身就是請求體,並且可以定義自己的頭部。這些請求頭可以用來描述的部分請求體,如它的 Content-Disposition 。如果 Content-Length 和 Content-Type 可用的話,則會自動添加。
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void postMultipart() throws Exception {
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png", RequestBody.create(MediaType.parse("image/png"), new File("website/static/logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + "...")
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
Response response = mOkHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
2.8 緩存響應和超時時間
我們只要在OkHttpClient創建時通過cache方法傳入一個Cache類對象,便可使請求支持緩存。而Cache對象的創建就是要傳入可進行讀寫緩存目錄和(一般應該是私自有目錄)一個緩存大小限制值即可。而connectTimeout、writeTimeout和readTimeout方法可設置訪問的連接、寫、讀的超時時間。
public void responseCaching(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response1 = client.newCall(request).execute();
if (response1.isSuccessful()) {
System.out.println(response1.body().string());
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());
}
Response response2 = client.newCall(request).execute();
if (response2.isSuccessful()) {
System.out.println(response2.body().string());
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());
}
}
2.9 取消請求
使用Callcancel()方法可立即停止正在進行的Call。如果一個線程目前正在寫請求或讀響應,它還會收到一個IOException異常,其異常信息如:java.io.IOException: Canceled
private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void syncGet() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2")
.get()
.build();
final Call call = mOkHttpClient.newCall(request);
// 將其取消
new Thread(new Runnable() {
@Override
public void run() {
call.cancel();
}
}).start();
try {
Response response = call.execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
} catch (Exception e) {
e.printStackTrace();
}
}
2.10 認證處理
OkHttp會自動重試未驗證的請求. 當響應是401 Not Authorized時,Authenticator會被要求提供證書. Authenticator的實現中需要建立一個新的包含證書的請求. 如果沒有證書可用, 返回null來跳過嘗試.
使用Response.challenges()來獲得任何authentication challenges的 schemes 和 realms. 當完成一個Basic challenge, 使用Credentials.basic(username, password)來解碼請求頭.
public void syncGet() throws Exception {
OkHttpClient client = new OkHttpClient.Builder()
.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already attempted to authenticate.
}
System.out.println("Authenticating for response: " + response);
System.out.println("Challenges: " + response.challenges());
String credential = Credentials.basic("jesse", "password1");
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
})
.build();
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.get()
.build();
Call call = client.newCall(request);
Response response = call.execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
3 攔截器
OkHttp2.2以後加入了攔截器,其設計思想可謂是整個框架的精髓,它可以實現網絡監聽、請求重寫、響應重寫、請求失敗重試等功能。從一個網絡請求的發出到響應的過程中間會經歷了數個攔截器。
攔截器本質上都是基於Interceptor接口,而我們開發者能夠自定義的攔截器有兩類,分別是:ApplicationInterceptor(應用攔截器,通過使用addInterceptor方法添加) 和 NetworkInterceptor(網絡攔截器,通過使用addNetworkInterceptor方法添加)。一個完整的攔截器結構大概如下圖所示:
3.1 攔截器的選擇
下面我們以實例的方式來認識攔截的使用。首先定義一個日誌攔截LoggingInterceptor,然後通過在創建OkHttpClient對象時,分別使用addInterceptor方法和addNetworkInterceptor方法將該日誌攔截器添加到不同的位置。
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
// 請求
Request request = chain.request();
long t1 = System.nanoTime();
System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));
// 響應
Response response = chain.proceed(request);
long t2 = System.nanoTime();
System.out.println(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
public void syncGet() throws Exception {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
// .addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.get()
.build();
Call call = client.newCall(request);
Response response = call.execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
}
來看使用addInterceptor輸出的結果:
Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
再來使用addNetworkInterceptor輸出的結果:
Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
因爲URL:http://www.publicobject.com/helloworld.txt 最終會重定向到 https://publicobject.com/helloworld.txt。能看出使用addInterceptor添加的ApplicationInterceptor輸出的信息只有初次請求和最終響應,而使用addNetworkInterceptor添加的NetworkInterceptor輸出的信息包含了初次的請求、初次的響應,重定向後的請求和重定向後的響應。
所以,我們總結ApplicationInterceptor和NetworkInterceptor在使用上的選擇,若不關心中間過程,只需最終結果的攔截使用ApplicationInterceptor即可;如果需要攔截請求過程中的中間響應,那麼就需要使用NetworkInterceptor。
3.2 重寫請求和重寫響應
攔截器的出現並不是爲了如上述給我們提供日誌的打印,攔截器還可以在請求前進行添加、移除或者替換請求頭。甚至在有請求主體時候,可以改變請求主體。以及在響應後重寫響應頭並且可以改變它的響應主體(重寫響應通常不建議,因爲這種操作可能會改變服務端所要傳遞的響應內容的意圖)。實現例如像以下代碼,以下攔截器實現瞭如請求體中不存在"Content-Encoding",則給它添加經過壓縮之後的請求主體。
final class GzipRequestInterceptor implements Interceptor {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
return chain.proceed(compressedRequest);
}
}
4 使用建議
建議在創建OkHttpClient 實例時,讓其是一個單例。因爲OkHttpClient 內部存在着相應的連接池和線程池,當多個請求發生時,重用這些資源可以減少延時和節省資源。還要注意的是,每一個Call對象只可能執行一次RealCall,否則程序會發生異常。