OkHttp source code analysis (builder mode, responsibility chain mode, main process)

Posted by mass on Sun, 20 Feb 2022 16:10:10 +0100

Before analyzing the core processes of OkHttp and the core classes, let's clarify two concepts: one is the builder mode used by OkHttpClient and Request when they are created; The other is the interceptor mode responsible for response processing;

Analysis of builder mode of OkHttpClient/Request

Basic concepts

The builder (also known as the builder) pattern allows us to use multiple simple objects to build a complex object step by step.

Conceptual interpretation

If you want to decorate the house, you will have to consider the overall design of the house. How to do it in Mediterranean style? European and American style? Pure Chinese style? Is the wall painted white? Tile? Or another color? How to deal with water and electricity installation? How to deal with waterproof? How to design the master bedroom? How to design the balcony? When you start decoration, you will find that you have too many things to deal with, so you think of a way to find someone from a decoration company to help you design.

They provide you with some default parameters, such as pure Chinese style, diatom mud on the wall, scheme A for waterproof, scheme B for hydropower, etc; You think the default style they provide you is very good. The only thing you think is that in their whole set of design, the balcony uses the "closed" design, and you want to use the "open" design, so you decide to use the default value for others, but the balcony uses the "open" design you put forward.

  • Advantages: we don't need to care about every detail when decorating the house; Because it provides default values;
  • Disadvantages: in addition to the money for decoration and construction, we also need to pay extra money for decoration and design;

Next, take this example into the OkHttp source code:

Why does OkHttpClient and Request use builder mode to create?

When the internal data of a class is too complex, for example, OkHttpClient contains objects such as Dispatcher, Proxy, ProxySelector, CookieJar, Cache and SocketFactory, and Request also contains objects such as HttpUrl, method, Headers, RequestBody and tag; These objects say more or less, but they all have one thing in common: many parameters do not need to be passed. For example, the codes of the two objects during construction are as follows:

OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url(url).build();

Although OkHttpClient and Request contain multiple parameters, these parameters can be passed or not to users. In the above code, they can only be passed through builder when building Requset url(url). Build () passed in a URL parameter; This is equivalent to that if we do not pass in parameters, the builder pattern will help us create default parameters; If the parameter is passed in, the builder mode will replace the default parameter with the one we passed in;

  • Advantages: we no longer pay attention to the construction details of OkHttpClient/Request. We don't need to pass in a large number of parameters every time. We just need to pass the parameters we care about, which is very easy to use;

  • Disadvantages: the whole code looks like "code redundancy", which is difficult for people who don't understand design patterns;

Simple code implementation of builder pattern

public class House3 {

    private double height;
    private double width;
    private String color;

    public House3(Builder builder) {
        this.height = builder.height;
        this.width = builder.width;
        this.color = builder.color;
    }

    public static final class Builder{
        double height;
        double width;
        String color;

        public Builder(){
            this.height = 100;
            this.width = 100;
            this.color = "red";
        }

        public Builder addHeight(double height) {
            this.height = height;
            return this;
        }

        public Builder addWidth(double width) {
            this.width = width;
            return this;
        }

        public Builder addColor(String color) {
            this.color = color;
            return this;
        }

        public House3 build(){
            return new House3(this);
        }

    }

    @Override
    public String toString() {
        return "House{" +
                "height=" + height +
                ", width=" + width +
                ", color='" + color + '\'' +
                '}';
    }

The above code is a very simple builder mode code. We will find that the internal properties of class Builder and House3 are completely consistent, which will give people a feeling of "code redundancy", and people who do not understand the design mode will feel astringent;

Analysis of OkHttp responsibility chain model

Basic concepts

The Chain of Responsibility Pattern creates a chain of receiver objects for requests. This mode decouples the sender and receiver of the request. In this pattern, each recipient usually contains a reference to another recipient. If the current receiver cannot process the request, it will be delivered to the next receiver, and so on;

Conceptual interpretation

If you miss your girlfriend in a different place very much, you decide to go to her city to meet her anyway this weekend, so you inquire about the transportation modes to her city: plane, high-speed railway, bus, car rental and motorcycle (the degree of convenience is in order), so you buy the air ticket first. If you buy it, you don't consider other ways. If you don't buy it, Choose to buy high-speed rail. If you buy a high-speed rail ticket, you don't consider the bus. If you don't have a high-speed rail ticket, you consider the bus. If you buy a bus ticket, you don't choose to rent a car. If you can't buy another car... And so on; As long as a node is processed (such as buying an air ticket), you will no longer continue to select the following node. On the contrary, you will continue to move to the next node.

  • Advantages: we have many "choices". As long as one of them is satisfied, we don't have to continue to dig deeply and waste more time;

  • Disadvantages: it takes a certain amount of time to make an expected scheme and each node of the scheme; If you don't make a plan and buy the plane ticket directly, and you happen to buy it, you will save more time, but if you can't buy it, you have to plan again what to do next. More time may be wasted;

Simple code implementation of responsibility chain

public interface IBaseTask {
    // Parameter 1: whether the task node has the ability to execute; Parameter 2: next task node
    public void doAction(String isTask,IBaseTask iBaseTask);
}
public class ChainManager implements IBaseTask{
    private List<IBaseTask> iBaseTaskList = new ArrayList<>();
    public void addTask(IBaseTask iBaseTask){
        iBaseTaskList.add(iBaseTask);
    }
    private int index = 0;
    @Override
    public void doAction(String isTask, IBaseTask iBaseTask) {
        if(iBaseTaskList.isEmpty()){
            // Throw exception
        }
        if(index >= iBaseTaskList.size()){
            return;
        }
        IBaseTask iBaseTaskResult = iBaseTaskList.get(index);// The first achievement was taskone
        index++;
        iBaseTaskResult.doAction(isTask,iBaseTask);
    }
}
public class TaskOne implements IBaseTask{
    @Override
    public void doAction(String isTask, IBaseTask iBaseTask) {
        if("no".equals(isTask)){
            System.out.println("Interceptor task node 1 processed.....");
            return;
        }else{
            iBaseTask.doAction(isTask,iBaseTask);
        }
    }
}

This simple code implementation only makes a TaskOne class. If you need to add multiple tasks, you can implement more tasks according to the implementation method of TaskOne class;

Final call method:

ChainManager chainManager = new ChainManager();
chainManager.addTask(new TaskOne());
chainManager.addTask(new TaskTwo());
chainManager.addTask(new TaskThree());
chainManager.doAction("ok",chainManager);

Next, take this example into the OkHttp source code:

Why does OkHttp use the chain of responsibility pattern to handle responses

When a request is sent from the OkHttp framework, we may carry out some other processing according to the business or background settings; For example, when a request is sent, we can first verify whether the request is reasonable; If it is reasonable, continue to send. In the process of sending, it is possible to judge whether the request has ready-made cache data. If so, do not request the server, but directly obtain the local cache data. The advantage of this is that it can handle more related verification, caching and other work before the request is sent, saving the traffic and time consumption caused by the request to the server;

The following code block is the process of adding interceptors for OkHttp; Corresponding notes have been made; After completing the responsibility chain mode, we can more clearly analyze the calling process of the whole interceptor;

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // Add a custom interceptor. For example, when sending a request, I need to verify whether the sent parameters are properly packaged and encrypted according to the agreement between the client and the server; If it is added, continue. If it fails, call back directly;
    interceptors.addAll(client.interceptors());
    // OkHttp's own retry and redirect interceptor;
    interceptors.add(retryAndFollowUpInterceptor);
    // OkHttp's own bridge interceptor
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // OkHttp's own cache interceptor
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // OkHttp's own link interceptor
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    // OkHttp's own request service interceptor
    interceptors.add(new CallServerInterceptor(forWebSocket));
	// According to the concept of responsibility chain mode, each interceptor needs to be referenced with the next interceptor. The following code is to enable the added interceptors to execute in turn;
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
  • Advantages: if we want to add a new interception point, we only need to customize our own interceptor and add it conveniently through OkHttpClient;

  • Disadvantages: developers who do not understand the responsibility chain model will find it difficult to sort out the calling relationship of the responsibility chain model code;

OkHttp mainline process analysis

The first main line - team operation

// Build OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
// Build Request
Request request = new Request.Builder().url(url).build();
// Create a RealCall object through request. RealCall is the specific implementation class of Call
Call call = okHttpClient.newCall(request);
// Join the queue and add callback
call.enqueue(new Callback() { // The Callback function}

The second main line - network access operation

Response getResponseWithInterceptorChain() throws IOException {    // Build a full stack of interceptors.     List<Interceptor> interceptors = new ArrayList<>();    //  Add custom interceptors addAll(client.interceptors());    //  Retry and redirect interceptors add(retryAndFollowUpInterceptor);    //  Bridge interceptors add(new BridgeInterceptor(client.cookieJar()));    //  Cache interceptors add(new CacheInterceptor(client.internalCache()));    //  Connect interceptors add(new ConnectInterceptor(client));     if (!forWebSocket) {      interceptors.addAll(client.networkInterceptors());    }    //  Request server interceptors add(new CallServerInterceptor(forWebSocket)); 	//  The responsibility chain design pattern calls the code interceptor Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,        originalRequest, this, eventListener, client.connectTimeoutMillis(),        client. readTimeoutMillis(), client. writeTimeoutMillis());     return chain. proceed(originalRequest);  }

[image upload failed... (image-127bf-1636555761641)]

Thread pool resolution to run the request

public synchronized ExecutorService executorService() {  if (executorService == null) {    executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,        new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));  }  return executorService;}
  • Number of core threads: 0, maximum number of threads: integer MAX_ VALUE;

  • 3 / 4 parameter: when the number of threads in the thread pool is greater than the number of core threads, the idle thread will wait for 60s before being terminated. If it is less than, it will be terminated immediately;

  • SynchronousQueue: blocking queue; This queue does not have any internal capacity; (Note: this definition is also a little vague, because there are still fetch and delete operations in the queue)

  • First confirm a concept. The thread pool has its own internal queue. Generally, idle threads will be created when initializing the thread pool. If a task comes in, you can use the idle threads in the thread pool to execute the task, and then become idle threads after execution. This is to reduce the creation and destruction of threads and improve performance; The internal queue is a buffer zone. If there are not enough threads to process the task, the task will be put into the queue and executed according to the queue first in first out method; But there is a problem. What if I submit a task and hope it can be implemented immediately? This is why SynchronousQueue is used; There is no capacity in the SynchronousQueue, which means that when there are too many tasks, the thread pool must open new threads for me instead of waiting in the queue; But in this case, you need to set the maximum thread pool of the thread pool to integer MAX_ Value to prevent new tasks from being processed;

Why is OkHttp faster to access?

So why is OkHttp's request thread pool designed like this? The fundamental reason is that OkHttp uses two queues to maintain the request when processing the request. In this case, it is more controllable than maintaining the queue by itself. Maintain its internal queue in the thread pool again, which may lead to corresponding delays in the request; This is one of the reasons why OkHttp requests are faster;

Other reasons:

  1. OkHttp uses Okio for data transmission, which encapsulates io/nio; Higher performance;

  2. Use of thread pool and connection pool;

  3. Support of keep alive mechanism; (in fact, this can be set in other frameworks)

Detailed explanation of interceptor

Retry and redirect interceptors

RetryAndFollowUpInterceptor: the interceptor mainly creates a streamalallocation object, which is responsible for managing the Socket connection pool (the parameters in the connection pool may change in the future (from the official description). At present, it can accommodate up to 5 idle connections for 5 minutes) and the request address (such as whether it is Htpps); At the same time, it is responsible for the retry and redirection of requests;

Bridge interceptor

BridgeInterceptor: the interceptor is mainly responsible for adding request header configuration information on the request, such as content type, accept encoding, etc;

Cache interceptor

CacheInterceptor: this interceptor is mainly used to optimize traffic and access speed. If there is a cache, it will obtain the cache and no longer access the server. At the same time, it also needs to maintain the cache;

Connection interceptor

ConnectInterceptor: it is responsible for connecting with the server. The interceptor obtains the streamalallocation object and a RealConnection (encapsulation of Socket connection);

Request server interceptor

CallServerInterceptor: it is responsible for writing the request data to the server, reading the data sent by the server, and using the builder mode to build the returned data into a Response object;

Topics: Android OkHttp Design Pattern