PREVIEW
- 導(dǎo)入
- 簡單使用
- 使用HttpUrl
- Header處理
- Post提交String
- Post提交表單
- Post提交文件
- Post提交流
- 使用Gson解析response
- 設(shè)置超時
- 配置新client
- 攔截器
? 應(yīng)用攔截器
? 網(wǎng)絡(luò)攔截器
? 兩者區(qū)別- 緩存處理
? 緩存策略
? 緩存流程
? 緩存總結(jié)
導(dǎo)入
在build.gradle下添加依賴,具體版本號參考OkHttp更新日志
dependencies {
...
implementation 'com.squareup.okhttp3:okhttp:4.7.2'
}
簡單使用
本文使用的OkHttp版本為4.7.2。OkHttp的核心類主要有OkHttpClient
荸型,Dispatcher
贺待,Call
,Request
蝙叛,Response
撮弧,Interceptor
饲做,Chain
飒箭。其中OkHttpClient
是負(fù)責(zé)管理多個Call
的組織者,而每個Call
又包含一個Request
和Response
蜒灰,并且Call
中的回調(diào)用于提供響應(yīng)結(jié)果弦蹂。要完成一次網(wǎng)絡(luò)請求,我們需要告訴Call
需要處理的Request
是什么樣的强窖,例如它的URL是什么凸椿,然后將Call
交給OkHttpClient
。OkHttpClient
僅對本次請求做一些配置翅溺,例如指定緩存路徑脑漫,它會讓Dispatcher
去決定何時執(zhí)行Call
。而Dispatcher
的底層實現(xiàn)就是一個由OkHttp默認(rèn)實現(xiàn)的線程池咙崎,它將最終執(zhí)行Call
中的.run()
方法优幸。最后的Interceptor
和Chain
將用于數(shù)據(jù)的攔截處理。OkHttp提供兩種方式提交網(wǎng)絡(luò)請求褪猛,分別是Call.execute()
和Call.enqueue(Callback)
网杆,前者會阻塞線程,后者加入隊列異步執(zhí)行。通過調(diào)用response.body().string()
我們可以得到響應(yīng)的body部分并以String
形式返回碳却,但值得注意的是.string()
只能調(diào)用一次队秩。
- 同步調(diào)用
一般來說要在得到結(jié)果的第一時間修改UI,我們可能會使用Call.execute()
和AsyncTask
完成提交請求昼浦。但AsyncTask
通常會導(dǎo)致context
內(nèi)存泄漏馍资,因為它是非靜態(tài)嵌套類,所以不推薦使用同步調(diào)用关噪。以下例子使用https://reqres.in測試請求:
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private OkHttpClient mClient = new OkHttpClient();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textView);
String url = "https://reqres.in/api/users/2";
OkHttpHandler okHttpHandler = new OkHttpHandler();
okHttpHandler.execute(url);
}
private class OkHttpHandler extends AsyncTask<String, String, String> {
@Override
protected String doInBackground(String... params) {
Request request = new Request.Builder()
.url(params[0])
.build();
try {
Response response = mClient.newCall(request).execute();
return response.body().string();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
mTextView.setText(s);
}
}
}
- 異步調(diào)用
拋開同步調(diào)用鸟蟹,使用Call.enqueue(Callback)
和Activity.runOnUiThread(Runnable)
的方式是提交請求的最佳方案。其中Activity.runOnUiThread(Runnable)
方法傳入Runnable
色洞,這個Runnable
將插入到UI線程的事件隊列末尾戏锹,等待執(zhí)行run()
方法。以下例子使用https://reqres.in測試請求:
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
private OkHttpClient mClient = new OkHttpClient();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.textView);
String url = "https://reqres.in/api/users/2";
try {
get(url);
} catch (IOException e) {
e.printStackTrace();
}
}
private void get(String url) {
Request request = new Request.Builder()
.url(url)
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
call.cancel();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String responseString = response.body().string();
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText(responseString);
}
});
}
});
}
}
使用HttpUrl
HttpUrl
用于生成含參的URL火诸,以下例子使用https://resttesttest.com測試請求:
HttpUrl.Builder urlBuilder = HttpUrl.parse("https://httpbin.org/get").newBuilder();
urlBuilder.addQueryParameter("category", "android");
urlBuilder.addQueryParameter("title", "okhttp");
String url2 = urlBuilder.build().toString();
Header處理
- 設(shè)置請求頭
.header()
設(shè)置唯一的請求頭锦针,舊值會被替換。.addHeader()
新增請求頭置蜀,可以添加多值
Request request = new Request.Builder()
.url(url)
.addHeader("Accept","application/json; charset=utf-8")
.header("Accept","application/json; charset=utf-8")
.post(requestBody)
.build();
- 獲得響應(yīng)頭
.header()
返回單值奈搜,.headers()
返回多值的響應(yīng)頭
String headerString=response.header("Server");
List<String> headerStrings=response.headers("Vary");
Log.i(TAG,headerString);
Iterator<String> it=headerStrings.iterator();
while (it.hasNext()) {
Log.i(TAG,it.next());
}
Post提交String
以下例子使用https://reqres.in測試請求:
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String url = "https://reqres.in/api/users/";
String jsonString = "{\n" +
" \"name\": \"morpheus\",\n" +
" \"job\": \"leader\"\n" +
"}";
private void post(String url, final String requestString) {
RequestBody requestBody = RequestBody.create(JSON, requestString);
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
call.cancel();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String responseString = response.body().string();
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText(responseString);
}
});
}
});
}
Post提交表單
final String url = "https://tieba.baidu.com/f";
RequestBody requestBody = new FormBody.Builder()
.add("ie", "utf-8")
.add("kw", "minecraft")
.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Post提交文件
public static final MediaType JPEG = MediaType.parse("image/jpeg");
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM), "building.jpg");
RequestBody requestBody = RequestBody.create(JPEG, file);
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Post提交流
RequestBody requestBody = new RequestBody() {
@Nullable
@Override
public MediaType contentType() {
return null;
}
@Override
public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException {
bufferedSink.writeUtf8(requestString);
}
};
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
使用Gson解析response
String url = "https://api.github.com/gists/c2a7c39532239ff261be";
class Gist{
Map<String,GistFile> files;
}
class GistFile{
String content;
}
Gson gson = new Gson();
Gist gist = gson.fromJson(response.body().charStream(),Gist.class);
for(Map.Entry<String,GistFile> entry:gist.files.entrySet()){
Log.i(TAG,entry.getKey()+ " "+entry.getValue().content);
}
設(shè)置超時
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.build();
配置新client
.newBuilder()
會返回一個配置相同的buidler
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.build();
OkHttpClient client2 = client.newBuilder()
.connectTimeout(5, TimeUnit.SECONDS)
.build();
攔截器
攔截器(Interceptor
)是OkHttp的概念,也是核心功能盯荤。OkHttp有兩種攔截器馋吗,分別是應(yīng)用攔截器和網(wǎng)絡(luò)攔截器。攔截器的主要目的在于重寫request
和response
秋秤,可以在發(fā)出request
前修改headers或body宏粤,也可以在收到response
前修改headers或body。我們完全可以在用戶收到reponse
前將其修改成一個完全不一樣的新response
灼卢,這一功能使得我們可以進(jìn)行后續(xù)的緩存策略修改或是使用gzip壓縮requestBody等操作绍哎。應(yīng)用攔截器在用戶發(fā)出一次請求后的全過程中僅調(diào)用一次,而網(wǎng)絡(luò)攔截器可能因為重定向等問題多次調(diào)用鞋真,例如有一次重定向就會調(diào)用兩次崇堰。攔截器可以設(shè)置多個,并按添加順序進(jìn)行攔截涩咖。下圖來自OkHttp文檔:
兩種攔截器區(qū)別如下海诲,參考OkHttp文檔原文:
Application interceptors
- Don’t need to worry about intermediate responses like redirects and retries.
- Are always invoked once, even if the HTTP response is served from the cache.
- Observe the application’s original intent. Unconcerned with OkHttp-injected headers like
If-None-Match
.- Permitted to short-circuit and not call
Chain.proceed()
.- Permitted to retry and make multiple calls to
Chain.proceed()
.- Can adjust
Call
timeouts usingwithConnectTimeout
,withReadTimeout
,withWriteTimeout
.Network Interceptors
- Able to operate on intermediate responses like redirects and retries.
- Not invoked for cached responses that short-circuit the network.
- Observe the data just as it will be transmitted over the network.
- Access to the
Connection
that carries the request.
個人翻譯如下:
應(yīng)用攔截器
- 使用時不需要考慮例如重定向、重試等中轉(zhuǎn)請求帶來的影響
- 全過程只攔截一次檩互,即使攔截的
response
來自緩存- 可處理來自Applcation(參考攔截器圖解)的本意特幔。(例如
no-cache
)不涉及OkHttp的頭部注入例如If-None-Match
頭部(這是在core注入的)- 可以不調(diào)用
Chain.proceed()
(例如return
一個來自緩存的response
,但不能return null
)- 可以重試和多次調(diào)用
Chain.proceed()
- 可通過
withConnectTimeout
,withReadTimeout
,withWriteTimeout
調(diào)整Call
的超時時間.網(wǎng)絡(luò)攔截器
- 可處理例如重定向闸昨、重試等中轉(zhuǎn)請求
- 不涉及緩存的調(diào)用
- 可處理來自服務(wù)器的原始響應(yīng)
- 可對最終發(fā)出前的請求做讀寫
- 實現(xiàn)攔截器
以上所說攔截器可對處于中間時期的request
和response
做修改敬辣,就是在chain.proceed(request)
的前后完成的雪标。
chain.proceed(request)
會返回通過core或服務(wù)器處理后得到的response
,這個方法會阻塞線程溉跃。
String url = "http://publicobject.com/helloworld.txt";
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
//do something to rewrite request
long t1 = System.nanoTime();
Log.i(TAG,String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
Log.i(TAG,String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
//do something to rewrite response
return response;
}
}
- 設(shè)置應(yīng)用攔截器
兩種攔截器在實現(xiàn)的時候沒有區(qū)別村刨,充當(dāng)那種攔截器取決于調(diào)用的方法是.addInterceptor()
或是.addNetworkInterceptor()
。.addInterceptor()
表示設(shè)置應(yīng)用攔截器撰茎,.addNetworkInterceptor()
則是網(wǎng)絡(luò)攔截器嵌牺。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
- 設(shè)置網(wǎng)絡(luò)攔截器
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
緩存處理
OkHttp默認(rèn)不使用緩存,可以調(diào)用.cache()
開啟龄糊,但.cache()
僅能設(shè)置緩存區(qū)大小和緩存讀寫的位置逆粹。Cache-Control
頭部是Http協(xié)議定義的,而OkHttp完全遵循Http協(xié)議炫惩,所以O(shè)kHttp的緩存策略是由請求頭或響應(yīng)頭中的Cache-Control
頭部而定的僻弹。如果服務(wù)器返回的response
已經(jīng)帶有Cache-Control響應(yīng)頭,在buidler中調(diào)用.cache()
即可使用緩存他嚷。反之當(dāng)收到的response
沒有設(shè)置Cache-Control
時蹋绽,可以在攔截器里手動添加,不同參數(shù)對應(yīng)不同的緩存策略筋蓖。不論response
是否有Cache-Control
卸耘,始終可以在發(fā)出request
時添加例如Cache-control: no-cache
來控制緩存使用與否。
啟用緩存
String url = "http://publicobject.com/helloworld.txt";
int _10MB = 10 * 1024 * 1024;
File cacheDir = getCacheDir();
Cache cache = new Cache(cacheDir, _10MB);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
緩存策略
Http協(xié)議的Cache-Control
的參數(shù)有很多粘咖,可設(shè)置多個參數(shù)蚣抗,多個參數(shù)間用逗號分隔開。以下主要介紹其中幾種的含義
Cache-Control: max-age=3600
設(shè)置緩存過期時間為一小時瓮下。單位為秒翰铡,用于response
Cache-Control: max-stale=3600
表示接受使用過期的緩存,最長時間為過期后的一小時讽坏,單位為秒两蟀。用于request
Cache-control: no-cache
先不使用本地緩存,向服務(wù)器驗證緩存是否過期后決定緩存使用與否震缭,且并不取消本次response
的緩存。用于request
Cache-control: no-store
本次請求不緩存得到的response战虏,也表示本次請求不讀取緩存拣宰。用于request
Cache-control: only-if-cached
僅嘗試使用緩存。用于request
Cache-control: public
表明響應(yīng)可以被任何對象(包括:發(fā)送請求的客戶端烦感,代理服務(wù)器巡社,等等)緩存,即使是通常不可緩存的內(nèi)容手趣。(例如該響應(yīng)沒有max-age
指令或Expires
消息頭晌该, 該響應(yīng)對應(yīng)的請求方法是 POST )Cache-control: private
表明響應(yīng)只能被單個用戶緩存肥荔,不能作為共享緩存(即代理服務(wù)器不能緩存它)。私有緩存可以緩存響應(yīng)內(nèi)容朝群,比如:對應(yīng)用戶的本地瀏覽器燕耿。Cache-Control: min-fresh=3600
表示客戶端希望獲取一個能在指定的秒數(shù)內(nèi)保持其最新狀態(tài)的響應(yīng)。Cache-control: must-revalidate
一旦資源過期(比如已經(jīng)超過max-age
)誉帅,在成功向原始服務(wù)器驗證之前,緩存不能用該資源響應(yīng)后續(xù)請求蚜锨。
此外與緩存有關(guān)的header可能還有Expires
和Pragma
,這里暫不介紹
- 直接修改Cache-Control頭部定義緩存策略
class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
request = request.newBuilder()
.header("Cache-Control", "max-stale=3600")
.build();
return chain.proceed(request);
}
}
Interceptor interceptor = new CacheInterceptor();
mClient = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.build();
- 使用CacheControl.Builder()定義緩存策略
CacheControl
類只能在攔截器中使用亚再,其實質(zhì)只是在請求頭或響應(yīng)頭為Cache-Control
添加不同的參數(shù)而已晨抡,并沒有其他作用
class ForceCacheInterceptor implements Interceptor {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
CacheControl cacheControl = new CacheControl.Builder()
.onlyIfCached()
.build();
request = request.newBuilder()
.cacheControl(cacheControl)
.build();
return chain.proceed(request);
}
}
Interceptor interceptor = new ForceCacheInterceptor();
mClient = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.build();
CacheControl.Builder的常用方法
- .maxAge(3600, TimeUnit.SECONDS);
- .maxStale(3600, TimeUnit.SECONDS);
- .noCache();
- .noStore();
- .onlyIfCached();
含義參考Cache-Control
參數(shù)介紹
- 使用CacheControl的伴生對象定義緩存策略
CacheControl
的伴生對象有兩個,CacheControl.FORCE_CACHE
和CacheControl.FORCE_NETWORK
圆雁,分別表示強(qiáng)制使用緩存和強(qiáng)制使用網(wǎng)絡(luò)。
public class ForceNetworkInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder builder = chain.request().newBuilder();
if (!NetworkUtils.internetAvailable()) {
builder.cacheControl(CacheControl.FORCE_NETWORK);
}
return chain.proceed(builder.build());
}
}
Interceptor interceptor = new ForceNetworkInterceptor();
mClient = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.build();
CacheControl.FORCE_CACHE
的本質(zhì)是將Cache-Control
請求頭設(shè)為"max-stale=2147483647, only-if-cached"
伪朽。
重新查看上圖就可以知道汛蝙,Application發(fā)出的請求是被OkHttp core處理,而OkHttp core發(fā)出的請求將提交給服務(wù)器窖剑,如果我們希望本次請求強(qiáng)制使用緩存坚洽,就應(yīng)該使用應(yīng)用攔截器而不是網(wǎng)絡(luò)攔截器,這段請求頭告訴OkHttp本次請求僅使用緩存的響應(yīng)西土。
CacheControl.FORCE_NETWORK
的本質(zhì)是將Cache-Control請求頭設(shè)為"no-cache"
讶舰。與FORCE_CACHE
同理,它也應(yīng)該使用應(yīng)用攔截器需了,這段請求頭告訴OkHttp本次請求僅使用來自網(wǎng)絡(luò)的響應(yīng)跳昼。
緩存流程
決定一次請求是否使用緩存的流程,主要的幾個步驟如下(任何一步?jīng)Q定使用網(wǎng)絡(luò)時將不再檢查后續(xù)步驟)
檢查
response
是否包含Date
肋乍,Expires
鹅颊,Last-Modified
,ETag
墓造,Age
這些響應(yīng)頭堪伍。若都不包含則使用網(wǎng)絡(luò)锚烦。檢查
request
的Cache-Control
請求頭,"no-cache"
使用網(wǎng)絡(luò)帝雇,"only-if-cached"
使用緩存涮俄。檢查
response
的ETag
響應(yīng)頭,若存在則使用網(wǎng)絡(luò)摊求,并且本次請求會帶有與ETag
值相同的If-None-Match
請求頭禽拔。若實際數(shù)據(jù)沒有變化,服務(wù)器處理后會給出304 Not Modified狀態(tài)碼室叉,表示資源沒有修改睹栖,并且不會返回body,指示客戶端使用緩存茧痕,所以此時OkHttp也會使用緩存野来。檢查
response
的Last-Modified
響應(yīng)頭,若存在則使用網(wǎng)絡(luò)踪旷,并且本次請求會帶有與Last-Modified
值相同的If-Modified-Since
請求頭曼氛。后續(xù)同ETag
。檢查
response
的Date
響應(yīng)頭令野,若存在則使用網(wǎng)絡(luò)舀患,并且本次請求會帶有與Date
值相同的If-Modified-Since
請求頭。后續(xù)同ETag
气破。檢查
response
的max-age
,如果過期則使用網(wǎng)絡(luò)现使,否則使用緩存。但也可能因為其他參數(shù)如max-stale
等影響最終計算結(jié)果顽冶。
緩存總結(jié)
一次完整的涉及緩存的網(wǎng)絡(luò)請求大致如下圖强重,其中成功的結(jié)果有兩個(綠框)间景,分別是使用緩存和使用服務(wù)器的新數(shù)據(jù)。在Force cache后找不到緩存就會失敱取(紅框)。從初始階段向下看哮缺,第一步判斷是否調(diào)用.cache()
開啟了緩存功能甲喝。第二步檢查之前是否緩存過埠胖,兩者任意一者不滿足則使用網(wǎng)絡(luò)直撤。第三步判斷是否需要驗證,與ETag
等有關(guān)红柱,存在則使用網(wǎng)絡(luò)向服務(wù)器驗證锤悄,服務(wù)器若返回304則response
完全從緩存中取出零聚。這步操作同普通請求一樣握牧,可能涉及無網(wǎng)絡(luò)問題沿腰。當(dāng)無網(wǎng)絡(luò)時可以Force cache進(jìn)行處理狈定,最后則是成功或失敗時的異常處理纽什。下圖來自Medium
- 當(dāng)存儲緩存時
如果此時要修改response
的頭部企巢,應(yīng)該使用網(wǎng)絡(luò)攔截器修改response
让蕾。
只要在構(gòu)建client
的時候調(diào)用了.cache()
,那么通過這個client
得到的響應(yīng)一定會被緩存誉裆,但之后不一定會被使用缸濒。存儲緩存時與Cache-Control
請求頭或響應(yīng)頭都無關(guān)庇配,Cache-Control
只有當(dāng)讀取緩存時才會用到讨永。
- 當(dāng)讀取緩存時
應(yīng)該使用應(yīng)用攔截器修改request
卿闹。
想要強(qiáng)制使用緩存,有以下3種方式:
- 使用
CacheControl.FORCE_CACHE
- 調(diào)用
CacheControl.Builder().onlyIfCached()
- 直接添加
Cache-Control: "only-if-cached"
請求頭
但如果緩存不存在著角,這次請求就會失敗并拋出IOException
吏口,并且得到一個帶有504 Gateway Timeout的response
产徊。
想要強(qiáng)制使用網(wǎng)絡(luò)舟铜,有以下3種方式:
- 使用
CacheControl.FORCE_NETWORK
- 調(diào)用
CacheControl.Builder().noCache()
- 直接添加
Cache-Control: "no-cache"
請求頭
如果你在請求頭沒有指定任何有關(guān)緩存的參數(shù)谆刨,OkHttp將按照緩存中response
的數(shù)個響應(yīng)頭進(jìn)行不同的處理痊夭,可能使用緩存脏里,也可能向服務(wù)器驗證response
后決定是否使用緩存,或是進(jìn)行一次普通的請求根吁。