OkOne-如何给okhttp的请求设置优先级

2,931 阅读5分钟

前言

当APP中有发起大量服务端接口调用请求时,或许有人希望能够指定某些请求任务的优先级较高,可以优先发起请求。或者指定其为低优先级,在靠后的位置再发起请求。那么如果可以给这些请求设置优先级,使之能够按优先级顺序执行的话就很方便了。

不过遗憾的是,OkHttp库不支持开发者给Request设置优先级。若要支持按优先级调度,则需要业务方自行维护请求任务队列,然后通过Call#execute方法依次执行请求任务,但该方案的弊端是不能利用OkHttp原有的请求队列管理,且对业务方具有侵入性。或者通过给Dispatcher设置自定义的ExecutorService,队列使用PriorityBlockingQueue,但该方式仅在执行任务且核心线程数满的情况下,任务进入PriorityBlockingQueue才能达到优先级调度的目的,且同样具有侵入性。

能否让开发者在利用OkHttp原有请求队列管理且不需要额外扩展自定义配置的基础上,只需要一行代码就可以给请求设置优先级呢?可以借助OkOne库来实现。

如何设置

首先集成OkOne库,集成步骤详见 github.com/chidehang/O…

集成后便可只用一行代码就能够设置请求优先级:

// 创建请求Request
Request request = Request.Builder().url(api).build();
// 给Request设置一个优先级
OkOne.setRequestPriority(request, priority);

有一点需要说明的是:Call#enqueue发起请求任务,若OkHttp内部的请求中队列(runningAsyncCalls)未达到最大并发数限制,则该任务会立即执行,否则任务会暂时在等待队列(readyAsyncCalls)中等待调度执行。因此优先级只对排队待执行的任务有意义。

效果演示

  1. 创建多个Request,并随机设置优先级
        val N = 10
        val requests = arrayOfNulls<Request>(N)
        val r = Random(System.currentTimeMillis())
        for (i in 0 until N) {
            // 随机生成请求优先级
            val priority = r.nextInt(20) - 10
            // 打印日志
            LogUtils.d(TAG, "$i => $priority")
            requests[i] = Request.Builder()
                    .url(api)
                    // TagEntity记录创建顺序和优先级,仅用于后续打印信息
                    .tag(TagEntity(i + 1, priority))
                    .build()
            // 给Request设置优先级
            OkOne.setRequestPriority(requests[i], priority)
        }
  1. 进行便于查看结果的设置
        val client = OkHttpClient.Builder().eventListener(object : EventListener() {
            override fun requestHeadersStart(call: Call) {
                // 在每个Request执行网络请求时打印日志
                val tag = call.request().tag() as TagEntity?
                LogUtils.d(TAG, "requestHeadersStart: $tag")
            }
        }).build()
        // 设置最大并发请求数为1,仅为了方便验证
        client.dispatcher.maxRequests = 1

这里在每个请求request header时打印该请求任务的创建顺序和优先级,以及限制了最大并发数为1。

  1. 按照Request创建顺序请求
        for (i in 0 until N) {
            // 依次调用enqueue入队等待执行
            client.newCall(requests[i]!!).enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {}
                @Throws(IOException::class)
                override fun onResponse(call: Call, response: Response) {
                }
            })
        }
  1. 查看日志验证实际请求顺序

p1

可以看到真正进行网络请求的顺序并不是按照创建和enqueue的顺序,而是按照优先级进行调度。 注意,若第一个任务的优先级较低,也会立即发起,因为未达到最大并发数限制,会立即发起请求。

原理剖析

OkHttp请求队列源码分析

大家知道OkHttp提供两个方法发起请求任务:enqueue(异步方式)和execute(同步方式),通过enqueue执行的任务会由Dispatcher来管理调度。

接下来进入OkHttp的enqueue执行流程查看相关源码:

RealCall#enqueue:

  override fun enqueue(responseCallback: Callback) {
    // ···
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }

该方法中创建了AsyncCall,持有Callback,又调用Dispatcher的enqueue方法

Dispatcher#enqueue:

  internal fun enqueue(call: AsyncCall) {
    synchronized(this) {
      // 首先将call添加进待请求队列
      readyAsyncCalls.add(call)

      // ···
    }
    // 调度请求任务
    promoteAndExecute()
  }

enqueue方法首先将call加入readyAsyncCalls待请求队列,没有立即发起请求。

private val readyAsyncCalls = ArrayDeque()

readyAsyncCalls是一个ArrayDeque双端队列,不支持按优先级排序功能。

接着看promoteAndExecute方法:

Dispatcher#promoteAndExecute:

  private fun promoteAndExecute(): Boolean {
    this.assertThreadDoesntHoldLock()

    // 用于记录当次要执行的请求任务
    val executableCalls = mutableListOf<AsyncCall>()
    val isRunning: Boolean
    synchronized(this) {
      val i = readyAsyncCalls.iterator()
      // 遍历待请求队列
      while (i.hasNext()) {
        val asyncCall = i.next()

        // 判断当前请求中任务数是否达到阈值
        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

        // 将任务从待请求队列中移除,转移到executableCalls和runningAsyncCalls中
        i.remove()
        asyncCall.callsPerHost.incrementAndGet()
        executableCalls.add(asyncCall)
        runningAsyncCalls.add(asyncCall)
      }
      isRunning = runningCallsCount() > 0
    }

    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      // 依次交由线程池执行任务
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }

这里不断将待请求任务取出执行,直到达到最大限制数。可以看到请求任务是按照先进先出的顺序执行的。

而当一个请求任务结束时,会再调用finished方法进行通知:

Dispatcher#finished:

  private fun <T> finished(calls: Deque<T>, call: T) {
    // ···
    synchronized(this) {
      // 将call从runningSyncCalls中移除
      if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
      // ···
    }

    // 再次调用promoteAndExecute方法,检查readyAsyncCalls中剩余任务
    val isRunning = promoteAndExecute()

    // ···
  }

OkHttp请求优先级实现分析

一.修改Request

首先给Request添加一个成员变量priority 这样业务方就可以方便的给Request赋值优先级。

二.修改readyAsyncCalls

从上文分析中可以知道OkHttp会将未能立即发起的请求任务暂存在readyAsyncCalls的双端队列中,因此需要将readyAsyncCalls的类型替换成支持优先级排序的队列。

可以通过继承ArrayDeque,重写添加、移除、获取、迭代等关键方法:

public class PriorityArrayDeque<E> extends ArrayDeque<E> implements Deque<E> {
    private LinkedList<E> queue;
    // ···
    // 重写关键方法
    // ···
}

在添加元素时,比较元素大小,维护元素在集合中的顺序。

三.修改AsyncCall

readyAsyncCalls中缓存的元素类型时AsyncCall,为了方便比较元素大小,因此需要让AsyncCall继承Comparable接口,实现compareTo方法。

在compareTo方法中比较时,通过AsyncCall#getRequest方法获取originalRequest,继而获取业务方设置的priority,便可以进行优先级比较。

总结一下,就是通过hook readyAsyncCalls,将它替换成支持优先级排序的集合。让AsyncCall实现Comparable,以便进行比较排序。