okhttp3连接池清理

okhttp3连接池RealConnectionPool

连接池是用来存储连接实例对象RealConnection。存储RealConnection是为了实现连接复用,提高网络连接工作效率,减少内存开支。

class RealConnectionPool(
  taskRunner: TaskRunner, 
  private val maxIdleConnections: Int,
  keepAliveDuration: Long,
  timeUnit: TimeUnit
) {
  private val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration)

  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce() = cleanup(System.nanoTime())
  }
  // 连接池集合
  private val connections = ArrayDeque<RealConnection>()
}

可以认为RealConnectionPool在一定程度上也是起了管理连接RealConnection的作用。在面试中有的同学会经常被问到okhttp是怎么实现连接复用的。有时候还会被问到okhttp是怎么清理连接的。这篇文章就是来解析一下连接池是怎么清理连接的

清理在什么时候触发

新建的RealConnection在ExchangeFinder中被创建并确认使用后(这里会涉及到ExchangeFinder.findConnection连接复用的逻辑,本次文章不做探讨),会put到RealConnectionPool的队列中。仔细看put方法,即可找到清理逻辑的痕迹了

fun put(connection: RealConnection) {
    this.assertThreadHoldsLock()

    connections.add(connection)
    cleanupQueue.schedule(cleanupTask)
  }

走,去看cleanupTask是个什么东西

private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce() = cleanup(System.nanoTime())
  }

额…原来cleanupTask是一个匿名内部类。且先不管runOnce是干什么的(一会会给予解释),先看cleanup方法

cleanUp方法

fun cleanup(now: Long): Long {
    var inUseConnectionCount = 0
    var idleConnectionCount = 0
    var longestIdleConnection: RealConnection? = null
    var longestIdleDurationNs = Long.MIN_VALUE

    synchronized(this) {
      for (connection in connections) {
        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++
          continue
        }
        idleConnectionCount++

        val idleDurationNs = now - connection.idleAtNs
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs
          longestIdleConnection = connection
        }
      }
      
      when {
        longestIdleDurationNs >= this.keepAliveDurationNs
            || idleConnectionCount > this.maxIdleConnections -> {
          connections.remove(longestIdleConnection)
          if (connections.isEmpty()) cleanupQueue.cancelAll()
        }
        idleConnectionCount > 0 -> {
          // A connection will be ready to evict soon.
          return keepAliveDurationNs - longestIdleDurationNs
        }
        inUseConnectionCount > 0 -> {
          return keepAliveDurationNs
        }
        else -> {
          return -1
        }
      }
    }
    longestIdleConnection!!.socket().closeQuietly()
    return 0L
  }

咳,如果是不怎么喜欢看源码的同学估计这时候开始嘀咕了:又是一个只会复制源码的狗货。

别急!!!我们把这段代码分开来看!

cleanUp方法的下半部分

不过在看之前,先打个叉。你们知道触发连接池清理的条件是什么吗?很好记的,两个五!

  1. RealConnection的闲置实现不超过5分钟;
  2. RealConnectionPool中闲置的RealConnection数量不超过5个;

清理代码最好从较为简单的底部开始理解:

longestIdleDurationNs: 连接队列中最长的闲置时间
idleConnectionCount: 连接队列中处于闲置的总数
inUseConnectionCount:连接队列中仍然在工作的连接总数

我们发现cleanUp方法是带返回的,Long类型。解释一下返回的是什么:下一次执行清理任务的时间。
有了这些线索再去看代码就会清晰很多了。

fun cleanup(now: Long): Long {
    var inUseConnectionCount = 0
    var idleConnectionCount = 0
    var longestIdleConnection: RealConnection? = null
    var longestIdleDurationNs = Long.MIN_VALUE

    synchronized(this) {
      ..........上半部的for循环逻辑............      
      //下半部分
      when {
        longestIdleDurationNs >= this.keepAliveDurationNs
            || idleConnectionCount > this.maxIdleConnections -> {
          connections.remove(longestIdleConnection)
          if (connections.isEmpty()) cleanupQueue.cancelAll()
        }
        idleConnectionCount > 0 -> {
          // A connection will be ready to evict soon.
          return keepAliveDurationNs - longestIdleDurationNs
        }
        inUseConnectionCount > 0 -> {
          return keepAliveDurationNs
        }
        else -> {
          return -1
        }
      }
    }
    longestIdleConnection!!.socket().closeQuietly()
    return 0L
  }

可能你已经悟了,但我还是要啰嗦的写一下这段代码的内容:

  1. 如果有Connection的闲置时间超过默认闲置时间,就需要将这个Connection清除(清除顺序是闲置时间大到小,顺序的理由后面讲)。如果Connection闲置的数量超过默认闲置数量,也需要清除,顺序依旧是闲置时间大到小;
  2. (先解释上一个懵)每次清理只清理一个。但是只要触发清理,返回值就是0,也就是要求再次触发清理任务。由于查找闲置Connection会比较闲置时间,找到闲置最长的Connection。所以说的是从大到小清理。所以注意如果是闲置数量超过的触发清除,可能会只清理到闲置数量小于默认值,就不清除了;
  3. idleConnectionCount > 0 的情况是什么呢?一、没超过闲置时间 二、闲置数量没超限。所以下一次任务触发时间是默认闲置时间-当前闲置最长时间;
  4. 没有闲置的Call只要大于0,就继续等默认闲置时间后再来检查;
  5. 如果没有闲置的Call,就返回-1,表示不需要继续做任务。

那么怎么记录最长闲置时间、闲置数量、在工作数量的呢?这涉及到上半部份的逻辑

cleanUp方法的上半部份

fun cleanup(now: Long): Long {
    var inUseConnectionCount = 0
    var idleConnectionCount = 0
    var longestIdleConnection: RealConnection? = null
    var longestIdleDurationNs = Long.MIN_VALUE

    synchronized(this) {
      for (connection in connections) {
        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++
          continue
        }
        idleConnectionCount++

        val idleDurationNs = now - connection.idleAtNs
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs
          longestIdleConnection = connection
        }
      }
      ...........下半部分的内容...............
  }

上半部份的逻辑:

  1. 遍历连接队列,检查连接是处于否闲置,非闲置则inUseConnectionCount++;
  2. 有闲置则idleConnectionCount++,每找到一个闲置连接就需要通过比对得到闲置时间最长的连接对象。

接下来我们去看看检查连接是否处于闲置的方法pruneAndGetAllocationCount干了什么

pruneAndGetAllocationCount

在看清理代码之前,我们需要知道一件事。RealConnection是一个连接实例,它持有了RealCall对象。是怎么持有的呢?

val calls = mutableListOf<Reference<RealCall>>()

Reference…玛莎嘎…嗯,没错!是一个继承了WeakRefercence的CallReference类(这一段代码就不贴了,可以在RealCall里头找到)。

竟然是弱引用!!!

然后我们继续吧~

private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
    val references = connection.calls
    var i = 0
    while (i < references.size) {
      val reference = references[i]
      if (reference.get() != null) {
        i++
        continue
      }

      // We've discovered a leaked call. This is an application bug.
      val callReference = reference as CallReference
      val message = "A connection to ${connection.route().address.url} was leaked. " +
          "Did you forget to close a response body?"
      Platform.get().logCloseableLeak(message, callReference.callStackTrace)

      references.removeAt(i)
      connection.noNewExchanges = true

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNs = now - keepAliveDurationNs
        return 0
      }
    }

    return references.size
  }

在Http1.1中,一个RealConnection只会持有一个RealCall,只支持一个Http请求。在Http2.0中,一个RealConnection会持有多个RealCall(一般是128个),支持多个Http请求。

所以对一个RealConnection来说,什么时候是非闲置的?队列size > 0,又由于队列是一个弱引用队列,也就是弱引用队列持有的对象不是null的。所以在最底部return references.size,返回队列大小。

为什么要用WeakReference呢?为什么这个方法内容还不少呢? 其实吧,这里有个特别值得玩味,也是特别值得我们平时使用okhttp需要注意的地方!

使用okhttp注意点

在pruneAndGetAllocationCount方法中会对队列做遍历检查,找到Reference.get==null的对象。其实在源码注释里,我们会看到,Square的工程师提醒我们,这里有泄漏,“Did you forget to close a response body?”,你是否忘记close response body了。对okhttp不熟的读者们这时候肯定会懵逼,卧槽,这个还要关闭?是的,是需要关闭的。使用Retrofit的你可能不太清楚,因为Retrofit已经帮我们实现这段关闭操作了。如果你是自己封装的okhttp,请务必记得要close response body。

而且经过试验也可以发现,如果不主动关闭RealCall释放RealCall,在非HTTP/2的环境下我们就无法使用连接重用,就会生成一大堆的Connection直到出发GC回收RealCall,再到RealConnectionPool做Cleanup()检查,才会清除这些有泄漏的RealCall的RealConnection

看到这里,各位客官可以离开的了。毕竟清理逻辑已经基本搞清楚了。

如果您还想我多说一下,那就再挖一挖清理任务Task和TaskQueue吧

清理任务怎么被执行的

上文一直梳理清理的逻辑。那各位客官知道清理任务是怎么被执行的吗?

猜测一下,执行清理任务应该是放在线程里执行的,而且一般来说既有可能会是放在线程池里执行的。

只是猜测,你有根据吗?有的

class ConnectionPool internal constructor(
  internal val delegate: RealConnectionPool
) {
  constructor(
    maxIdleConnections: Int,
    keepAliveDuration: Long,
    timeUnit: TimeUnit
  ) : this(RealConnectionPool(
      taskRunner = TaskRunner.INSTANCE, 
      maxIdleConnections = maxIdleConnections,
      keepAliveDuration = keepAliveDuration,
      timeUnit = timeUnit
  ))
  //我没瞎说吧,两个五:最大闲置数量、最大闲置时间
  constructor() : this(5, 5, TimeUnit.MINUTES)
	.......忽略部分代码.......
}

TaskRunner.INSTANCE的构建方法中会有一个RealBackend的类。该类初始化阶段会生成线程池(具体代码不贴出来,毕竟这个不是重点)。

让我们回到清理任务开始的地方。

fun put(connection: RealConnection) {
    this.assertThreadHoldsLock()

    connections.add(connection)
    cleanupQueue.schedule(cleanupTask)//cleanupQueue是TaskQueue类的对象
  }

调用TaskQueue.schedule(task: Task)方法开始执行清理任务。那就去看看TaskQueue吧

TaskQueue

class TaskQueue internal constructor(
  internal val taskRunner: TaskRunner,
  internal val name: String
) {
  .....省略部分代码.....
  //activeTask正在运行的Task(作用一会就说)
  internal var activeTask: Task? = null
  //futureTasks存储着等待被执行的Task
  internal val futureTasks = mutableListOf<Task>()
  
  .....省略部分代码.....
  
  //schedule方法入口
  fun schedule(task: Task, delayNanos: Long = 0L) {
    synchronized(taskRunner) {
      //这一段是在执行前做中断判断,可以忽略
      if (shutdown) {
        if (task.cancelable) {
          taskLog(task, this) { "schedule canceled (queue is shutdown)" }
          return
        }
        taskLog(task, this) { "schedule failed (queue is shutdown)" }
        throw RejectedExecutionException()
      }
	  //对传进来的任务及执行时间做处理
      if (scheduleAndDecide(task, delayNanos, recurrence = false)) {
        //scheduleAndDecide返回true,那就可以开始执行了。
        //也就意味着scheduleAndDecide是个编排执行顺序的方法,顺序是按照时间来定的
        taskRunner.kickCoordinator(this)
      }
    }
  }
  
  internal fun scheduleAndDecide(task: Task, delayNanos: Long, recurrence: Boolean): Boolean {
    // task 把taskQueue存入Task中,相当于Task知道自己属于哪一个TaskQueue的
    task.initQueue(this)

    val now = taskRunner.backend.nanoTime()
    val executeNanoTime = now + delayNanos

    //在futureTasks中查找一下是否当前task已经入队
    //有 existingIndex = 序列位置
    //无 existingIndex = -1
    val existingIndex = futureTasks.indexOf(task)
    
    if (existingIndex != -1) {
	  //task已在队列中
	  //如果executeNanoTime大于等于下一次执行时间,就意味着任务已经被编排了,以最短的时间为准
	  //并return false表示结束
      if (task.nextExecuteNanoTime <= executeNanoTime) {
        taskLog(task, this) { "already scheduled" }
        return false
      }
      //如果executeNanoTime小于任务编排时间,那就肯定需要以executeNanoTime时间编排
      //在编排前,先把这个任务从队列中移除出去
      futureTasks.removeAt(existingIndex) 
    }
    //更新任务的编排时间
    task.nextExecuteNanoTime = executeNanoTime
    taskLog(task, this) {
      if (recurrence) "run again after ${formatDuration(executeNanoTime - now)}"
      else "scheduled after ${formatDuration(executeNanoTime - now)}"
    }
	//遍历任务队列,按照时间从小到大的顺序,找到插入序号,将任务重新入队。
    var insertAt = futureTasks.indexOfFirst { it.nextExecuteNanoTime - now > delayNanos }
    if (insertAt == -1) insertAt = futureTasks.size
    futureTasks.add(insertAt, task)
	//如果插入的序号是0,就返回true
    return insertAt == 0
  }
}

总结一下scheduleAndDecide所做的事情:

  1. 如果需要计划进行的任务在队列里,那就查看一下它的计划执行时间。如果时间比计划还早,那就不做处理,按原定时间进行即可。如果计划执行时间比较晚,那就先把任务从队列中取出;
  2. 取出任务之后,遍历任务表,重新筛选一个符合时间顺序的位置,把任务塞进这个位置里;
  3. return true相当于立即执行,如果塞进去的位置在第一位(也就是0),那也是true,否则为false

TaskRunner.kickCoordinator(taskQueue: TaskQueue)准备开始执行

TaskRunner

关于TaskRunner的内容解析,我没有想到特别好的解析方式。所以暂时还是以源码+注释的方式进行解析。如果客官老爷有什么意见建议,可以和我说说。

我们就开始吧

class TaskRunner(
  val backend: Backend
) {
  .......省略部分代码........
  //正在运行的task所在的TaskQueue队列
  private val busyQueues = mutableListOf<TaskQueue>()

  //准备运行的TaskQueue队列
  private val readyQueues = mutableListOf<TaskQueue>()

  //TaskQueue在片编排完任务Task的执行序列之后来到这里。
  internal fun kickCoordinator(taskQueue: TaskQueue) {
    this.assertThreadHoldsLock()

	//把当前的taskQueue加入到readyQueue队列中
    if (taskQueue.activeTask == null) {
      if (taskQueue.futureTasks.isNotEmpty()) {
        readyQueues.addIfAbsent(taskQueue)
      } else {
        readyQueues.remove(taskQueue)
      }
    }
    if (coordinatorWaiting) {
      backend.coordinatorNotify(this@TaskRunner)
    } else {
      //coordinatorWaiting一般为false,所以会执行runnable
      //backend(我一度看成了碧咸)是持有线程池的对象,通过execute开始执行
      backend.execute(runnable)
    }
  }
}

所以需要看一下runnable这个对象是干什么的

private val runnable: Runnable = object : Runnable {
    override fun run() {
      while (true) {
        //从awaitTaskToRun方法中获取满足执行条件的Task
        val task = synchronized(this@TaskRunner) {
          awaitTaskToRun()
        } ?: return
        logElapsed(task, task.queue!!) {
          var completedNormally = false
          try {
            runTask(task)
            completedNormally = true
          } finally {
            if (!completedNormally) {
              backend.execute(this)
            }
          }
        }
      }
    }
  }

awaitTaskToRun这个方法虽然不复杂,但是好长啊。我有点后悔来装逼了…

在进去看这个方法之前,为了避免你会混乱懵逼。我先给你捋一捋。

  1. 清理任务的任务形式是Task;
  2. Task会加入TaskQueue的futureTask队列
  3. TaskQueue会加入到TaskRunner的readyQueues队列中

因此可以大概推测awaitTaskToRun方法是从自己的readyQueues队列中,找到时间最靠前的TaskQueue,再从这个TaskQueue的队列中取出时间最靠前的Task来执行。

走,看看是不是这么一回事。

fun awaitTaskToRun(): Task? {
    this.assertThreadHoldsLock()

    while (true) {
      //准备运行的队列为空的情况,就返回null,这个挺好理解的...
      if (readyQueues.isEmpty()) {
        return null // Nothing to do.
      }

      val now = backend.nanoTime()
      var minDelayNanos = Long.MAX_VALUE
      var readyTask: Task? = null
      var multipleReadyTasks = false

      //遍历readyQueues
      //对于一个TaskQueue来说,它可执行时间最早的task,一定是在futureTasks的队头,也就是0位
      //所以这里就是要找到这个最早的task,readyTask指向这个最早的task
      eachQueue@ for (queue in readyQueues) {
        val candidate = queue.futureTasks[0]
        //执行时间修正(避免出现负数时间)
        val candidateDelay = maxOf(0L, candidate.nextExecuteNanoTime - now)

        when {
          // Compute the delay of the soonest-executable task.
          candidateDelay > 0L -> {
            minDelayNanos = minOf(candidateDelay, minDelayNanos)
            continue@eachQueue
          }
  
          readyTask != null -> {
            multipleReadyTasks = true
            break@eachQueue
          }

          // We have a task to execute when we complete the loop.
          else -> {
            readyTask = candidate
          }
        }
      }

      // Implement the decision.
      when {
        // We have a task ready to go. Get ready.
        readyTask != null -> {
          //在awaitTaskToRun返回Task前,会先执行beforeRun方法(这个一会讲干嘛使的)
          beforeRun(readyTask)

          // Also start another thread if there's more work or scheduling to do.
          if (multipleReadyTasks || !coordinatorWaiting && readyQueues.isNotEmpty()) {
            backend.execute(runnable)
          }

          return readyTask
        }
		.......省略掉部分代码........
      }
    }
  }

在awaitTaskToRun方法返回了一个Task之后,runnable会执行runTask方法。而在runTask方法的后半段,会执行afterRun方法。

//  执行Task的三大方法
  
 private fun beforeRun(task: Task) {
    this.assertThreadHoldsLock()

    task.nextExecuteNanoTime = -1L
    val queue = task.queue!!
    queue.futureTasks.remove(task)
    readyQueues.remove(queue)
    queue.activeTask = task
    busyQueues.add(queue)
  }

  private fun runTask(task: Task) {
    this.assertThreadDoesntHoldLock()

    val currentThread = Thread.currentThread()
    val oldName = currentThread.name
    currentThread.name = task.name

    var delayNanos = -1L
    try {
      delayNanos = task.runOnce()
    } finally {
      synchronized(this) {
        afterRun(task, delayNanos)
      }
      currentThread.name = oldName
    }
  }

  private fun afterRun(task: Task, delayNanos: Long) {
    this.assertThreadHoldsLock()

    val queue = task.queue!!
    check(queue.activeTask === task)

    val cancelActiveTask = queue.cancelActiveTask
    queue.cancelActiveTask = false
    queue.activeTask = null
    busyQueues.remove(queue)

    if (delayNanos != -1L && !cancelActiveTask && !queue.shutdown) {
      queue.scheduleAndDecide(task, delayNanos, recurrence = true)
    }

    if (queue.futureTasks.isNotEmpty()) {
      readyQueues.add(queue)
    }
  }

懒得在代码里写注释了,哈哈哈哈哈哈。个人觉得这里需要结合起来看比较省时间,所以总结一下大致的逻辑

在梳理结构之前需要知道在线程池的清理任务task是进入了TaskQueue队列里,再有就是TaskQueue需要放到TaskRunner内才能够运行。

TaskQueue自身有一个activeTask的对象,代表着正在运行executing的任务。而Task队列futureTasks,以Task的执行时间作为排序。

TaskRunner自身则是带有了两个队列,两个都是TaskQueue的队列busyQueues和readyQueues

  1. 一个Task进入到TaskQueue中做好排序,进入TaskQueue的futureTask队列中;
  2. TaskQueue会调用TaskQueue的kickCoodinator方法,把自己加入到TaskRunner的readyQueue中。运行Runnable;
  3. 处理任务Task会分为1+3步一是awaitTaskToRun,三是beforeRun、runTask、afterRun
  4. awaitTaskToRun方法中会遍历readyQueue队列,队列的头部task;
  5. awaitTaskToRun从readyQueue队列中取TaskQueue,并取Queue首个Task,如果计划执行时间晚于当前时间(也就是时间差<=0)即可执行该Task;
  6. 找到时间合适的Task开始执行beforeRun;
  7. beforeRun传入参数是即将运行的Task,Task自身有一个成员变量指向自己所属的TaskQueue,所以在方法中可以拿到TaskQueue,将Task从TaskQueue的futureTask中移除掉,设置成TaskQueue的activeTask,代表着对TaskQueue而言,该Task不再是future,而是active。然后将Task所属的Queue从TaskRunner的readyQueues队列中移除,放到busyQueue中,代表正在运行的队列;
  8. runTask是最好理解的,运行Task方法并拿到下一次的运行时间。传入到afterRun方法中
  9. 在afterRun方法中将当前Task的所属Queue从busyQueues中移除。先对下一次运行时间做一下判断,不为-1的就重新加入当前Queue的队列中。最后只要当前Queue的队列不为空,就可以继续放入到TaskRun的readyQueue中

后话

对于普通的开发者而言,okhttp的连接池和平时的开发工作直接关系不大。了解这个的用处可能还是拿来在面试的时候吹牛逼。

但是对于我个人来说,当初是看到了连接池清理的逻辑才了解到response是需要close的,对这个知识点的印象非常深刻。刚好最近想着开始写博客,就以这个为第一篇分享内容吧。

写得不好的地方还请各位客官多多指点了

本次源码参考是okhttp4.4.1版,现在okhttp都是用kotlin来写的了~
参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值