OkHttp 系列三:分发器是如何执行的,拦截器又是什么玩的?

523 阅读11分钟

OkHttp 分发器

本文概述

  • 文章以OkHttp 分发器、拦截器为讨论重点;深入源码层面系统分析了分发器的工作流程(应对同/异步请求)并讨论了OkHttp 中拦截器的责任链设计模式以及OkHttp 默认五大拦截器;文末给出了部分面试题解题思路;

正文部分:

  • 整体流程

    1. 实例化OkHttpClient 对象

    2. 实例化Request 对象

    3. 实例化newCall(request):得到的是ReCall 对象

      • 源码:

      image-20220729211608380

      • Call:是一个接口,包含方法

        • excute:同步
        • enqueue:异步
  • 异步请求中干了什么

    • 源码展示

    image-20220729213344436

    1. 判断这个Recall 是否被执行:check

      • 已经被执行过了 ---> 抛出异常

      image-20220729212024554

      • 底层使用AtomicBoolean (默认值为false)

      image-20220729212058227

      • 第一次执行(无论同步/异步) ---> 将这个值改为true
      • 第二次执行(无论同步/异步) ---> 抛出异常
    2. 事件回调:callStart()

      • 在构建OkHttpClient 的时候如果配置了eventListener(),在请求的时候就会回调
      • 配置eventListener

      image-20220729212721992

      • 有什么事件

        • 缓存:

          • 缓存命中
          • 缓存miss
        • 请求:

          • 请求开始
          • 请求结束
          • 请求失败
        • 任务:取消任务

    3. 分发器分发请求

      image-20220729213130840

      • 其实在构建OkHttpClient 的时候就可以去定制分发器

        • 可以去修改参数,但一般情况下不去改
      • 传入参数AsyncCall

        • 这个是内部类并且实现了Runnable 接口
        • 存在类属性:callsPerHost : AtomicInteger
  • 分发器的enqueue 方法干了什么事情 :从ready 队列移到running 队列

    • 分发器:异步请求工作流程

      • 拿到AsyncCall 代表异步请求任务(是一个Runnable ),交给Dispatcher 分发

      • 先将AsyncCall 放入ready 队列等待执行

      • 执行promoteAndExcute

        • 对ready 队列进行遍历,判断是否满足限制

          • 当前的满足,从ready 队列中拿出AsyncCall 给running 队列

          • 当前的不满足,检查ready 队列中的下一个AsyncCall

            • 都不满足,什么都不干
      • 某一个任务(AsyncCall )执行完成

        • 从running 队列中将其移除

        • 执行分发器的finish 方法

          • 执行promoteAndExcute (重复刚才的流程)

    image-20220729221218310

    1. 将call 放入准备执行的异步请求队列

      image-20220729213508250

      • 此时存在三个队列(关注两个异步请求队列)

        image-20220729213621769

      • 为什么会存在两个异步请求队列:对庞大的请求进行限制

    2. 如果此时为Http 请求

      1. 查找两个异步队列中有没有跟当前请求的目标主机相同的请求

        • 有,就拿到这个AsyncCall 对象

        • 将当前AsyncCall 对象的callsPerHost 赋值为找到的这个AsyncCall 的callsPerHost

          • 使得两个AsyncCall 对象中的callsPerHost 相同
    1. 分发请求:promoteAndExecute

      • 迭代准备执行的异步请求队列

         while (i.hasNext()) {
             val asyncCall = i.next()
        
        • 限制正在执行异步请求的任务数 不能大于 64个,跳出迭代

          • 不可能一下子异步干10000 个请求
           //正在执行异步请求的任务数 不能大于 64个
           if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
          
        • 限制同一个host的请求数 不能大于5

           //同一个host的请求数 不能大于5
           // Host max capacity.
           if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue 
          
          • 可以存在6 个对百度的请求,但第6 个请求需要等待前五个正在执行的请求让出一个位置才能执行
          • 缓解服务端的压力
    2. 将拿到的任务从ready 队列中移除:准备开始执行这个任务了

       i.remove()
      
    3. callsPerHost 加一

      • 这个记录的是:存在多少个相同host 的正在执行的请求数
       asyncCall.callsPerHost.incrementAndGet()
      
    4. 将这个任务添加到临时集合:设计亮点

       executableCalls.add(asyncCall)  //需要开始执行的任务集合
      
      • 会扫描这个队列,准备执行:executeOn

         for (i in 0 until executableCalls.size) {
               val asyncCall = executableCalls[i]
               asyncCall.executeOn(executorService)
             }
        
      • executeOn 传入线程池,执行这个AsyncCall 的run 方法

        image-20220729220158834

    5. 将这个任务添加到正在执行的异步请求队列中

       runningAsyncCalls.add(asyncCall)
      
    6. 存在的问题:

      • 如果当前任务不满足上述的两个限制那么就会进入等待状态,那这个等待状态的Async 什么时候执行呢?

        • 在Async 的run 方法中重新进行启动
         //执行请求 ---> 实质上是执行拦截器
         val response = getResponseWithInterceptorChain()
        
        • 接着会调用

           client.dispatcher.finished(this)
          
          • 首先对callsPerHost 减一

             call.callsPerHost.decrementAndGet()
            
          • 执行重载的finish 方法

            • 当有65 个请求,结束了一个请求后 ---> 调用finish ,重新启动任务,将原第65 个请求放进来(从ready 队列取)
             finished(runningAsyncCalls, call)
            
            • 再次执行promoteAndExecute 启动任务

               val isRunning = promoteAndExecute()
              
    7. 线程池的执行小细节

      • 当一个任务通过execute(Runnable)方法添加到线程池时:

        • 线程数量小于corePoolSize,新建线程(核心)来处理被添加的任务;
      • 线程数量大于等于 corePoolSize,新任务被添加到等待队列,若添加失败:

        • 线程数量小于maximumPoolSize,新建线程执行新任务;
        • 线程数量等于maximumPoolSize,使用RejectedExecutionHandler拒绝策略。
    1. Dispatcher 中的线程池长什么样子

      • 核心线程数:0

      • 最大线程数:Int.MAX_VALUE

      • 保活(闲置)时间:60 秒

      • 阻塞队列:无界队列 设计亮点

        • 向里面提交任务一定会失败 :希望提交的任务能得到及时执行而不等待

          • 如果提交成功 ---> 一定会等待执行
          • 如果提交失败 ---> 新建线程,马上执行这个任务

      image-20220729222240009

      • 这个跟newCachedThreadPool 是相同的

        • 让请求并发执行,不能让它等

OkHttp 同步请求:execute

  • 请求流程:

    1. check:判断此请求是否初次执行

       check(executed.compareAndSet(false, true)) { "Already Executed" }
      
    2. callStart():事件回调

    3. 分发器分发:

       client.dispatcher.executed(this)
      
  • 同步请求的分发器是怎么玩的

    • 将当前请求任务放入同步请求队列

       @Synchronized internal fun executed(call: RealCall) {
           runningSyncCalls.add(call)
       }
      
      • 同步请求没有等待队列,来就直接干
    • 执行这个请求:拿到请求的结果Response

      • 异步请求还是调用这个方法获得对应的响应结果
       return getResponseWithInterceptorChain()
      
    • 执行同步队列的finish 方法:同样会触发ready 队列到running 队列的检查

      • 在OkHttp 3.X 中就不会在同步请求结束后触发异步任务的检查
    • 从同步队列中移除这个请求

OkHttp 拦截器责任链设计模式

  • 首先:

    • OkHttp 通过分发器调用getResponseWithInterceptorChain 拿到同/异步请求的Response

    • 添加拦截器:

      • 创建一个拦截器集合

         val interceptors = mutableListOf<Interceptor>()
        
      • 添加自定义拦截器:addInterceptor

         //添加到前面
         interceptors += client.interceptors
        
      • 添加默认的四个拦截器

         interceptors += RetryAndFollowUpInterceptor(client)
         interceptors += BridgeInterceptor(client.cookieJar)
         interceptors += CacheInterceptor(client.cache)
         interceptors += ConnectInterceptor
        
      • 如果不是WebSocket 类型的请求的话,添加自定义拦截器

         //适用于Http 请求 
         if (!forWebSocket) {
             interceptors += client.networkInterceptors
         }
        
      • 添加最后一个默认的拦截器

         interceptors += CallServerInterceptor(forWebSocket)
        
      • 注册自定义拦截器,是可以拿到chain 链条(一定调用proceed 方法)

        • 不然,不成连;
      • 问题:为什么两种自定义拦截器添加的时机不同

        • 拿到请求,拿到响应的时机不同;

        • 有个HttpLoggingIntercept (日志)

          • 记录用户的请求:放到前面
          • 记录真正的请求:放到后面
      • 拦截器的应用场景:签名

        • 根据请求对象/URL ---> 带了签名的请求对象/URL;
        • 因为请求是一定会经过自定义拦截器的(类似RxJava 的卡片式编程)
    • 示意图:

    图片.png

  • OkHttp 的默认拦截器种类

    • 重试重定向:RetryAndFollowUpInterceptor

    • 处理桥接:BridgeInterceptor

      • 处理请求/响应头信息
    • 处理缓存:CacheInterceptor

      • 缓存中存在那种不会改变的资源正好是想要的,那就不用发起这次请求
    • 网络请求拦截器:ConnectionInterceptor

      • 适用于缓存没有直接命中
      • 根据跟服务器的链接对象去执行最后一个拦截器
    • 服务器通讯拦截器:CallServerInterceptor

      • 根据网络请求拦截器拿到的与服务器的链接对象,将请求的数据(requst),将request 编码成Http 报文,交给Sockect 的OutputStream 发给服务端获得响应
  • OkHttp 拦截器执行流程

    • 责任链模式(对象行为型模式),请求自顶向下,响应从下至上

      • 对象行为型模式,为请求创建了一个接收者对象的链,在处理请求的时候执行过滤(各司其职)。
      • 责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。
    • 生活场景:吃外卖的我,不关心这个外卖盐是放了多少,等着吃就行了

      图片.png

OkHttp 默认五大拦截器

  • 工作流程:

    图片.png

  • 重试拦截器:

    • 在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;

      • 拦截器结束这个请求,不向下走
    • 在获得了结果之后 ,会根据响应码判断是否需要重定向

      • 拦截器结束这个请求,不回馈给用户;
    • 如果满足条件那么就会重启执行所有拦截器。

      • 根据Http 状态码:3XX

        • 将重定向的URL 组成新的URL 执行重定向
  • 桥接拦截器:

    • 在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)

      • Http 请求必须携带Host 请求头
      • OkHttp 在实例化OkHttpClient 时不用显示添加Host 请求头,桥接拦截器会做
    • 并添加一些默认的行为(如:GZIP压缩);

      • 服务端在进行GZIP 压缩了数据后,会通过响应头告诉客户端
      • 客户端会进行GZIP 解析
    • 在获得了结果后,调用保存cookie接口并解析GZIP数据。

      • 在实例化OkHttpClient ,可以添加CookieJar

        • saveFromResponse

          • 响应存在Cookie 数据,桥接拦截器回调这个方法,将对应的URL 以及Cookie 数据回传给用户 ,由用户完成保存
        • loadFromResponse

          • 在请求时,桥接拦截器会回调这个方法,根据请求的url 判断有没有Cookie 需要设置给这个URL 请求头中去
        • 第二次请求相同的网站,将Cookie 拿出去

  • 缓存拦截器:

    • 顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
  • 连接拦截器:

    • 在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。
    • 连接池中有就拿,没有就新建一个
  • 请求服务器拦截器:

    • 进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

面试题汇总:

  • OkHttp 的请求流程是什么样的

    • 实例化OkHttpClient 对象,实例化Request 对象;将Request 对象作为参数实例化一个实现了Call 接口的RealCall 对象;RealCall 存在同步/异步方法,但只能执行一次

    • 执行同步请求:在拦截器中会将该RealCall请求保存在正在执行的同步请求队列中去

      • RealCall.execute

        • 分发器中:只是将正在执行的请求放入同步队列
        • 拦截器中:执行完后就移除这个任务
    • 执行异步请求:根据RealCall 生成AsyncCall ,将其交给分发器;

      • 在分发器中:

        • 拿到AsyncCall 代表异步请求任务(是一个Runnable ),交给Dispatcher 分发

        • 先将AsyncCall 放入ready 队列等待执行

        • 执行promoteAndExcute

          • 对ready 队列进行遍历,判断是否满足限制

            • 限制一:正在执行异步请求的任务数 不能大于 64个,跳出迭代
            • 限制二:同一个host的请求数 不能大于5
            • 当前的满足,从ready 队列中拿出AsyncCall 给running 队列

              • 拿给线程池执行,进入拦截器处理
            • 当前的不满足,检查ready 队列中的下一个AsyncCall

              • 都不满足,什么都不干
        • 某一个任务(AsyncCall )执行完成

          • 从running 队列中将其移除

          • 执行分发器的finish 方法

            • 执行promoteAndExcute (重复刚才的流程)
      • 在拦截器中:请求自顶向下,响应自下而上;

  • 拦截器是如何进行工作的:

    • OkHttp 拦截器执行流程

      • 责任链模式(对象行为型模式),请求自顶向下,响应从下至上

        • 对象行为型模式,为请求创建了一个接收者对象的链,在处理请求的时候执行过滤(各司其职)。
        • 责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。
  • 应用拦截器与网络拦截器的区别

    • 应用拦截器:自定义拦截器一

    • 网络拦截器:自定义拦截器二

    • 区别:

      • 应用拦截器:第一个拿到请求(用户的请求),最后一个拿到响应
      • 网络拦截器:倒数第二个拿到请求(真正发出去的Request对象),第二个拿到响应