导入
在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文档原文:
个人翻译如下:
- 实现拦截器
以上所说拦截器可对处于中间时期的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
的参数有很多,可设置多个参数,多个参数间用逗号分隔开。以下主要介绍其中几种的含义
此外与缓存有关的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();
含义参考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种方式:
但如果缓存不存在,这次请求就会失败并抛出IOException
,并且得到一个带有504 Gateway Timeout的response
。
想要强制使用网络,有以下3种方式:
如果你在请求头没有指定任何有关缓存的参数,OkHttp将按照缓存中response
的数个响应头进行不同的处理,可能使用缓存,也可能向服务器验证response
后决定是否使用缓存,或是进行一次普通的请求。
最后
配合各种资料辅助学习
在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了
很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘
如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。
2020最新上万页的大厂面试真题
七大模块学习资料:如NDK模块开发、Android框架体系架构...
只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。
复制链接查看隐藏内容:https://shimo.im/docs/QdyGqGHXX8PyQ8pw
最后
有段话想分享给大家:
“如果你热爱,那么请继续热爱,你的付出终将获得与之匹配的回报,如果眼前觉得没有希望,不妨再坚持一会,‘今天很残酷,明天也很残酷,但是后天很美好’ ‘冬天都已经来了,春天还会远吗?’ ”
道理就是这个道理,但是“大道理大家都懂”,而那些成功的人,就是把这些道理运用到了工作和生活当中。
共勉!