OkHttp源码解析(构建者模式、责任链模式、主线流程)

1,243 阅读9分钟

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战


在分析OkHttp的核心流程已经核心类之前,我们先搞清楚两个概念,一个是OkHttpClient和Request在创建时所使用的构建者模式;另外一个则是负责响应处理的拦截器模式;

OkHttpClient/Request的构建者模式解析

基本概念

构建者(又称建造者)模式允许我们使用多个简单的对象一步一步构建成一个复杂的对象。

概念解释

如果你要装修房子,你就会要考虑这个房子的整体设计怎么做,用地中海风格?欧美风格?纯中式风格?墙面是刷白色的漆?贴瓷砖?还是用其他颜色?水电安装怎么处理?防水怎么处理?主卧怎么设计?阳台怎么设计?当你开始装修的时候,你就发现你要处理的事情实在太多,所以你想了一个办法,找了一家装修公司的人帮你做设计。

他们给你提供了一些默认的参数,比如,就用纯中式的风格,墙面使用硅藻泥,防水使用A方案设计,水电使用B方案设计等等;你觉得他们给你提供的默认风格都挺好的,唯一你觉得不好的就是他们这一整套设计里,阳台用的是“封闭式”设计,而你希望用“开放式”设计,所以你决定其他的都使用默认值,唯独这个阳台使用你提出的“开放式”设计。

  • 优点:我们不需要再关心装修房子时的每个细节;因为它提供了默认值;
  • 缺点:除了装修施工的钱,我们还需要额外付出装修设计的钱;

接下来再将这个例子带入到OkHttp源码中去看待:

为什么OkHttpClient以及Request使用构建者模式来创建?

当一个类的内部数据过于复杂的时候,比如OkHttpClient中包含Dispatcher,Proxy,ProxySelector,CookieJar,Cache,SocketFactory等等对象,Request中也包含了HttpUrl,method,Headers,RequestBody,tag等对象;这些对象说多不多,说少也不少,但它们都有一个共同点:很多参数都是不需要我们传递的,比如这两个对象在具体构建时的代码如下:

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

OkHttpClient和Request中虽然包含了多个参数,但这些参数对于用户来说,是可传可不传的,在上述代码中只有在构建Requset时通过Builder.url(url).build()传入了一个url参数;这就相当于,如果我们不传入参数,构建者模式会帮我们创建默认的参数;如果传入参数,构建者模式则会用我们传入的替换默认的参数;

  • 优点:我们不再关注OkHttpClient/Request的构建细节,不用每次构建时都传入大量参数,只需要传递我们关心的参数,非常易用;

  • 缺点:整个代码看上去会有“代码冗余”的感觉,对于不了解设计模式的人来说会觉得生涩难懂;

构建者模式简易代码实现

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 + '\'' +
                '}';
    }

上述代码就是一个非常简易的构建者模式代码,我们会发现class Builder和House3内部的属性完全一致,会给人一种“代码冗余”的感觉,而且不懂该设计模式的人更会觉得生涩;

OkHttp责任链模式解析

基本概念

责任链模式(Chain of Responsibility Pattern)为请求创建一个接收者对象的链。这种模式让请求的发送者和接收者进行解耦。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果当前接收者无法处理该请求,则会传递到下一个接收者,依此类推;

概念解释

如果你非常想念你异地的女朋友,你决定这个周末无论如何都要去她的城市见她,于是你查询了去她城市的交通方式:飞机,高铁,大巴,租车,骑摩托这五种方式(方便的程度按照顺序来),于是你先买飞机票,如果买到了,你就不考虑其他方式,如果没买到,就选择买高铁,如果买到了高铁票就不考虑大巴,如果高铁票也没有,则考虑大巴,如果买到大巴票就不选择租车,如果买不到再租车......依次类推;只要在某一个节点被处理了(比如买到了飞机票),就不再继续选择后面的节点,反之则继续走向下一个节点。

  • 优点:我们拥有多种“选择”,只要其中一个满足,就不用再继续深挖,浪费更多的时间;

  • 缺点:需要先耗费一定的时间,做出一个预想方案以及方案的各个节点;如果不做方案,直接购买了飞机票,而且恰好买到了,则时间更省,但如果买不到,你就得重新去规划接下来怎么办。可能浪费更多的时间;

责任链简易代码实现

public interface IBaseTask {
    // 参数1:任务节点是否有能力执行;参数2:下一个任务节点
    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()){
            // 抛出异常
        }
        if(index >= iBaseTaskList.size()){
            return;
        }
        IBaseTask iBaseTaskResult = iBaseTaskList.get(index);// 第一次取得是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("拦截器任务节点1处理了.....");
            return;
        }else{
            iBaseTask.doAction(isTask,iBaseTask);
        }
    }
}

这个简易代码实现,只是做了一个TaskOne类,如果各位同学需要添加多个Task,可按照TaskOne类的实现方式,多实现几个;

最终调用方式:

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

接下来再将这个例子带入到OkHttp源码中去看待:

为什么OkHttp使用责任链模式处理响应

当一个请求从OkHttp框架发出时,根据业务或者后台一些设置,我们都有可能进行一些其他处理;比如:请求在发送出去时,我们可以先验证这个请求是否合理;合理则继续发送,在发送的过程中,又有可能先去判断一下这个请求是否有现成的缓存数据,如果有则不去请求服务器,而是直接获取本地的缓存数据。这样做的好处是能在请求发出去之前处理更多的相关验证,缓存等工作,节省了去服务器请求所带来的流量,时间消耗;

下面代码块为OkHttp添加拦截器的过程;已经做了相应的注释;搞定了责任链模式后,就能更加清晰的分析出整个拦截器的调用过程;

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // 添加自定义的拦截器,比如发送请求的时候,我要先验证发出去的参数是否按照客户端与服务器的约定进行了适当包装与加密;如果添加了则继续,没通过则直接打回;
    interceptors.addAll(client.interceptors());
    // OkHttp自带的重试与重定向拦截器;
    interceptors.add(retryAndFollowUpInterceptor);
    // OkHttp自带的桥拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // OkHttp自带的缓存拦截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // OkHttp自带的链接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    // OkHttp自带的请求服务拦截器
    interceptors.add(new CallServerInterceptor(forWebSocket));
	// 根据责任链模式的概念,每个拦截器都需要与下一个拦截器有引用,以下代码就是为了能够让添加进来的拦截器依次执行;
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
  • 优点:如果我们要添加新的拦截点,只需要自定义自己的拦截器,通过OkHttpClient方便的add进去;

  • 缺点:不懂责任链模式的开发者,会觉得这个责任链模式代码的调用关系难以梳理;

OkHttp主线流程分析

第一条主线-入队操作

// 构建OkHttpClient
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
// 构建Request
Request request = new Request.Builder().url(url).build();
// 通过request创建RealCall对象,RealCall为Call的具体实现类
Call call = okHttpClient.newCall(request);
// 入队,添加回调
call.enqueue(new Callback() { // 此处省略Callback回调函数 }

入队流程图

第二条主线-网络访问操作

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // 添加自定义拦截器
    interceptors.addAll(client.interceptors());
    // 重试和重定向拦截器
    interceptors.add(retryAndFollowUpInterceptor);
    // 桥拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 缓存拦截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // 连接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    // 请求服务器拦截器
    interceptors.add(new CallServerInterceptor(forWebSocket));
	// 责任链设计模式调用代码
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    return chain.proceed(originalRequest);
  }

image.png

运行请求的线程池解析

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;
}
  • 核心线程数:0,最大的线程数:Integer.MAX_VALUE;

  • 3/4参数:当线程池中的线程数量大于核心线程数,空闲线程就会等待60s才会被终止,如果小于则会立即终止;

  • SynchronousQueue:阻塞队列;这个队列没有任何内部容量;(注意:这个定义也有点模糊,因为队列还是存在取和删除的操作)

  • 先确认一个概念,线程池中拥有自己的内部队列,一般情况下,初始化线程池的时候会创建空闲的线程,如果有任务进来,可以使用线程池中的空闲线程执行任务,执行完又变成空闲线程,这样就是为了减少线程的创建和销毁,提高性能;而这个内部队列就是一个缓冲地带,如果没有足够的线程去处理任务时,则将任务放到队列中,按照队列先进先出的方式来执行;但这样就有一个问题,如果我提交的任务希望它能够立即执行怎么办?这就是使用SynchronousQueue的原因;由于SynchronousQueue内部没有任何容量,也就意味着当任务过多的时候,线程池必须给我开启新线程,而不是放入队列里等待;但这种情况下就需要将线程池的最大线程池设置为Integer.MAX_VALUE,以防止新的任务无法被处理;

为什么OkHttp的访问速度更快?

那么为什么OkHttp的请求线程池要这么设计呢? 根本就在于OkHttp在处理请求的时候使用了两个队列来维护请求,在这种情况下更加的可控,而不是自己维护完队列,线程池里再维护一次它内部的队列,这样会导致请求可能出现相应的延迟;这也是为什么OkHttp的请求速度会更快的原因之一;

其他原因:

  1. OkHttp使用Okio进行数据传输,Okio封装了io/nio;性能更高;

  2. 线程池和连接池的使用;

  3. keep-alive机制的支持;(其实这个在其他框架也能设置)

拦截器详解

重试和重定向拦截器

RetryAndFollowUpInterceptor:该拦截器中主要创建了一个StreamAllocation对象,这个对象负责管理Socket连接池(这个连接池中的参数未来可能会更改(来自官方说明),目前最多可容纳的是5个空闲连接,保持5分钟)和请求地址(比如是否是Htpps);同时负责请求的重试和重定向工作;

桥拦截器

BridgeInterceptor:该拦截器主要负责在request上添加请求头配置信息,比如content-type,Accept-Encoding等等;

缓存拦截器

CacheInterceptor:该拦截器主要为了流量以及访问速度上的优化,如果有缓存则获取缓存,不再去访问服务器,同时它也需要对缓存进行维护;

连接拦截器

ConnectInterceptor:负责与服务器进行连接,该拦截器获取了StreamAllocation对象,并得到一个RealConnection(Socket连接的封装);

请求服务器拦截器

CallServerInterceptor:负责向服务器写入请求数据,读取服务器发送过来的数据,并使用构建者模式将返回的数据构建成Response对象;

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 小新聊Android 』,不定期分享原创知识
  3. 同时可以期待后续文章ing🚀