Android开源框架系列-OkHttp4.11.0-请求转发详细分析

前言

OkHttp是一个开源网络库,封装了http协议的一整套协议,并且被引人到安卓源码中,所以要学习网络相关的知识,绕不开OkHttp,今天我们就开始完整的读一下OkHttp的源码,以对其整体实现有一个充分认识。

一个常规的OKhttp请求是这样的,创建出OkHttpClient,然后创建request,设置url(示例代码为get请求),再通过client.newCall得到一个Call对象(RealCall),通过调用enqueue异步进行网络请求(同步请求execute),今天我们就以示例代码为入口来读源码。

vbscript 复制代码
OkHttpClient client = new OkHttpClient().newBuilder()
        .build();
Request request = new Request.Builder()
        .url("https://www.jnnews.tv/guanzhu/p/2023-11/25/1016488.html")
        .build();
Call call = client.newCall(request);
//异步请求
call.enqueue(new Callback());
//同步请求
Response execute = call.execute();

enqueue方法

为了控制字数,有些简单的源码就绕过了,我们直接从enqueue方法跟随断点调试入手。首先要知道newCall得到的Call是一个接口,它的唯一实现是RealCall,所以进入RealCall的enqueue方法。

kotlin 复制代码
override fun enqueue(responseCallback: Callback) {
  //client.dispatcher为转发器,负责转发请求,它有一个默认的实现类Dispatcher
  //new一个AsyncCall,将responseCallback传入,以AsyncCall为参数执行enqueue
  client.dispatcher.enqueue(AsyncCall(responseCallback))
}

readyAsyncCalls是一个ArrayDeque类型的队列,将本次的call添加进去,然后试图寻找一个已经存在的和本次请求host相同的call,如果有,则记录下同一个host的请求次数,记录到RealCall中的callsPerHost变量上(OkHttp对同一域名下能同时进行的请求数量有控制)。

kotlin 复制代码
internal fun enqueue(call: AsyncCall) {
  synchronized(this) {
    readyAsyncCalls.add(call)
    //http请求为true,
    if (!call.call.forWebSocket) {
      //寻找已经开始请求或者等待开始请求的具有相同host的call对象,这里可以认为一定不会返回空,
      //因为上一步刚刚讲新创建的call加入readyAsyncCalls队列。
      val existingCall = findExistingCallWithHost(call.host)
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
  //准备开始执行
  promoteAndExecute()
}

进入promoteAndExecute要准备开始执行请求了,执行请求之前有一些控制条件,首先我们要了解,OKhttp内部有三个队列(runningSyncCalls我们先不关心了,属于同步请求的逻辑),默认情况下,能同时进行的异步请求最大数量为64,所以runningAsyncCalls的最大容量为64,超过这个数量则会加入到readyAsyncCalls队列进行等待,另外同一个host的请求同时最大不能超过5个。

java 复制代码
//准备好可以执行的异步请求队列,存放未进行的异步任务
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
//正在执行的异步请求队列,存放正在执行的异步任务
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
//正在执行的同步请求队列,存放正在执行的同步任务
private val runningSyncCalls = ArrayDeque<RealCall>()

所以在promoteAndExecute方法中。

kotlin 复制代码
private fun promoteAndExecute(): Boolean {
  val executableCalls = mutableListOf<AsyncCall>()
  synchronized(this) {
    //遍历所有准备好的异步请求
    val i = readyAsyncCalls.iterator()
    while (i.hasNext()) {
      val asyncCall = i.next()
      //如果正在进行的请求数量大于64,则跳出循环,本次不请求
      if (runningAsyncCalls.size >= this.maxRequests) break 
      //当前请求域名正在请求的数量是否超过5,如果是,跳过这个请求
      if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue
      //走到这了,说明满足请求的条件
      i.remove()
      //callsPerHost自增1,表示当前host正在请求的数量增加了1
      asyncCall.callsPerHost.incrementAndGet()
      //加入list
      executableCalls.add(asyncCall)
      //加入runningAsyncCalls队列,记录正在请求的call,请求完成后会从这里移除
      runningAsyncCalls.add(asyncCall)
    }
  }
  for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
    //executableCalls中记录的是本次可以执行的call,所以遍历一遍,统统丢入线程池执行
    asyncCall.executeOn(executorService)
  }
}

看下asyncCall的executeOn方法,这里提交到线程池执行后,会在finally中判断是否成功,如果提交失败(这里失败的几率极低,finish方法还有其他执行的场景),会执行finished方法,前边我们分析时,提到过加入最大请求数超过64或者同一host请求数超过5,是不会立即执行新的请求,而是加入了等待执行队列中,那么等待执行队列中的请求是什么时候开始执行的,就跟finished方法有关,我们看一下它的实现。

kotlin 复制代码
fun executeOn(executorService: ExecutorService) {
  var success = false
  try {
    //在线程池中执行
    executorService.execute(this)
    success = true
  } catch (e: RejectedExecutionException) {
    //这里表示提交到线程池失败,执行了线程池的拒绝策略,通常不会走到这里,因为okhttp的线程池是一个
    //可以认为是无限容量的,所以这里这样做主要是为了代码的逻辑正常
    responseCallback.onFailure(this@RealCall, ioException)
  } finally {
    if (!success) {
      client.dispatcher.finished(this) 
    }
  }
}

finished方法顾名思义就是一个请求完成后执行的逻辑,不管这个请求是成功了还是失败了,都要去执行,前边executeOn中执行的时机是提交到线程池失败后执行。

kotlin 复制代码
internal fun finished(call: AsyncCall) {
  //将host下正在进行的请求数量-1
  call.callsPerHost.decrementAndGet()
  finished(runningAsyncCalls, call)
}
private fun <T> finished(calls: Deque<T>, call: T) {
  //idleCallback默认为空,有点类似于安卓中消息队列中的idlehandler,用于在请求空闲的时候执行任务
  //可以通过Dispatcher的setIdleCallback方法设置
  val idleCallback: Runnable?
  synchronized(this) {
    //将当前请求从runningAsyncCalls中移除
    if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
    idleCallback = this.idleCallback
  }
  //从runningAsyncCalls中移除一个请求之后,正在请求的数量就会小于64了(没有其他线程添加的情况下)
  //此时再次尝试从准备队列中拿到等待的请求去执行,promoteAndExecute方法我们前边已经看过了
  val isRunning = promoteAndExecute()
  //没有任何请求在执行的时候才会返回isRunning为false,所以这个时候请求一定是空闲的,执行idleCallback
  if (!isRunning && idleCallback != null) {
    idleCallback.run()
  }
}

回到执行中的任务,任务被提交到线程池,被提交的任务指的就是AsyncCall,它实现了Runnable接口,看下它的run方法,这里就进入了okhttp的五大拦截器流程中,也是okhttp的核心流程,这些源码会在后边的文章中单独进行分析。

less 复制代码
override fun run() {
  threadName("OkHttp ${redactedUrl()}") {
    var signalledCallback = false
    //超时逻辑
    timeout.enter()
    try {
      //这是okhttp的主线,通过5大拦截器处理网络请求,最后返回结果
      val response = getResponseWithInterceptorChain()
      signalledCallback = true
      //正常响应后回调成功
      responseCallback.onResponse(this@RealCall, response)
    } catch (e: IOException) {
      responseCallback.onFailure(this@RealCall, e)
    } catch (t: Throwable) {
      responseCallback.onFailure(this@RealCall, canceledException)
    } finally {
      //本次请求不管成功还是失败,都执行一次finished,尝试启动等待中的请求
      client.dispatcher.finished(this)
    }
  }
}

execute

异步请求看完了,顺便也看一眼同步请求,同步请求的逻辑很简单,和异步请求也有重合的地方,他们最终都是通过五大拦截器执行请求。

kotlin 复制代码
override fun execute(): Response {
  //超时逻辑
  timeout.enter()
  callStart()
  try {
    //看下执行逻辑,将当前请求添加到同步请求队列runningSyncCalls
    client.dispatcher.executed(this)
    //五大拦截器进行网络请求
    return getResponseWithInterceptorChain()
  } finally {
    //执行完成,调用finished将当前请求从runningSyncCalls队列中移除(同步请求也会被加入队列)
    //执行完成后移除,所以runningSyncCalls中始终都只有一条记录,另外你会发现,同步请求执行完成
    //之后也调用到了promoteAndExecute方法,这个方法只会检查等待中的异步请求,如果有满足条件的
    //就执行它们
    client.dispatcher.finished(this)
  }
}
@Synchronized internal fun executed(call: RealCall) {
  runningSyncCalls.add(call)
}

总结

今天我们分析了okhttp的异步请求和同步请求的执行逻辑,异步请求有最大请求数的限制,最大64个线程同时执行,这是考虑了客户端硬件能力的发挥,我们知道无限的开启线程并不能提高系统的执行效率,这也算是对客户端系统资源的一种合理利用,另外针对同一个域名下,同时只能有5个请求,是对服务端的一种保护,控制同一时刻请求服务端的数量。后边我们会进行五大拦截器的分析,这才是okhttp的重中之重,也是难点,包含的知识点很多,更有利于我们去学习网络编程,敬请期待。

相关推荐
LCG元3 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
拭心5 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
GISer_Jing7 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245527 小时前
吉利前端、AI面试
前端·面试·职场和发展
带电的小王7 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡8 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道8 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
TodoCoder9 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
阿甘知识库9 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道10 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频