Okhttp3 是我们经常使用的一个网络框架,可扩展性强,支持 get 缓存, spdy、http2.0,gzip 压缩减少数据流量,同步和异步请求,连接池复用机制等特性让广大 android 开发者深爱不已,今天我就带大家从 Okhttp 简单使用,到各种好用拦截器原理了解 Okhttp3
一. OkHttp3使用指南
1.1 引入
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okio:okio:2.9.0'
清单文件注册一下权限:
<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" />
1.2 OkHttp3 使用方式
- 构建客户端对象OkHttpClient
- 构建请求Request
- 生成Call对象
- Call发起请求(同步/异步)
1.2.1 OkHttp3 post 请求
1.2.1.1 post 请求提交String文件
get
和 post
请求 的区别是 在构造Request
对象时,需要多构造一个RequestBody
对象,用它来携带我们要提交的数据,其他都是一样的。
OkHttpClient httpClient = new OkHttpClient(); MediaType contentType = MediaType.Companion.parse("text/x-markdown; charset=utf-8"); String content = "hello!"; RequestBody body = RequestBody.Companion.create(content,contentType); 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(@NotNull Call call, @NotNull IOException e) { } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { Log.i("MainActivity", "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" + Objects.requireNonNull(response.body()).string()); } }); }
构建Request时的get()改成post(body),并传入RequestBody实例。RequestBody实例是通过create方法创建,需要指定请求体内容类型、请求体内容。这里是传入了一个指定为markdown格式的文本
1.2.1.2 post 请求提交复杂请求体
复杂请求体可以同时包含多种类型的的请求体数据。
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()); } });
1.2.2 OkHttp3 get 请求
同步请求
// 创建 OkHttpClient 实例 OkHttpClient httpClient = new OkHttpClient(); // 将 MicroKibaco github 实例 url 传入Request.Builder 实例 String url = "https://github.com/MicroKibaco"; Request getRequest = new Request.Builder() .url(url) .get() .build(); // httpClient.newCall 传入 Request实例生成call Call call = httpClient.newCall(getRequest); //同步请求,要放到子线程执行 new Thread(new Runnable() { @Override public void run() { try { Response response = call.execute(); Log.i(TAG, "okHttpGet run: response:"+ response.body().string()); } catch (IOException e) { e.printStackTrace(); } } }).start();
异步请求
OkHttpClient httpClient = new OkHttpClient(); String url = "https://www.baidu.com/"; Request getRequest = new Request.Builder() .url(url) .get() .build(); Call call = httpClient.newCall(getRequest); call.enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { Log.i("MainActivity", "okHttpGet run: response:"+ Objects.requireNonNull(response.body()).string()); } }); }
取消请求
每一个Call
只能执行一次。 如果想要取消正在执行的请求,可以使用call.cancel()
,通常在离开页面时都要取消执行的请求的。
结果处理
- onFailure
connection连接失败或读写超时 - onResponse
成功的从服务器获取到了结果,但是这个结果的response.code()可能是404、500等,也可能就是200 - response.code() 200
表示应用层请求成功了。此时我们可以获取Response的ResponseBody,这是响应体。从面看到,可以从ResponseBody获取string、byte[]、InputStream,这样就可以对结果进行很多操作了,比如UI上展示string(要用Handler切换到UI线程)、通过InputStream写入文件等等
1.3 OkHttp3 请求配置项
- 如何全局设置超时时长?
- 缓存位置、最大缓存大小 呢?
- 考虑有这样一个需求,我要监控App通过 OkHttp 发出的 所有 原始请求,以及整个请求所耗费的时间,如何做?
OkHttpClient client = new OkHttpClient.Builder() // 设置了连接、读取、写入的超时时长 .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) // cache()方法传入了由缓存目录、缓存大小构成的Cache实例 .cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024)) .addInterceptor(new Interceptor() { // intercept()方法会在开始执行请求时调用。其中chain.proceed(request)内部是真正请求的过程,是阻塞操作,执行完后会就会得到请求结果ResponseBody @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()); // chain.proceed(request)的前后取当前时间,那么就知道整个请求所耗费的时间。上面chain.proceed(request)的前后分别打印的日志和时间 Response response = chain.proceed(request); ResponseBody body = response.body(); Log.i(TAG, "intercept: proceed end: url"+ url+ ", at "+System.currentTimeMillis()); return response; } }) .build();
单独配置
Request getRequest = new Request.Builder() .url("http://yun918.cn/study/public/file_upload.php") .post(multipartBody) .addHeader("key","value") .cacheControl(CacheControl.FORCE_NETWORK) .build();
二. OkHttp3 重点 API 介绍
2.1 RealCall
RealCall作用:
RealCall是 OKHttp 真正的请求执行者,有两个比较重要的方法: execute 同步方法 和 enqueue 异步 方法,我们来看看同步方法是怎么做的吧
首先检查这个 call 是否已经被执行了,每个 call 只能被执行一次,如果想要一个完全一样的 call ,可以利用 clone 方法进行克隆;
然后利用 client.dispatcher().executed(this) 来进行实际执行
首先,它会检查这个 call 是否已经被执行了,每个 call 只能被执行一次,如果想要一个完全一样的 call ,可以利用 clone 方法进行克隆
再调用 getResponseWithInterceptorChain() 函数获取 HTTP 返回结果,从函数名可以看出,这一步还会进行一系列“拦截”操作。
最后将涉及到 dispatcher 的内容只不过是告知它我们的执行状态加入到running队列,
- 开始执行了(调用 executed )
- 执行完毕了(调用 finished )
我们在来看看异步方法是怎么做的吧
在子线程中,asyncCall是线程,UI需要切换线程
真正任务执行是在调用我们的 dispatcher.equeue
如果中的 runningAsynCalls 不满,且 call 占用的 host 小于最大数量,则将 call 加入到 runningAsyncCalls 队列中执行,同时利用线程池执行 call ;
否则将 call 加入到 readyAsyncCalls 中,这也符合生产者消费者模式双端队列模型设计
2.2 OKHttpClient
OKHttpClient是OKHttp的 流程的总控制者 ,通过Builder模式配置我们的参数如: http协议, http版本,缓存socketfactory针对的是http还是 socket ;端口80还是443;dns等等
三. OkHttp3 工作流程
3.1 总的大纲:
用了 责任链设计模式 ,它将请求一层一层向下传,直到有一层能够得到Resposne就停止向下传递,并 Response 向上面的拦截器传递,然后各个拦截器会对 respone 进行一些处理,最后会传到 RealCall 类中通过 execute 来得到 Response 。
- 3.1.1 通过 OkHttpClient 实例化 初始化我们需要初始化的 http连接协议 代理地址 缓存控制 Cookie 证书连接 代理选择器等
- 3.1.2 通过 Request Builder模式 拿到我们传入的 url method Headers RequestBody 以及 tags
- 在 OkHttpClient 我们用RealCall接口回调执行Dispacher的 回调实现,入: equeqe(同步) ennque(异步请求) 其中 Dispacher 的同步请求 是 将我们的请求塞到同步队列里面,这个有可能会导致线程阻塞 ennque(异步请求) 是 生产者消费者模型 会将满足 小于并发64 同host请求小于4 的请求塞到我们的 运行队列里面 执行,不满足该条件放到 等待队列里面
然后交给线程池 去执行具体请求事宜 ,拿到响应后
- 3.1.3 我们 会将 响应 通过 getResourceChainPain 通过拦截器网下透传到 重定向拦截器, 桥拦截器 ,缓存拦截器 ,连接池拦截器,网络拦截器,请求拦截器 如果中间有被拦截,就进行一系列的拦截处理,追钟会返回我们客户端,我们客户端再通过 Response 去解析 code message Header ReponseBody 这些参数信息
四. OkHttp3 拦截器
拦截器按照职责主要分为RetryAndFollowUpInterceptor,BridgeInterceptor,CacheInterceptor,ConnectInterceptor,NetworkInterceptors这几类。 当我们执行同步或异步请求的时候,所有的拦截器会被添加到RealInterceptorChain这个类里面
当我们实例化实例化下一个拦截器对应的 RealIterceptorChain 对象,这个对象会在传递给当前的拦截器
然后得到当前的拦截器: interceptors 是存放拦截器的 ArrayList
调用当前拦截器的 intercept() 方法,并将下一个拦截器的 RealIterceptorChain 对象传递下去
除了在client中自己设置的 interceptor ,第一个调用的就是 retryAndFollowUpInterceptor
4.1 RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor 主要的职责是: 负责失败重试以及重定向,一次okhttp请求,是一个个拦截器执行interrept获取到response,对response处理再传给上一个Interrupt,重试最大次数20,超过的话释放资源
4.2 CacheInterceptor
4.2.1 Http缓存
http缓存分为两种:一种是强制缓存,另一种是对比缓存,字段表示为: Cache-Control
对比缓存标识字段是
实际开发中服务端可以控制缓存,然后交给客户端控制缓存时间
- 第一请求:
- 第二次请求:
4.2.2 okHttp 缓存
首先根据 request 来判断 cache 中是否有缓存的 response
if 有,得到这个 response
然后进行判断当前 response 是否有效
if 没有将 cacheCandate 赋值为空。
根据request判断缓存的策略,是否要使用了网络,缓存 或两者 都使用
调用下一个拦截器,决定从网络上来得到 response
如果本地已经存在 cacheResponse ,那么让它和网络得到的 networkResponse 做比较,决定是否来更新缓存的 cacheResponse
缓存未经缓存过的 response,我们这么处理
4.3 ConnectInterceptor
了解ConnectInterceptor前,我们得了解一下什么是连接复用
当我们打开一个新的连接,写入http请求数据的时候,读取响应数据,释放连接这个过程中,我们再释放连接我们可以复用connection写入http请求数据,这就是复用原理
接下来我们看看 ConnectInterceptor 的 原理吧,这里给大家总结了一份原理图
- 4.3.1 判断 streamAllocation 对象内部是否有可复用的 connection 对象
- 4.3.2 如果 streamAllocation 对象无可用的 connection 对象,则通过 Internal.instance 从连接池中查找可用的 connection 对象;
- 4.3.3 如果连接池中仍未找到,则遍历所有路由路径,尝试再次从 ConnectionPool 中寻找可复用的连接;
4.3.4 前面三步都未找到,则新建一个连接,进行 TCP+ TLS 握手
- 4.3.5 将新建的连接放入连接池中,并返回结果
4.4 CallServerInterceptor
正地去进行网络IO读写了——写入http请求的header和body数据、读取响应的header和body。
- 检查请求方法,用 Httpcode 处理 request
- 进行网络请求得到 response
- 返回 response
4.5 自定义拦截器
- 异常上报
- 异地登陆,根据返回 code 处理
- 添加 cookie
五. OkHttp的优点
- 5.1 无缝支持GZIP来减少数据流量 ,发送的数据和接受的收据在传递过程中都是经过gzip压缩的,BridgeIntercept的intercept方法中设置的
- 5.2 通过连接池来减少请求延时 在okhttp中,我们每次的request请求都可以理解为一个connection,而每次发送请求的时候我们都要经过tcp的三次握手,然后传输数据,最后在释放连接。在高并发或者多个客户端请求的情况下,多次创建就会导致性能低下。如果能够connection复用的话,就能够很好地解决这个问题了。能够复用的关键就是客户端和服务端能够保持长连接,并让一个或者多个连接复用。
- 5.3 在BridgeInterceptor的 intercept() 方法中requestBuilder.header("Connection", "Keep-Alive"),我们在request的请求头添加了("Connection", "Keep-Alive")的键值对,这样就能够保持长连接。而连接池ConnectionPoll就是专门负责管理这些长连接的类。需要注意的是,我们在初始化 ConnectionPoll的时候,会设置 闲置的connections 的最大数量为5个,每个最长的存活时间为5分钟。
- 5.4 通过连接池来减少请求延时 在okhttp中,我们每次的request请求都可以理解为一个connection,而每次发送请求的时候我们都要经过tcp的三次握手,然后传输数据,最后在释放连接。在高并发或者多个客户端请求的情况下,多次创建就会导致性能低下。如果能够connection复用的话,就能够很好地解决这个问题了。能够复用的关键就是客户端和服务端能够保持长连接,并让一个或者多个连接复用。
六 . OKHttp 缓存策阅
- GET 请求
- Post 请求