歡迎關(guān)注公眾號(hào):胡飛洋
因?yàn)橐恢睕](méi)有去詳細(xì)了解okhttp原理,在網(wǎng)上找了很多文章,發(fā)現(xiàn)沒(méi)有類似 郭霖的Glide系列那種 細(xì)致的詳解系列槐雾,很不爽,決定自己整一下幅狮,應(yīng)該會(huì)耗費(fèi)不少時(shí)間募强,不過(guò)也是對(duì)自己的挑戰(zhàn),還有點(diǎn)興奮呢崇摄。也希望和大家一起討論钻注。
這是第一篇,按照慣例配猫,就介紹基本的使用方法幅恋,比較簡(jiǎn)單。
好了泵肄,閑話少敘捆交,開(kāi)始!
所需 預(yù)備知識(shí):
HTTP協(xié)議詳解
HTTP請(qǐng)求報(bào)文和響應(yīng)報(bào)文
OkHttp3是由square公司開(kāi)發(fā)腐巢,Android中公認(rèn)最好用的網(wǎng)絡(luò)請(qǐng)求框架品追,在接口封裝上做的簡(jiǎn)單易用,GitHub地址冯丙。
它有以下默認(rèn)特性:
- 支持HTTP/2肉瓦,允許所有同一個(gè)主機(jī)地址的請(qǐng)求共享同一個(gè)socket連接
- 使用連接池減少請(qǐng)求延時(shí)
- 透明的GZIP壓縮減少響應(yīng)數(shù)據(jù)的大小
- 緩存響應(yīng)內(nèi)容,避免一些完全重復(fù)的請(qǐng)求
當(dāng)網(wǎng)絡(luò)出現(xiàn)問(wèn)題的時(shí)候OkHttp 會(huì)自動(dòng)恢復(fù)一般的連接問(wèn)題胃惜,如果你的服務(wù)有多個(gè)IP地址泞莉,當(dāng)?shù)谝粋€(gè)IP請(qǐng)求失敗時(shí),OkHttp會(huì)交替嘗試你配置的其他IP船殉。
一鲫趁、引入
gradle引入依賴即可。
implementation 'com.squareup.okhttp3:okhttp:3.14.7'
implementation 'com.squareup.okio:okio:1.17.5'
3.14.x版本及以前的版本利虫,采用Java語(yǔ)言編寫(xiě)挨厚,4.0.0以后采用kotlin語(yǔ)言;本系列文章中源碼引自3.14.x版本糠惫,以Java語(yǔ)言講解疫剃。
其中Okio庫(kù) 是對(duì)Java.io和java.nio的補(bǔ)充,以便能夠更加方便硼讽,快速的訪問(wèn)巢价、存儲(chǔ)和處理你的數(shù)據(jù)。OkHttp的底層使用該庫(kù)作為支持。
另外蹄溉,別忘了申請(qǐng)網(wǎng)絡(luò)請(qǐng)求權(quán)限咨油,如果還使用網(wǎng)絡(luò)請(qǐng)求的緩存功能,那么還要申請(qǐng)讀寫(xiě)外存的權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
二柒爵、使用方式
基本使用步驟如下
- 構(gòu)建客戶端對(duì)象OkHttpClient
- 構(gòu)建請(qǐng)求Request
- 生成Call對(duì)象
- Call發(fā)起請(qǐng)求(同步/異步)
下面跟著具體使用實(shí)例役电,詳細(xì)介紹。
2.1 get請(qǐng)求
以百度主頁(yè)為例棉胀,進(jìn)行Get請(qǐng)求:
OkHttpClient httpClient = new OkHttpClient();
String url = "https://www.baidu.com/";
Request getRequest = new Request.Builder()
.url(url)
.get()
.build();
Call call = httpClient.newCall(getRequest);
new Thread(new Runnable() {
@Override
public void run() {
try {
//同步請(qǐng)求法瑟,要放到子線程執(zhí)行
Response response = call.execute();
Log.i(TAG, "okHttpGet run: response:"+ response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
首先,創(chuàng)建了OkHttpClient實(shí)例唁奢,接著用Request.Builder構(gòu)建了Request實(shí)例并傳入了百度主頁(yè)的url霎挟,然后httpClient.newCall方法傳入Request實(shí)例生成call,最后在子線程調(diào)用call.execute()執(zhí)行請(qǐng)求獲得結(jié)果response麻掸。
所以酥夭,使用OkHttp進(jìn)行g(shù)et請(qǐng)求,是比較簡(jiǎn)單的脊奋,只要在構(gòu)建Request實(shí)例時(shí)更換url就可以了熬北。
有個(gè)問(wèn)題,你可能注意到了诚隙,這里是放在子線程執(zhí)行請(qǐng)求的讶隐,這是因?yàn)閏all.execute()是同步方法。想要在主線程直接使用而不用手動(dòng)創(chuàng)建子線程可以嘛久又?當(dāng)然可以巫延,使用call.enqueue(callback)即可:
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpGet enqueue: onResponse:"+ response.body().string());
}
});
call.enqueue會(huì)異步執(zhí)行,需要注意的是地消,兩個(gè)回調(diào)方法onFailure炉峰、onResponse是執(zhí)行在子線程的,所以如果想要執(zhí)行UI操作犯建,需要使用Handler切換到UI線程讲冠。
另外,注意每一個(gè)Call只能執(zhí)行一次(原因會(huì)在下篇流程分析中說(shuō)明)适瓦。
執(zhí)行后結(jié)果打印如下:
2020-05-04 21:52:56.446 32681-3631/com.hfy.androidlearning I/OkHttpTestActivity: okHttpGet run: response:<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新聞</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地圖</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>視頻</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>貼吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登錄</a> </noscript> <script>document.write('<a + encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登錄</a>');
</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多產(chǎn)品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>關(guān)于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必讀</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意見(jiàn)反饋</a> 京ICP證030173號(hào) <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
可見(jiàn)百度首頁(yè)的get請(qǐng)求成功響應(yīng)了谱仪。
2.2 post請(qǐng)求
2.2.1 post請(qǐng)求提交String玻熙、文件
post請(qǐng)求與get請(qǐng)求的區(qū)別是 在構(gòu)造Request對(duì)象時(shí),需要多構(gòu)造一個(gè)RequestBody對(duì)象疯攒,用它來(lái)攜帶我們要提交的數(shù)據(jù)嗦随。示例如下:
OkHttpClient httpClient = new OkHttpClient();
MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
String content = "hello!";
RequestBody body = RequestBody.create(contentType, content);
Request getRequest = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(body)
.build();
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
}
});
對(duì)比get請(qǐng)求,把構(gòu)建Request時(shí)的get()改成post(body),并傳入RequestBody實(shí)例枚尼。RequestBody實(shí)例是通過(guò)create方法創(chuàng)建贴浙,需要指定請(qǐng)求體內(nèi)容類型、請(qǐng)求體內(nèi)容署恍。這里是傳入了一個(gè)指定為markdown格式的文本崎溃。
結(jié)果打印如下:
2020-05-05 13:18:26.445 13301-13542/com.hfy.androidlearning I/OkHttpTestActivity: okHttpPost enqueue:
onResponse:Response{protocol=http/1.1, code=200, message=OK, url=https://api.github.com/markdown/raw}
body:<p>hello!</p>
請(qǐng)求成功并把請(qǐng)求體內(nèi)容又返回來(lái)了。
傳入RequestBody的 MediaType 還可以是其他類型盯质,如客戶端要給后臺(tái)發(fā)送json字符串袁串、發(fā)送一張圖片,那么可以定義為:
// RequestBody:jsonBody呼巷,json字符串
String json = "jsonString";
RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
//RequestBody:fileBody, 上傳文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), file);
MediaType更多類型信息可以查看 RFC 2045囱修。
2.2.2 post請(qǐng)求提交表單
構(gòu)建RequestBody除了上面的方式,還有它的子類FormBody王悍,F(xiàn)ormBody用于提交表單鍵值對(duì)破镰,這種能滿足平常開(kāi)發(fā)大部分的需求。
//RequestBody:FormBody压储,表單鍵值對(duì)
RequestBody formBody = new FormBody.Builder()
.add("username", "hfy")
.add("password", "qaz")
.build();
FormBody是通過(guò)FormBody.Builder用構(gòu)建者模式創(chuàng)建鲜漩,add鍵值對(duì)即可。它的contentType在內(nèi)部已經(jīng)指定了渠脉。
private static final MediaType CONTENT_TYPE = MediaType.get("application/x-www-form-urlencoded");
2.2.2 post請(qǐng)求提交復(fù)雜請(qǐng)求體
RequestBody另一個(gè)子類MultipartBody宇整,用于post請(qǐng)求提交復(fù)雜類型的請(qǐng)求體。復(fù)雜請(qǐng)求體可以同時(shí)包含多種類型的的請(qǐng)求體數(shù)據(jù)芋膘。
上面介紹的 post請(qǐng)求 string鳞青、文件、表單为朋,只有單一類型臂拓。考慮一種場(chǎng)景--注冊(cè)場(chǎng)景习寸,用戶填寫(xiě)完姓名胶惰、電話,同時(shí)要上傳頭像圖片霞溪,這時(shí)注冊(cè)接口的請(qǐng)求體就需要 接受 表單鍵值對(duì) 以及文件了孵滞,那么前面講的的post就無(wú)法滿足了。那么就要用到MultipartBody了鸯匹。 完整代碼如下:
OkHttpClient httpClient = new OkHttpClient();
// MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
// String content = "hello!";
// RequestBody body = RequestBody.create(contentType, content);
//RequestBody:fileBody,上傳文件
File file = drawableToFile(this, R.mipmap.bigpic, new File("00.jpg"));
RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), file);
//RequestBody:multipartBody, 多類型 (用戶名坊饶、密碼、頭像)
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("username", "hufeiyang")
.addFormDataPart("phone", "123456")
.addFormDataPart("touxiang", "00.png", fileBody)
.build();
Request getRequest = new Request.Builder()
.url("http://yun918.cn/study/public/file_upload.php")
.post(multipartBody)
.build();
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i(TAG, "okHttpPost enqueue: \n onFailure:"+ call.request().toString() +"\n body:" +call.request().body().contentType()
+"\n IOException:"+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
}
});
可見(jiàn)殴蓬,在構(gòu)建RequestBody時(shí)是使用MultipartBody.Builder構(gòu)建了MultipartBody實(shí)例匿级,通過(guò)addFormDataPart方法傳入了姓名、電話的鍵值對(duì),也通過(guò)addFormDataPart("touxiang", "00.png", fileBody)傳入了頭像圖片痘绎,其中"touxiang"是key值津函, "00.png"是文件名,fileBody是要以上傳的圖片創(chuàng)建的RequestBody孤页。
因?yàn)樗袛?shù)據(jù)都是以鍵值對(duì)的表單形式提交尔苦,所以要設(shè)置setType(MultipartBody.FORM)。
請(qǐng)求抓包結(jié)果:
可見(jiàn)請(qǐng)求體重確實(shí)包含了姓名散庶、電話蕉堰、頭像,并且注意到Content-Type值是 multipart/form-data悲龟。響應(yīng)是200屋讶,說(shuō)明請(qǐng)求成功了。
其他請(qǐng)求方式像put须教、header皿渗、delete,主要在構(gòu)建Request時(shí)把get()或post()換成put()轻腺、header()乐疆、delete()就可以了,但一般在Android端很少用到贬养。
2.4 請(qǐng)求配置項(xiàng)
先看幾個(gè)問(wèn)題:
- 如何全局設(shè)置超時(shí)時(shí)長(zhǎng)?
- 緩存位置挤土、最大緩存大小 呢?
- 考慮有這樣一個(gè)需求误算,我要監(jiān)控App通過(guò) OkHttp 發(fā)出的 所有 原始請(qǐng)求仰美,以及整個(gè)請(qǐng)求所耗費(fèi)的時(shí)間,如何做儿礼?
這些問(wèn)題咖杂,在OkHttp這里很簡(jiǎn)單。把OkHttpClient實(shí)例的創(chuàng)建蚊夫,換成以下方式即可:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024))
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String url = request.url().toString();
Log.i(TAG, "intercept: proceed start: url"+ url+ ", at "+System.currentTimeMillis());
Response response = chain.proceed(request);
ResponseBody body = response.body();
Log.i(TAG, "intercept: proceed end: url"+ url+ ", at "+System.currentTimeMillis());
return response;
}
})
.build();
這里通過(guò)OkHttpClient.Builder通過(guò)構(gòu)建者模式設(shè)置了連接诉字、讀取、寫(xiě)入的超時(shí)時(shí)長(zhǎng)知纷,用cache()方法傳入了由緩存目錄壤圃、緩存大小構(gòu)成的Cache實(shí)例,這樣就解決了前兩個(gè)問(wèn)題琅轧。
還注意到埃唯,使用addInterceptor()方法添加了Interceptor實(shí)例,且重寫(xiě)了intercept方法鹰晨。Interceptor意為攔截器,intercept()方法會(huì)在開(kāi)始執(zhí)行請(qǐng)求時(shí)調(diào)用。其中chain.proceed(request)內(nèi)部是真正請(qǐng)求的過(guò)程模蜡,是阻塞操作漠趁,執(zhí)行完后會(huì)就會(huì)得到請(qǐng)求結(jié)果ResponseBody,所以chain.proceed(request)的前后取當(dāng)前時(shí)間忍疾,那么就知道整個(gè)請(qǐng)求所耗費(fèi)的時(shí)間闯传。上面chain.proceed(request)的前后分別打印的日志和時(shí)間,這樣第三個(gè)問(wèn)題也解決了卤妒。
具體Interceptor是如何工作甥绿,會(huì)在下一篇流程分析中介紹。
另外则披,通常OkHttpClient實(shí)例是全局唯一的共缕,這樣這些基本配置就是統(tǒng)一,且內(nèi)部維護(hù)的連接池也可以有效復(fù)用(會(huì)在下一篇流程分析中介紹)士复。
全局配置的有了图谷,單個(gè)請(qǐng)求的也可以有一些單獨(dú)的配置。
Request getRequest = new Request.Builder()
.url("http://yun918.cn/study/public/file_upload.php")
.post(multipartBody)
.addHeader("key","value")
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
這個(gè)Request實(shí)例阱洪,
- 使用addHeader()方法添加了請(qǐng)求頭便贵。
- 使用cacheControl(CacheControl.FORCE_NETWORK)設(shè)置此次請(qǐng)求是能使用網(wǎng)絡(luò),不用緩存冗荸。(還可以設(shè)置只用緩存FORCE_CACHE承璃。)
好了,okhttp的使用就講這里了蚌本。下篇是工作流程分析盔粹,敬請(qǐng)期待~