PREVIEW
- 导入
- 简单使用
- 使用HttpUrl
- Header处理
- Post提交String
- Post提交表单
- Post提交文件
- Post提交流
- 使用Gson解析response
- 设置超时
- 配置新client
- 拦截器
应用拦截器
网络拦截器
两者区别- 缓存处理
缓存策略
缓存流程
缓存总结
导入
在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
是负责管理多个Call
的组织者,而每个Call
又包含一个Request
和Response
,并且Call
中的回调用于提供响应结果。要完成一次网络请求,我们需要告诉Call
需要处理的Request
是什么样的,例如它的URL是什么,然后将Call
交给OkHttpClient
。OkHttpClient
仅对本次请求做一些配置,例如指定缓存路径,它会让Dispatcher
去决定何时执行Call
。而Dispatcher
的底层实现就是一个由OkHttp默认实现的线程池,它将最终执行Call
中的.run()
方法。最后的Interceptor
和Chain
将用于数据的拦截处理。OkHttp提供两种方式提交网络请求,分别是Call.execute()
和Call.enqueue(Callback)
,前者会阻塞线程,后者加入队列异步执行。通过调用response.body().string()
我们可以得到响应的body部分并以String
形式返回,但值得注意的是.string()
只能调用一次。
- 同步调用
一般来说要在得到结果的第一时间修改UI,我们可能会使用Call.execute()
和AsyncTask
完成提交请求。但AsyncTask
通常会导致context
内存泄漏,因为它是非静态嵌套类,所以不推荐使用同步调用。以下例子使用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);
}
}
}
- 异步调用
抛开同步调用,使用Call.enqueue(Callback)
和Activity.runOnUiThread(Runnable)
的方式是提交请求的最佳方案。其中Activity.runOnUiThread(Runnable)
方法传入Runnable
,这个Runnable
将插入到UI线程的事件队列末尾,等待执行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处理
- 设置请求头
.header()
设置唯一的请求头,旧值会被替换。.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();
- 获得响应头
.header()
返回单值,.headers()
返回多值的响应头
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);
}
设置超时
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有两种拦截器,分别是应用拦截器和网络拦截器。拦截器的主要目的在于重写request
和response
,可以在发出request
前修改headers或body,也可以在收到response
前修改headers或body。我们完全可以在用户收到reponse
前将其修改成一个完全不一样的新response
,这一功能使得我们可以进行后续的缓存策略修改或是使用gzip压缩requestBody等操作。应用拦截器在用户发出一次请求后的全过程中仅调用一次,而网络拦截器可能因为重定向等问题多次调用,例如有一次重定向就会调用两次。拦截器可以设置多个,并按添加顺序进行拦截。下图来自OkHttp文档:
两种拦截器区别如下,参考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.
个人翻译如下:
应用拦截器
- 使用时不需要考虑例如重定向、重试等中转请求带来的影响
- 全过程只拦截一次,即使拦截的
response
来自缓存- 可处理来自Applcation(参考拦截器图解)的本意。(例如
no-cache
)不涉及OkHttp的头部注入例如If-None-Match
头部(这是在core注入的)- 可以不调用
Chain.proceed()
(例如return
一个来自缓存的response
,但不能return null
)- 可以重试和多次调用
Chain.proceed()
- 可通过
withConnectTimeout
,withReadTimeout
,withWriteTimeout
调整Call
的超时时间.网络拦截器
- 可处理例如重定向、重试等中转请求
- 不涉及缓存的调用
- 可处理来自服务器的原始响应
- 可对最终发出前的请求做读写
- 实现拦截器
以上所说拦截器可对处于中间时期的request
和response
做修改,就是在chain.proceed(request)
的前后完成的。
chain.proceed(request)
会返回通过core或服务器处理后得到的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;
}
}
- 设置应用拦截器
两种拦截器在实现的时候没有区别,充当那种拦截器取决于调用的方法是.addInterceptor()
或是.addNetworkInterceptor()
。.addInterceptor()
表示设置应用拦截器,.addNetworkInterceptor()
则是网络拦截器。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
- 设置网络拦截器
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
缓存处理
OkHttp默认不使用缓存,可以调用.cache()
开启,但.cache()
仅能设置缓存区大小和缓存读写的位置。Cache-Control
头部是Http协议定义的,而OkHttp完全遵循Http协议,所以OkHttp的缓存策略是由请求头或响应头中的Cache-Control
头部而定的。如果服务器返回的response
已经带有Cache-Control响应头,在buidler中调用.cache()
即可使用缓存。反之当收到的response
没有设置Cache-Control
时,可以在拦截器里手动添加,不同参数对应不同的缓存策略。不论response
是否有Cache-Control
,始终可以在发出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协议的Cache-Control
的参数有很多,可设置多个参数,多个参数间用逗号分隔开。以下主要介绍其中几种的含义
Cache-Control: max-age=3600
设置缓存过期时间为一小时。单位为秒,用于response
Cache-Control: max-stale=3600
表示接受使用过期的缓存,最长时间为过期后的一小时,单位为秒。用于request
Cache-control: no-cache
先不使用本地缓存,向服务器验证缓存是否过期后决定缓存使用与否,且并不取消本次response
的缓存。用于request
Cache-control: no-store
本次请求不缓存得到的response,也表示本次请求不读取缓存。用于request
Cache-control: only-if-cached
仅尝试使用缓存。用于request
Cache-control: public
表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如该响应没有max-age
指令或Expires
消息头, 该响应对应的请求方法是 POST )Cache-control: private
表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。Cache-Control: min-fresh=3600
表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。Cache-control: must-revalidate
一旦资源过期(比如已经超过max-age
),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
此外与缓存有关的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
类只能在拦截器中使用,其实质只是在请求头或响应头为Cache-Control
添加不同的参数而已,并没有其他作用
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
参数介绍
- 使用CacheControl的伴生对象定义缓存策略
CacheControl
的伴生对象有两个,CacheControl.FORCE_CACHE
和CacheControl.FORCE_NETWORK
,分别表示强制使用缓存和强制使用网络。
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
的本质是将Cache-Control
请求头设为"max-stale=2147483647, only-if-cached"
。
重新查看上图就可以知道,Application发出的请求是被OkHttp core处理,而OkHttp core发出的请求将提交给服务器,如果我们希望本次请求强制使用缓存,就应该使用应用拦截器而不是网络拦截器,这段请求头告诉OkHttp本次请求仅使用缓存的响应。
CacheControl.FORCE_NETWORK
的本质是将Cache-Control请求头设为"no-cache"
。与FORCE_CACHE
同理,它也应该使用应用拦截器,这段请求头告诉OkHttp本次请求仅使用来自网络的响应。
缓存流程
决定一次请求是否使用缓存的流程,主要的几个步骤如下(任何一步决定使用网络时将不再检查后续步骤)
检查
response
是否包含Date
,Expires
,Last-Modified
,ETag
,Age
这些响应头。若都不包含则使用网络。检查
request
的Cache-Control
请求头,"no-cache"
使用网络,"only-if-cached"
使用缓存。检查
response
的ETag
响应头,若存在则使用网络,并且本次请求会带有与ETag
值相同的If-None-Match
请求头。若实际数据没有变化,服务器处理后会给出304 Not Modified状态码,表示资源没有修改,并且不会返回body,指示客户端使用缓存,所以此时OkHttp也会使用缓存。检查
response
的Last-Modified
响应头,若存在则使用网络,并且本次请求会带有与Last-Modified
值相同的If-Modified-Since
请求头。后续同ETag
。检查
response
的Date
响应头,若存在则使用网络,并且本次请求会带有与Date
值相同的If-Modified-Since
请求头。后续同ETag
。检查
response
的max-age
,如果过期则使用网络,否则使用缓存。但也可能因为其他参数如max-stale
等影响最终计算结果。
缓存总结
一次完整的涉及缓存的网络请求大致如下图,其中成功的结果有两个(绿框),分别是使用缓存和使用服务器的新数据。在Force cache后找不到缓存就会失败(红框)。从初始阶段向下看,第一步判断是否调用.cache()
开启了缓存功能。第二步检查之前是否缓存过,两者任意一者不满足则使用网络。第三步判断是否需要验证,与ETag
等有关,存在则使用网络向服务器验证,服务器若返回304则response
完全从缓存中取出。这步操作同普通请求一样,可能涉及无网络问题。当无网络时可以Force cache进行处理,最后则是成功或失败时的异常处理。下图来自Medium
- 当存储缓存时
如果此时要修改response
的头部,应该使用网络拦截器修改response
。
只要在构建client
的时候调用了.cache()
,那么通过这个client
得到的响应一定会被缓存,但之后不一定会被使用。存储缓存时与Cache-Control
请求头或响应头都无关,Cache-Control
只有当读取缓存时才会用到。
- 当读取缓存时
应该使用应用拦截器修改request
。
想要强制使用缓存,有以下3种方式:
- 使用
CacheControl.FORCE_CACHE
- 调用
CacheControl.Builder().onlyIfCached()
- 直接添加
Cache-Control: "only-if-cached"
请求头
但如果缓存不存在,这次请求就会失败并抛出IOException
,并且得到一个带有504 Gateway Timeout的response
。
想要强制使用网络,有以下3种方式:
- 使用
CacheControl.FORCE_NETWORK
- 调用
CacheControl.Builder().noCache()
- 直接添加
Cache-Control: "no-cache"
请求头
如果你在请求头没有指定任何有关缓存的参数,OkHttp将按照缓存中response
的数个响应头进行不同的处理,可能使用缓存,也可能向服务器验证response
后决定是否使用缓存,或是进行一次普通的请求。