Android知名三方库OKHttp(三) - 拦截器源码分析

源代码
GitHub源代码

本文目标

理解okhttp的拦截器各自的作用

拦截器

在发起请求的时候,当执行到execute()方法会调用

 Response result = getResponseWithInterceptorChain();

这行代码,就是通过责任链模式的拦截器拿到响应的,具体代码如下

final class RealCall implements Call {

  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this);
    }
  }

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    // 拦截器的一个集合
    List<Interceptor> interceptors = new ArrayList<>();
    // 客户端的所有自定义拦截器
    interceptors.addAll(client.interceptors());// 自己的
    // OKhttp 5 个拦截器 ,责任链设计模式,每一个拦截器只处理与他相关的部分 
    interceptors.add(retryAndFollowUpInterceptor);// 重试
    interceptors.add(new BridgeInterceptor(client.cookieJar()));// 基础
    interceptors.add(new CacheInterceptor(client.internalCache()));// 缓存
    interceptors.add(new ConnectInterceptor(client));// 建立连接
    interceptors.add(new CallServerInterceptor(forWebSocket));// 写数据

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
}

我们自己也可以实现 Interceptor 接口来自定义拦截器,okhttp会先将我们的拦截器添加到集合中,下面的5个拦截器都是okhttp自带的拦截器

1.RetryAndFollowUpInterceptor

处理重试的一个拦截器,会去处理一些异常,只要不是致命的异常就会重新发起一次请求(把Request给下级),如果是致命的异常就会抛给上一级;
会处理一些重定向等等,比如3XX 307,407就会从头部中获取新的路径,生成一个新的请求交给下一级(重新发送一次请求)

2.BridgeInterceptor

做一个简单的处理,设置一些通用的请求头,Content-Type,Connection,Content-Length,Cookie
做一些返回的处理,如果返回的数据被压缩了采用 ZipSource,保存Cookie

3.CacheInterceptor

在缓存可用的情况下,读取本地的缓存的数据,如果没有直接去服务器,如果有首先判断有没有缓存策略,然后判断有没有过期,如果没有过期直接拿缓存,如果过期了需要添加一些之前头部信息如:If-Modified-Since,这个时候后台有可能会给你返回 304 代表你是可以拿本地缓存,每次读取到新的响应后做一次缓存

4.ConnectInterceptor

findHealthyConnection()找一个连接,首先判断有没有健康的,没有就创建(建立Socket,握手连接),连接缓存
okHttp是基于原生的 Socket + okio (原生IO的封装)
封装 HttpCodec 里面封装了okio的 Source(输入) 和 Sink(输出),我们通过 HttpCodec 就可以操作 Socket的输入输出,我们就可以向服务器写数据和读取返回数据

5.CallServerInterceptor

写数据和读取数据
写头部信息,写body表达信息等等

连接的三个核心类

  • RealConnection 建立连接的一个对象的封装
  • ConnectionPool 保存了连接
  • StreamAllocation 找一些链接,做一下封装

6.分发器(内部维护队列与线程池,完成请求调配)

对于同步请求,分发器只记录请求,⽤于判断IdleRunnable是否需要执⾏。对于异步请求,向分发器中提交请求。
如何决定将请求放⼊ready还是running队列?
如果当前正在请求数不⼩于64放⼊ready;如果⼩于64,但是已经存在同⼀域名主机的请求5个也会放⼊ready!

从running移动到ready队列的条件是什么?
每个请求执⾏完成就会从running移除,同时进⾏第⼀步相同逻辑的判断,决定是否移动到ready!

分发器线程池的⼯作⾏为?
使⽤了缓存线程池(⽆等待,最⼤并发)+ 定义了线程⼯程(设置了线程名,设置不是守护线程)。

7.责任链模式

为请求创建了⼀个接收者对象的链,在处理请求的时候执⾏过滤(各司其职)。责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,⽆须关⼼请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。

getResponseWithInterceptorChain()方法都干了什么
1.把所有配置好的 Interceptor 放在⼀个 List ⾥,然后作为参数,创建⼀个 RealInterceptorChain 对象,并调⽤ chain.proceed(request) 来发起请求和获取响应。
2.在 RealInterceptorChain 中,多个 Interceptor 会依次调⽤⾃⼰的intercept() ⽅法。这个⽅法会做三件事:
1)、对请求进⾏预处理。
2)、预处理之后,重新调⽤ RealInterceptorChain.proceed() 把请求交给下⼀个 Interceptor。
3)、在下⼀个 Interceptor 处理完成并返回之后,拿到Response 进⾏后续处理,并返回给上⼀个拦截器。当然了,最后⼀个 Interceptor 的任务只有⼀个:做真正的⽹络请求并拿到响应。

8.Okhttp执行流程总结:

Okhttp的所有的逻辑⼤部分集中在拦截器中,但是在进⼊拦截器之前还需要依靠分发器来调配请求任务。

首先newCall(Request)⽅法会返回⼀个 RealCall对象,它是Call 接⼝的实现。
当调⽤ RealCall.execute() 的时候,RealCall.getResponseWithInterceptorChain() 会被调⽤,它会发起⽹络请求并拿到返回的响应,装进⼀个 Response 对象并作为返回值返回;
当调用RealCall.enqueue() 时候和RealCall.execute() ⼤同⼩异,区别在于 enqueue() 会使⽤ Dispatcher的线程池来把请求发在后台线程进⾏,但实质上使⽤的同样也是 getResponseWithInterceptorChain()⽅法。

getResponseWithInterceptorChain()这行代码,就是通过责任链模式的拦截器拿到响应的,简单总结一下就是

整个OkHttp功能的实现就在这五个默认的拦截器中,分别为: 重试桥接缓存连接请求服务这5个拦截器。每⼀个拦截器负责的⼯作不⼀样,就好像⼯⼚流⽔线,最终经过这五道⼯序,就完成了最终的产品。但是与流⽔线不同的是,OkHttp中的拦截器每次发起请求都会在交给下⼀个拦截器之前⼲⼀些事情,在获得了结果之后⼜⼲⼀些事情。整个过程在请求时是顺序的,⽽响应时则是逆序。当⽤户发起⼀个请求后,会由任务分发器Dispatcher将请求包装并交给重试拦截器处理。
1、重试拦截器在交出(交给下⼀个拦截器)之前,负责判断⽤户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满⾜条件那么就会重新执⾏所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加⼊其中(如:Host,Content-Type,Connection,Content-Length等等)并添加⼀些默认的⾏为(如:GZIP压缩);在获得了结果后,调⽤保存cookie接⼝并解析GZIP数据
3、缓存拦截器顾名思义,交出之前读取并判断是否使⽤缓存;获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建⼀个连接,并获得对应的socket流;在获得结果后不进⾏额外的处理。
5、请求服务器拦截器进⾏真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。
在经过了这⼀系列的流程后,就完成了⼀次HTTP请求!

OKhttp针对⽹络层有哪些优化?
1、多路复⽤(ConnectInterceptor):在 StreamAllocation.newStream⽅法中获取 RealConnection,在获取的时候我们会判断有没有之前的连接可以复⽤。
2、缓存(CacheInterceptor): okhttp 的缓存策略是,key 为请求 url的 MD5 值value 为响应。
3、压缩(bridgeInterceptor): response 通过 bridgeInterceptor 处理的时候会进⾏ gzip 压缩,这样可以⼤⼤减⼩我们的 response,但不是什么情况下都压缩,只有⽀持的时候才会进⾏压缩。

9.连接池相关:

OkHttp怎么实现连接池?
首先要思考一个问题是为什么需要连接池?
1.频繁的进⾏建⽴Sokcet连接断开Socket是⾮常消耗⽹络资源和浪费时间的,所以HTTP中的keepalive连接对于降低延迟和提升速度有⾮常重要的作⽤。
2.它可以在⼀次TCP连接中可以持续发送多份数据⽽不会断开连接。所以连接的多次使⽤,也就是复⽤就变得格外重要了,⽽复⽤连接就需要对连接进⾏管理,于是就有了连接池的概念。
3.OkHttp中使⽤ConectionPool实现连接池,默认⽀持5个并发KeepAlive,默认链路⽣命为5分钟。

连接流程
1)⾸先,ConectionPool中维护了⼀个双端队列Deque,也就是两端都可以进出的队列,⽤来存储连接。
2)然后在ConnectInterceptor,也就是负责建⽴连接的拦截器中,⾸先会找可⽤连接,也就是从连接池中去获取连接,具体的就是会调⽤到ConectionPoolget⽅法。也就是遍历了双端队列,如果连接有效,就会调⽤acquire⽅法计数并返回这个连接。
3)如果没找到可⽤连接,就会创建新连接,并会把这个建⽴的连接加⼊到双端队列中,同时开始运⾏线程池中的线程,其实就是调⽤了ConectionPoolput⽅法。其实这个线程池中只有⼀个线程,是⽤来清理连接的,也就是cleanupRunnable

连接池如何清理
cleanupRunnablerun⽅法中会不停的调⽤cleanup⽅法清理线程池,并返回下⼀次清理的时间间隔,然后进⼊wait等待。它使⽤的回收算法类似于Java GC中的标记清除算法(标记不活跃的连接并删除:使⽤weakReference.get()检测到空闲的socket超过5个或者keepalive时间⼤于5分钟将当前连接从连接池删除并关闭连接)。这样被线程池检测到被回收,这样就可以保持多个健康的keep-alive连接。

总结一下连接池
主要就是管理双端队列``Deque<RealConnection>,可以⽤的连接就直接⽤,然后定期清理连接,同时通过对StreamAllocation的引⽤计数实现⾃动回收。

你从这个库中学到什么有价值的或者说可借鉴的设计思想?
使⽤责任链模式实现拦截器的分层设计,每⼀个拦截器对应⼀个功能,充分实现了功能解耦,易维护。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,976评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,249评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,449评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,433评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,460评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,132评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,721评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,641评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,180评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,267评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,408评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,076评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,767评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,255评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,386评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,764评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,413评论 2 358

推荐阅读更多精彩内容