OkHttp作為square公司出品的一個網(wǎng)絡(luò)請求框架禽最,應(yīng)該算是目前Android端最火爆的網(wǎng)絡(luò)框架了嗤攻。我公司目前的項目中采用的都是Rxjava結(jié)合Retrofit進行網(wǎng)絡(luò)請求的處理毛嫉,對于底層真正實現(xiàn)網(wǎng)絡(luò)請求的OkHttp關(guān)注的不是很多。最近探究了一下OkHttp的源碼妇菱,對OkHttp的使用有了一些新的認(rèn)識承粤,在此做一下總結(jié)。
1 OkHttp的優(yōu)點
OkHttp作為當(dāng)前Android端最火熱的網(wǎng)絡(luò)請求框架闯团,必然有很多的優(yōu)點辛臊。
- 支持HTTP/2 協(xié)議,允許連接到同一個主機地址的所有請求共享Socket房交。這必然會提高請求效率彻舰。
- 在HTTP/2協(xié)議不可用的情況下,通過連接池減少請求的延遲候味。
- GZip透明壓縮減少傳輸?shù)臄?shù)據(jù)包大小刃唤。
- 響應(yīng)緩存,避免同一個重復(fù)的網(wǎng)絡(luò)請求白群。
2 網(wǎng)絡(luò)處理3要素
對于客戶端來講尚胞,我們關(guān)注的就是把正確的請求發(fā)送到服務(wù)端并拿到結(jié)果來進行處理。在OkHttp中帜慢,我認(rèn)為可以分為3個部分:
- Request類封裝客戶端發(fā)送的請求笼裳,包括請求的url,請求方法method(主要是GET和POST方法)粱玲、請求頭header以及請求體requestBody躬柬;
- Response類封裝了服務(wù)器響應(yīng)的數(shù)據(jù),包括code抽减、message允青、body、header等胯甩。
- OkHttpClient負責(zé)發(fā)送請求Request并通過同步或者異步的方式返回服務(wù)器的響應(yīng)Response昧廷,就好比是一個瀏覽器。
OkHttp中通過建造者模式來構(gòu)建OkHttpClient偎箫、Request和Response木柬。對于客戶端來講,我們不需要過多關(guān)注Response是如何構(gòu)建的淹办,因為這個是OkHttp對響應(yīng)結(jié)果進行了封裝處理眉枕。我們只關(guān)注請求Request和客戶端OkHttpClient如何構(gòu)建即可。
2.1 請求Request
Request采用建造者模式來配置url怜森,請求方法method速挑、header、tag和cacheControl副硅。
- 設(shè)置url姥宝。可以是String類型恐疲、URL類型和HttpUrl類型腊满。最終都是用到HttpUrl類型。
- 設(shè)置method培己,包含get碳蛋、post方法等。默認(rèn)的是get方法省咨。post方法要傳RequestBody肃弟,類似的還有delete、put零蓉、patch笤受。
- 設(shè)置header,方法有addHeader(String name, String value)壁公、 removeHeader(String name)感论、header(String name, String value)、headers(Headers headers)紊册。headers(Headers headers)調(diào)用之后其它的header都會被移除比肄,只添加這一個header。而header(String name, String value)方法調(diào)用之后囊陡,其它與這個name同名的header都會被移除芳绩,只保留這一個header。
- 設(shè)置tag撞反,設(shè)置tag可以用來取消這一請求妥色。如果未指定tag或者tag為null,那么這個request本身就會當(dāng)做是一個tag用來被取消請求遏片。
- 設(shè)置cacheControl嘹害,這個是設(shè)置到請求頭中撮竿。用來替換其它name是"Cache-Control"的header。如果cacheControl是空的話就會移除請求頭中name是"Cache-Control"的header笔呀。
OkHttp采用POST方法向服務(wù)器發(fā)送一個請求體幢踏,在OkHttp中這個請求體是RequestBody。這個請求體可以是:
- String類型
- Stream流類型
- File文件類型
- Form表單形式的key-value類型
- 類似Html文件上傳表單的復(fù)雜請求體類型(多塊請求)许师。
RequestBody有幾個靜態(tài)方法用于創(chuàng)建不同類型的請求體:
//創(chuàng)建String類型的請求體
public static RequestBody create(MediaType contentType, String content)
//創(chuàng)建文件類型的請求體
public static RequestBody create(final MediaType contentType, final File file)
最終都是相當(dāng)于重寫了RequestBody的兩個抽象方法來寫入流房蝉,如果傳遞流類型的參數(shù),只要重寫這兩個抽象方法即可微渠。
//對應(yīng)的是name為Content-Type的header
public abstract MediaType contentType();
//這個BufferedSink位于Okio包下搭幻,提供高效的寫入。
public abstract void writeTo(BufferedSink sink) throws IOException;
//在寫入的時候可以傳遞內(nèi)容的大小逞盆,如果不知道就返回-1即可檀蹋。
public long contentLength() throws IOException { return -1;}
例如,我們提交一個String:
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();
提交File:
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();
提交流:
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();
對于提交表單和分塊請求云芦,OkHttp提供了兩個RequestBody的子類续扔,FormBody和MultipartBody
2.1.1 表單FormBody
FormBody也是采用建造者模式, 這個很簡單焕数,添加key-value形式的鍵值對即可纱昧。
添加鍵值對有兩個方法:
//采用OkHttp默認(rèn)的編碼
public Builder add(String name, String value)
//采用用戶要求的編碼
public Builder addEncoded(String name, String value)
例如:
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();
2.1.2 分塊MultipartBody
MultipartBody也是采用建造者模式,MultipartBody.Builder可以構(gòu)建兼容Html文件上傳表單的復(fù)雜請求體堡赔。每一部分的多塊請求體都是它自身的請求體识脆,并且可以定義它自己的請求頭。如果存在的話善已,這些請求頭用來描述這部分的請求體灼捂。例如Content-Disposition、Content-Length 和 Content-Type如果可用就會被自動添加到頭换团。
MIME類型有:
public static final MediaType MIXED = MediaType.parse("multipart/mixed");
public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative");
public static final MediaType DIGEST = MediaType.parse("multipart/digest");
public static final MediaType PARALLEL = MediaType.parse("multipart/parallel");
有幾個主要的方法:
//設(shè)置MIME類型悉稠,如MIXED(默認(rèn)的)
public Builder setType(MediaType type) {}
//添加請求體
public Builder addPart(RequestBody body) {
return addPart(Part.create(body));
}
//添加包含header的請求體
public Builder addPart(Headers headers, RequestBody body) {
return addPart(Part.create(headers, body));
}
//請求體添加表單
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
//請求體中包含文件
public Builder addFormDataPart(String name, String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
//添加自己定義的part
public Builder addPart(Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
例如提交一個圖片文件:
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();
2.2 客戶端OkHttpClient
OkHttpClient采用建造者模式,通過Builder可以配置連接超時時間艘包、讀寫時間的猛,是否緩存、是否重連想虎,還可以設(shè)置各種攔截器interceptor等卦尊。
建議在一個App中,OkHttpClient保持一個實例舌厨。一個OkHttpClient支持一定數(shù)量的并發(fā)岂却,請求同一個主機最大并發(fā)是5,所有的并發(fā)最大是64。這個與OkHttp中的調(diào)度器Dispatcher有關(guān)躏哩,可以設(shè)置并發(fā)數(shù)署浩。本文不對Dispatcher進行討論。
OkHttpClient okHttpClient=new OkHttpClient.Builder().build();
//如果不需要我們額外配置扫尺,可以使用默認(rèn)的配置
OkHttpClient okHttpClient1 = new OkHttpClient();
一個例子:
int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheDirectory = new File(getCacheDir(), "OkHttpCache");
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)//連接超時時間
.readTimeout(60, TimeUnit.SECONDS)//讀的時間
.writeTimeout(60, TimeUnit.SECONDS)//寫的時間
.cache(cache)//配置緩存
.build();
OkHttpClient支持單獨配置瑰抵,例如原來設(shè)置不同的請求時間,可以通過OkHttpClient的newBuilder()方法來重新構(gòu)造一個OkHttpClient器联。例如:
OkHttpClient client = new OkHttpClient();
//讀的時間設(shè)置為500ms
OkHttpClient copy = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
//讀的時間設(shè)置為3000ms
OkHttpClient copy = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build();
3 同步請求和異步請求
上面已經(jīng)講了如何創(chuàng)建Request和OkHttpClient,剩下的就是發(fā)送請求并得到服務(wù)器的響應(yīng)了婿崭。OkHttp發(fā)送請求可分為同步和異步拨拓。OkHttpClient首先通過Request構(gòu)建一個Call,通過這個Call去執(zhí)行同步或者異步請求氓栈。
#OkHttpClient
public Call newCall(Request request)
同步方式渣磷,調(diào)用Call的execute()方法,返回Response授瘦,會阻塞當(dāng)前線程:
response = client.newCall(request).execute();
異步方式醋界,調(diào)用Call的enqueue(CallBack callBack)方法,會在另一個線程中返回結(jié)果提完。
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//處理錯誤的回調(diào)
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//處理正確的回調(diào)
}
});
4 其他
4.1 配置響應(yīng)緩存
為了緩存響應(yīng)形纺,需要一個可讀寫并且設(shè)置大小Size的緩存目錄。緩存目錄需要私有徒欣,其它不信任的應(yīng)用不能訪問這個文件逐样。
如果同時有多個緩存訪問同一個緩存目錄會報錯。所以最好只在App中初始化一次OkHttpClient打肝,給這個實例配置緩存脂新,在整個App生命周期內(nèi)都用這一個緩存。否則幾個緩存會相互影響粗梭,導(dǎo)致緩存出錯争便,引起程序崩潰。
響應(yīng)緩存采用Http頭來配置断医,你可以添加這樣的請求頭Cache-Control: max-stale=3600滞乙。 max-age指的是客戶端可以接收生存期不大于指定時間(以秒為單位)的響應(yīng)。
為了防止響應(yīng)使用緩存鉴嗤,可以用CacheControl.FORCE_NETWORK酷宵。為了防止使用網(wǎng)絡(luò),采用 CacheControl.FORCE_CACHE躬窜。
注意:如果使用FORCE_CACHE禁止使用網(wǎng)絡(luò)浇垦,而響應(yīng)又沒有緩存存在,OkHttp會報**504 Unsatisfiable Request **響應(yīng)錯誤荣挨。
4.2 取消請求
調(diào)用Call.cancel()方法可以立即取消一個網(wǎng)絡(luò)請求男韧。如果當(dāng)前線程正在寫request或者讀response會報IO異常朴摊。如果不再需要網(wǎng)絡(luò)請求,采用這種方法是比較方便的此虑。例如在App中返回了上一頁甚纲。無論是同步還是異步的請求都可以被取消。
4.3 Response讀取響應(yīng)結(jié)果
可以通過Response的code來判斷請求是否成功朦前,如果服務(wù)器返回的有數(shù)據(jù)介杆,可以通過Response的body得到一個ResponseBody讀取。
如果采用ResponseBody的string()方法會一次性把數(shù)據(jù)讀取到內(nèi)存中韭寸,如果數(shù)據(jù)超過1MB可能會報內(nèi)存溢出春哨,所以對于超過1MB的數(shù)據(jù),建議采用流的方式去讀取恩伺,如ResponseBody的byteStream()方法赴背。
需要說明的是:
- 如果ResponseBody的內(nèi)容不讀取的話,不會觸發(fā)IO流的讀取操作
- 內(nèi)容讀取之后晶渠,這個body需要關(guān)閉凰荚。
5 總結(jié)
OkHttp中的很多類都用到了建造者模式,可以根據(jù)需要靈活配置褒脯。采用建造者模式的有:
- OkHttpClient.Builder
- Request.Builder
- FormBody.Builder
- MultipartBody.Builder
- Response.Builder
如果單獨使用OkHttp進行網(wǎng)絡(luò)請求便瑟,通常需要開發(fā)者自己再封裝一下,如果不想重復(fù)造輪子番川,Github上面的有一些優(yōu)秀開源庫可以拿來使用(本文只列出star較多的幾個):
- hongyangAndroid/okhttputils(曾經(jīng)在項目中用過)
- jeasonlzy/okhttp-OkGo
- yanzhenjie/NoHttp