前言
当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)中等待调度执行。因此优先级只对排队待执行的任务有意义。
效果演示
- 创建多个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)
}
- 进行便于查看结果的设置
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。
- 按照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) {
}
})
}
- 查看日志验证实际请求顺序
可以看到真正进行网络请求的顺序并不是按照创建和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,以便进行比较排序。