OkHttp 面试题汇总
一、前言
如今面试中高级开发工程师岗位,OKhttp 原理是必问环节,只会使用已经无法满足 Android 开发市场的需求,优秀的第三方框架源码剖析不仅能深度理解框架,也能对自己学习带来很大的帮助。
本篇文章根据朋友反馈和亲身经历简单整理的一些关于 Okhttp 常见面试题目。
1.Okhttp 基本实现原理
OkHttp 主要是通过 5 个拦截器和 3 个双端队列(2 个异步队列,1 个同步队列)工作。内部实现通过一个责任链模式完成,将网络请求的各个阶段封装到各个链条中,实现了各层的解耦。
OkHttp 的底层是通过 Socket 发送 HTTP 请求与接受响应,但是 OkHttp 实现了连接池的概念,即对于同一主机的多个请求,可以公用一个 Socket 连接,而不是每次发送完 HTTP 请求就关闭底层的 Socket,这样就实现了连接池的概念。而 OkHttp 对 Socket 的读写操作使用的 OkIo 库进行了一层封装。
执行流程:
- 通过构建者构建出OkHttpClient对象,再通过newCall方法获得RealCall请求对象.
- 通过RealCall发起同步或异步请求,而决定是异步还是同步请求的是由线程分发器dispatcher来决定.
- 当发起同步请求时会将请求加入到同步队列中依次执行,所以会阻塞UI线程,需要开启子线程执行.
- 当发起异步请求时会创建一个线程池,并且判断请求队列是否大于最大请求队列64,请求主机数是否大于5,如果大于请求添加到异步等待队列中,否则添加到异步执行队列,并执行任务.
2.Okhttp 网络缓存如何实现?
OKHttp 默认只支持 get 请求的缓存。
- 第一次拿到响应后根据头信息决定是否缓存。
- 下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
- 如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
3.Okhttp 网络连接怎么实现复用?
HttpEngine 在发起请求之前,会先调用nextConnection()来获取一个Connection对象,如果可以从ConnectionPool中获取一个Connection对象,就不会新建,如果无法获取,就会调用createnextConnection()来新建一个Connection对象,这就是 Okhttp 多路复用的核心,不像之前的网络框架,无论有没有,都会新建Connection对象。
20210312101552861.png4.Dispatcher 的功能是什么?
Dispatcher中文是分发器的意思,和拦截器不同的是分发器不做事件处理,只做事件流向。他负责将每一次Requst进行分发,压栈到自己的线程池,并通过调用者自己不同的方式进行异步和同步处理。 通俗的讲就是主要维护任务队列的作用。
- 记录同步任务、异步任务及等待执行的异步任务。
- 调度线程池管理异步任务。
- 发起/取消网络请求 API:execute、enqueue、cancel。
Dispatcher 类,该类中维护了三个双端队列(Deque):
readyAsyncCalls:准备运行的异步请求
runningAsyncCalls:正在运行的异步请求
runningSyncCalls:正在运行的同步请求
OkHttp 设置了默认的最大并发请求量 maxRequests = 64 和单个 Host 主机支持的最大并发量 maxRequestsPerHost = 5
5.addInterceptor 与 addNetworkInterceptor 的区别?
二者通常的叫法为应用拦截器和网络拦截器,从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。
1、首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
2、其次,如上文提到除了CallServerInterceptor,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。
3、最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。
6、Okhttp 拦截器的作用是什么?
1、应用拦截器
拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
- RetryAndFollowUpInterceptor 处理错误重试和重定向
- BridgeInterceptor 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
- CacheInterceptor 缓存拦截器,如果命中缓存则不会发起网络请求。
- ConnectInterceptor 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
2、网络拦截器
用户自定义拦截器,通常用于监控网络层的数据传输。
- CallServerInterceptor 请求拦截器,在前置准备工作完成后,真正发起了网络请求。
7、Okhttp 有哪些优势?
- 支持 http2,对一台机器的所有请求共享同一个 Socket
- 内置连接池,支持连接复用,减少延迟
- 支持透明的 gzip 压缩响应体
- 响应缓存可以完全避免网络重复请求
- 请求失败时自动重试主机的其他 ip,自动重定向
- 丰富的 API,可扩展性好
8、response.body().string() 为什么只能调用一次?
我们可能习惯在获取到Response对象后,先response.body().string()打印一遍 Log,再进行数据解析,却发现第二次直接抛异常,其实直接跟源码进去看就发现,通过source拿到字节流以后,直接调用closeQuietly()方法关闭了,这样第二次再去通过source读取就直接流已关闭的异常了。
public final String string() throws IOException {
BufferedSource source = source();
try {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
} finally {
//这里讲resource给悄悄close了
Util.closeQuietly(source);
}
}
解决方案:
1.内存缓存一份response.body().string();
2.自定义拦截器处理 Log。
9、OkHttp请求整体流程是怎么样?
- Request-》OkHttpClient-》RealCall
- 同步 -》 在调用线程 执行五大拦截器
- 异步 -》 使用分发器将任务在线程池执行 五大拦截器
var okHttpClient = OkHttpClient.Builder().build()
var request = Request.Builder().url("https://www.baidu.com")
.cacheControl(CacheControl.FORCE_CACHE)
.build()
var call = okHttpClient.newCall(request)
val result = call.execute()
println(result.isSuccessful)
result.close()
280da08ecf58408515783ab19d468ec.png
- 分发器:
内部维护队列与线程池,完成请求调配;
- 拦截器:
完成整个请求过程。
10、分发器是如何工作的?
对于同步请求,分发器只记录请求,用于判断IdleRunnable是否需要执行;
对于异步请求,向分发器中提交请求;
- 同步-》记录同步任务:RealCall
- 异步-》首先将任务加入ready队列等待执行 -》是否需要将ready中的任务放入running 执行
- 同时请求的异步任务数不得大于64个
- 从ready中取出来的异步任务,与其相同的HOST,不得大于5个
- 若已经存在5个相同HOST的任务在执行,则继续从ready中检查下一个等待任务
Q:如何决定将请求放入ready还是running?
A:如果当前正在请求数为64,则将请求放入ready等待执行;如果小于64,但是已经存在同一域名主机的请求5个,也会放入ready;否则放入running队列立即执行;
Q:从ready移动running的条件是什么?
A:每个请求执行完成就会从running移除,同时进行第一步相同逻辑的判断,决定是否移动!
11、拦截器是如何工作的?
- 责任链设计模式 将请求者 与 执行者 解耦
- 让请求者 只需要将请求发给责任链即可,无需关系请求过程与细节。
- 重试重定向、桥接、缓存、连接、请求服务
12、应用拦截器与网络拦截器的区别?
OkHttp中拦截器有:自定义应用拦截器
、重试重定向
、桥接
、缓存
、连接
、自定义网络拦截器
、请求服务
。
自定义应用拦截器与自定义网络拦截器的区别主要是顺序的区别,由于以上拦截器采用责任链设计模式组合执行,因此顺序不同,带来的影响是:
应用拦截器不需要关心是否重定向或者失败重连(只会执行一次);同时它也能决定是否执行其他拦截器
。而网络拦截器则可以操作重定向与重试并且可能不会执行(直接在缓存中获得结果),同时它也可以观察到真正的Request以及相关的连接信息(经过其他拦截器处理完毕后
)。
13、OkHttp缓存机制
-
OkHttp基于Http协议实现了缓存,但是
默认是关闭状态
,需要在配置OkHttpClient时候使用:OkHttpClient.Builder().cache(Cache(文件,大小)) .build() 开启
。 -
OkHttp只会缓存GET请求的响应,在RFC7231中GET,
HEAD和某些情况下的POST都是可缓存的,但是绝大多数的实现里只支持GET和HEAD的缓存,这是因为post做的一般是修改和删除的工作,所以必须与服务端交互,所以不能使用缓存
。
而Http的缓存又分为强缓存
与协商缓存
。
14、强缓存
- 命中强缓存时,浏览器并不会将请求发送给服务器。强缓存是利用http的返回头中的Expires或者Cache- Control两个字段来控制的,用来表示资源的缓存时间;
- 若未命中强缓存,则浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modify/If-Modify- Since或Etag/If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,客户端从缓存中加载资源。
-
expires,它的值为一个绝对时间,如果发送请求的时间在expires之前,那么本地缓存有效,能够直接使用缓存。
-
cache-control:max-age=number,资源第一次的请求时间和该值相加,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行。另外此响应头还能设置为:
- no-cache:不使用本地缓存。
- no-store:不允许被缓存
- public:可以被任何用户缓存,包括终端用户和CDN等中间代理服务器。
- private:只能被终端用户缓存,不允许CDN等中间代理服务器缓存。
- immutable:(响应)资源不会改变
15、协商缓存
协商缓存的意思是:浏览器会将请求发送至服务器。服务端可能响应304(不包含响应体数据)表示缓存可用,可能正常响应,如200同时携带响应体。为了让服务端判断是否可用缓存,在请求时,需要携带标识:**If-Modified-Since或者If-None-Match**
。
其中If-Modified-Since需要和响应的Last-Modified 配合使用,而If-None-Match 则与Etag 配合。
Last-Modified/ If-Modified-Since
在缓存的响应中响应头包含:Last-Modified
,表示服务端告知的对应请求的资源在服务器上的最后修改时间。再次发起请求,需要在请求头中携带:If-Modified-Since
。其值就是响应中的Last-Modified
的值。意思就是告诉服务端我的缓存,在服务端什么时候修改后拿到的。服务端判断这个时间后是否修改过资源,未修改则返回3-04,否则正常返回响应数据。
Etag/If-None-Match
这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变。在请求时携带If-None-Match
,其值是缓存的响应头中的Etag
,服务端获取到请求头中的If-None-Match
会重新计算此次请求资源的标识,如果一致则返回304。
16、Okhttp 运用了哪些设计模式?
Okhttp 运用了六种设计模式:
- 构造者模式(OkhttpClient,Request 等各种对象的创建)
- 工厂模式(在 Call 接口中,有一个内部工厂 Factory 接口。)
- 单例模式(Platform 类,已经使用 Okhttp 时使用单例)
- 策略模式(在 CacheInterceptor 中,在响应数据的选择中使用了策略模式,选择缓存数据还是选择网络访问。)
- 责任链模式(拦截器的链式调用)
- 享元模式(Dispatcher 的线程池中,不限量的线程池实现了对象复用)
17、HTTP1和HTTP2的区别
- 1.新的二进制格式:HTTP2采用二进制格式而HTTP1使用文本格式。
- 2.多路复用:HTTP2是完全多复用的,而非有序并阻塞的,只需一个连接即可实现并行。HTTP1一个连接只能发送一个请求。
- 3.头部压缩:HTTP 1.1中,每一次发送和响应,都有HTTP头信息。HTTP 2压缩头信息,减少带宽。
- 4.服务器推送:HTTP 1只能客户端发送数据,服务器端返回数据。HTTP2中,服务器可以主动向客户端发起一些数据传输(如css和png等),服务器可以并行发送html,css,js等数据。。
18、为什么需要头部压缩?
HTTP协议是不带有状态的,每次请求头部都会附上所有的信息,而且很多的信息都是重复的,这会浪费很多宽带也会影响速度,所以HTTP2对头部进行了压缩,一方面使用gzip或compress进行头部压缩,另一方面,客户端和服务器会同时维护同一张头信息表,所有的字段都会存入这张表中,生成一个索引号,以后就不需要再发送同样的字段了,只发送索引号,提示了速度。