preface
When an APP has a large number of server-side interface invocation requests, someone might want to be able to specify that some of the request tasks have a higher priority and can initiate requests first. Or you can assign it a low priority and request it later. It would be convenient to prioritize these requests so that they can be executed in priority order.
Unfortunately, the OkHttp library does not allow developers to prioritize requests. In order to support scheduling by priority, the business side needs to maintain the request task queue by itself, and then execute the request tasks in sequence through the Call#execute method. However, the disadvantage of this scheme is that the original request queue management of OkHttp cannot be utilized, and it is intrusive to the business side. Or with a custom ExecutorService for the Dispatcher, the queue uses PriorityBlockingQueue, but only if the task is executed and the core thread count is full. Tasks can only be prioritized by entering the PriorityBlockingQueue, which is also intrusive.
Can developers prioritize requests with just one line of code while leveraging OkHttp’s original request queue management and not needing to extend the custom configuration? This can be done with the help of the OkOne library.
How to set up
First integrate OkOne library, see github.com/chidehang/O… .
This integration allows you to set the request priority in a single line of code:
// Create a Request Request
Request request = Request.Builder().url(api).build();
// Set a priority for Request
OkOne.setRequestPriority(request, priority);
Copy the code
One caveat: Call#enqueue initiates a request task. If the number of runningAsyncCalls in OkHttp does not reach the maximum number of concurrent requests, the task will be executed immediately. Otherwise, the task will wait temporarily in readyAsyncCalls waiting queue. Therefore, priorities are only meaningful for tasks that are queued for execution.
Results demonstrate
- Create multiple requests and set priorities randomly
val N = 10
val requests = arrayOfNulls<Request>(N)
val r = Random(System.currentTimeMillis())
for (i in 0 until N) {
// Randomly generate the request priority
val priority = r.nextInt(20) - 10
// Prints logs
LogUtils.d(TAG, "$i= >$priority")
requests[i] = Request.Builder()
.url(api)
// TagEntity Record creation sequence and priority, which are only used for subsequent information printing
.tag(TagEntity(i + 1, priority))
.build()
// Set the priority for Request
OkOne.setRequestPriority(requests[i], priority)
}
Copy the code
- Make Settings that make it easy to view the results
val client = OkHttpClient.Builder().eventListener(object : EventListener() {
override fun requestHeadersStart(call: Call) {
// Prints a log for each Request executing a network Request
val tag = call.request().tag() as TagEntity?
LogUtils.d(TAG, "requestHeadersStart: $tag")
}
}).build()
// Set the maximum number of concurrent requests to 1, just for verification purposes
client.dispatcher.maxRequests = 1
Copy the code
Here, the creation order and priority of the request task are printed for each request request header, and the maximum number of concurrent requests is limited to 1.
- Requests are created in the order requested
for (i in 0 until N) {
// Call enqueue to queue for executionclient.newCall(requests[i]!!) .enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response){}})}Copy the code
- View logs to verify the actual order of requests
You can see that the actual order of network requests is not the order of creation and enqueue, but scheduling by priority. Note that if the first task has a lower priority, it will also be initiated immediately, since the maximum concurrency limit has not been reached and the request will be initiated immediately.
The principle of analyzing
OkHttp request queue source analysis
OkHttp provides two methods to initiate request tasks: enqueue (asynchronous) and Execute (synchronous). Tasks executed through enqueue are managed by Dispatcher.
OkHttp enqueue queue
RealCall#enqueue:
override fun enqueue(responseCallback: Callback) {
/ /...
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
Copy the code
This method creates AsyncCall, holds Callback, and calls enQueue of Dispatcher
Dispatcher#enqueue:
internal fun enqueue(call: AsyncCall) {
synchronized(this) {
// First add the call to the queue
readyAsyncCalls.add(call)
/ /...
}
// Schedule the request task
promoteAndExecute()
}
Copy the code
The enQueue method first adds the call to the readyAsyncCalls queue and does not initiate the request immediately.
private val readyAsyncCalls = ArrayDeque()
ReadyAsyncCalls is an ArrayDeque two-ended queue that does not support sorting by priority.
Moving on to the promoteAndExecute method:
Dispatcher#promoteAndExecute:
private fun promoteAndExecute(a): Boolean {
this.assertThreadDoesntHoldLock()
// This is used to record requests that are executed as secondary tasks
val executableCalls = mutableListOf<AsyncCall>()
val isRunning: Boolean
synchronized(this) {
val i = readyAsyncCalls.iterator()
// Iterate through the queue
while (i.hasNext()) {
val asyncCall = i.next()
// Determine whether the number of tasks in the current request reaches the threshold
if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
if (asyncCall.callsPerHost.get() > =this.maxRequestsPerHost) continue // Host max capacity.
ExecutableCalls and runningAsyncCalls remove tasks from the pending queue and move them to executableCalls and 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]
// Commit tasks to the thread pool in turn
asyncCall.executeOn(executorService)
}
return isRunning
}
Copy the code
Here, the pending tasks are pulled out and executed until the maximum number is reached. You can see that the request tasks are executed in first-in, first-out order.
When a request task ends, the FINISHED method is called to notify:
Dispatcher#finished:
private fun <T> finished(calls: Deque<T>, call: T) {
/ /...
synchronized(this) {
// Remove call from runningSyncCalls
if(! calls.remove(call))throw AssertionError("Call wasn't in-flight!")
/ /...
}
// Call the promoteAndExecute method again to check for the remaining tasks in readyAsyncCalls
val isRunning = promoteAndExecute()
/ /...
}
Copy the code
OkHttp request priority implementation analysis
1. Modify Request
Start by adding a member variable to the Requestpriority.This allows the business side to easily assign priority to the Request.
Modify readyAsyncCalls
As you can see from the above analysis, OkHttp will temporarily store request tasks that cannot be initiated immediately in a double-ended queue of readyAsyncCalls, so you need to replace the type of readyAsyncCalls with a queue that supports priority sorting.
You can override key methods like add, remove, get, and iterate by inheriting ArrayDeque:
public class PriorityArrayDeque<E> extends ArrayDeque<E> implements Deque<E> {
private LinkedList<E> queue;
/ /...
// Override key methods
/ /...
}
Copy the code
When adding elements, compare their size and maintain their order in the collection.
Modify AsyncCall
The type of element cached in readyAsyncCalls is AsyncCall. To make it easier to compare the size of elements, we need to have AsyncCall inherit the Comparable interface and implement the compareTo method.
During comparison in compareTo method, the originalRequest is obtained through AsyncCall#getRequest method, and then the priority set by the business side is obtained for priority comparison.
To summarize, replace it with a prioritized collection via hook readyAsyncCalls. Let AsyncCall implement Comparable for comparison sorting.