Android小知识-剖析OkHttp中的五个拦截器(下篇)

本平台的文章更新会有延迟,大家可以关注微信公众号-顾林海,包括年底前会更新kotlin由浅入深系列教程,目前计划在微信公众号进行首发,如果大家想获取最新教程,请关注微信公众号,谢谢

在上一节介绍了缓存拦截器CacheInterceptor的缓存机制,内部采用DiskLruCache来缓存数据,本节介绍剩下的两个拦截器,分别是ConnectInterceptor和CallServerInterceptor拦截器。

ConnectInterceptor拦截器

ConnectInterceptor是网络连接拦截器,我们知道在OkHttp当中真正的网络请求都是通过拦截器链来实现的,通过依次执行这个拦截器链上不同功能的拦截器来完成数据的响应,ConnectInterceptor的作用就是打开与服务器之间的连接,正式开启OkHttp的网络请求。

走进ConnectInterceptor的intercept方法:

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Request request = realChain.request();
        //标记1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ...
    }
复制代码

在标记1处可以看到从上一个拦截器中获取StreamAllocation对象,在讲解第一个拦截器RetryAndFollowUpInterceptor重试重定向的时候已经介绍过StreamAllocation,在RetryAndFollowUpInterceptor中只是创建了这个对象并没有使用,真正使用它的是在ConnectInterceptor中,StreamAllocation是用来建立执行HTTP请求所需要的网络组件,既然我们拿到了StreamAllocation,接下来看这个StreamAllocation到底做了哪些操作。

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        //标记1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ..
        //标记2
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        ...
    }
复制代码

在标记2处通过StreamAllocation对象的newStream方法创建了一个HttpCodec对象,HttpCodec的作用是用来编码我们的Request以及解码我们的Response。

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        //标记1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ...
        //标记2
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        //标记3
        RealConnection connection = streamAllocation.connection();
        ...
    }
复制代码

在标记3处通过StreamAllocation对象的connection方法获取到RealConnection对象,这个RealConnection对象是用来进行实际的网络IO传输的。

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        ...
        //标记1
        StreamAllocation streamAllocation = realChain.streamAllocation();
        ...
        //标记2
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        //标记3
        RealConnection connection = streamAllocation.connection();
        //标记4
        return realChain.proceed(request, streamAllocation, httpCodec, connection);
    }
复制代码

标记4处是我们非常熟悉的代码了,继续调用拦截器链的下一个拦截器并将Request、StreamAllocation、HttpCodec以及RealConnection对象传递过去。

总结: 首先ConnectInterceptor拦截器从拦截器链获取到前面传递过来的StreamAllocation,接着执行StreamAllocation的newStream方法创建HttpCodec,HttpCodec对象是用于处理我们的Request和Response。 最后将刚才创建的用于网络IO的RealConnection对象,以及对于服务器交互最为关键的HttpCodec等对象传递给后面的拦截器。

从上面我们了解了ConnectInterceptor拦截器的intercept方法的整体流程,从前一个拦截器中获取StreamAllocation对象,通过StreamAllocation对象的newStream方法创建了一个HttpCodec对象,我们看看这个newStream方法具体做了哪些操作。

    public HttpCodec newStream(
            OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
        ...
        try {
            //标记1
            RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                    writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
            HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
            ...
        } catch (IOException e) {
            throw new RouteException(e);
        }
    }
复制代码

我们可以看到在标记1处创建了一个RealConnection对象,以及HttpCodec对象,这两个对象在上面已经介绍过了,RealConnection对象是用来进行实际的网络IO传输的,HttpCodec是用来编码我们的Request以及解码我们的Response。

通过findHealthyConnection方法生成一个RealConnection对象,来进行实际的网络连接。

findHealthyConnection方法:

    private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                                 int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
                                                 boolean doExtensiveHealthChecks) throws IOException {
        while (true) {
            RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
                    pingIntervalMillis, connectionRetryEnabled);
            ...
            synchronized (connectionPool) {
                if (candidate.successCount == 0) {
                    return candidate;
                }
            }
            ...
            return candidate;
        }
    }
复制代码

在方法中开启了while循环,内部的同步代码块中判断candidate的successCount如果等于0,说明整个网络连接结束并直接返回candidate,而这个candidate是通过同步代码块上面的findConnection方法获取的。

    private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                                 int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
                                                 boolean doExtensiveHealthChecks) throws IOException {
        while (true) {
            RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
                    pingIntervalMillis, connectionRetryEnabled);
            synchronized (connectionPool) {
                if (candidate.successCount == 0) {
                    return candidate;
                }
            }
            //标记1
            if (!candidate.isHealthy(doExtensiveHealthChecks)) {
                noNewStreams();
                continue;
            }
            return candidate;
        }
    }
复制代码

往下看标记1,这边会判断这个连接是否健康(比如Socket没有关闭、或者它的输入输出流没有关闭等等),如果不健康就调用noNewStreams方法从连接池中取出并销毁,接着调用continue,继续循环调用findConnection方法获取RealConnection对象。

通过不停的循环调用findConnection方法来获取RealConnection对象,接着看这个findConnection方法做了哪些操作。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //标记1
            releasedConnection = this.connection;
            ...
            if (this.connection != null) {
                result = this.connection;
                releasedConnection = null;
            }
            ...
        }
        ...
        return result;
    }
复制代码

在findConnection方法的标记1处,尝试将connection赋值给releasedConnection,然后判断这个connection能不能复用,如果能复用,就将connection赋值给result,最后返回这个复用的连接。如果不能复用,那么result就为null,我们继续往下看。

   private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //标记1
            ...
            //标记2
            if (result == null) {
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
            ...
        }
        ...
        return result;
    }
复制代码

如果result为null说明不能复用这个connection,那么就从连接池connectionPool中获取一个实际的RealConnection并赋值给connection,接着判断connection是否为空,不为空赋值给result。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //标记1
            ...
            //标记2
            if (result == null) {
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
            ...
        }
        ...
        //标记3
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        ...
        return result;
    }
复制代码

标记3处,拿到我们的RealConnection对象result之后,调用它的connect方法来进行实际的网络连接。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        Connection releasedConnection;
        ...
        synchronized (connectionPool) {
            ...
            //标记1
            ...
            //标记2
            if (result == null) {
                Internal.instance.get(connectionPool, address, this, null);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                } else {
                    selectedRoute = route;
                }
            }
            ...
        }
        ...
        //标记3
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        ...
        //标记4
        Internal.instance.put(connectionPool, result);
        ...
        return result;
    }
复制代码

在标记4处,进行真正的网络连接后,将连接成功后的RealConnection对象result放入connectionPool连接池,方便后面复用。

上面我们介绍了StreamAllocation对象的newStream方法的具体操作,接下来看看ConnectInterceptor拦截器中一个很重要的概念-连接池。

不管HTTP协议是1.1还是2.0,它们的Keep-Alive机制,或者2.0的多路复用机制在实现上都需要引入一个连接池的概念,来维护整个网络连接。OkHttp中将客户端与服务端之间的链接抽象成一个Connection类,而RealConnection是它的实现类,为了管理所有的Connection,OkHttp提供了一个ConnectionPool这个类,它的主要作用就是在时间范围内复用Connection。

接下来主要介绍它的get和put方法。

    @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
            if (connection.isEligible(address, route)) {
                streamAllocation.acquire(connection, true);
                return connection;
            }
        }
        return null;
    }
复制代码

在get方法中遍历连接池中的Connection,通过isEligible方法判断Connection是否可用,如果可以使用就会调用streamAllocation的acquire方法来获取所用的连接。

进入StreamAllocation的acquire方法:

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();
    //标记1
    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    //标记2
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }
复制代码

标记1处,从连接池中获取的RealConnection对象赋值给StreamAllocation的成员变量connection。

标记2处,将StreamAllocation对象的弱引用添加到RealConnection的allocations集合中去,这样做的用处是通过allocations集合的大小来判断网络连接次数是否超过OkHttp指定的连接次数。

put方法:

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
复制代码

put方法中在添加连接到连接池之前,会处理清理任务,做完清理任务后,将我们的connection添加到连接池中。

connection自动回收l利用了GC的回收算法,当StreamAllocation数量为0时,会被线程池检测到,然后进行回收,在ConnectionPool中有一个独立的线程,它会开启cleanupRunnable来清理连接池。

  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
         //标记1
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              //标记2
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };
复制代码

在run方法中是一个死循环,内部标记1处首次进行清理时,需要返回下次清理的间隔时间。标记2处调用了wait方法进行等待,等待释放锁和时间片,当等待时间过了之后会再次调用Runnable进行清理,同时返回下次要清理的间隔时间waitNanos。

标记2处的cleanup方法内部实现了具体的GC回收算法,该算法类似Java GC当中的标记清除算法;cleanup方法循环标记出最不活跃的connection,通过响应的判断来进行清理。

CallServerInterceptor拦截器

CallServerInterceptor拦截器主要作用是负责向服务器发起真正的网络请求,并获取返回结果。

CallServerInterceptor的intercept方法:

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
    ...
}
复制代码

intercept方法中先是获取五个对象,下面分别介绍这5个对象的含义。

  • RealInterceptorChain:拦截器链,真正进行请求的地方。

  • HttpCodec:在OkHttp中,它把所有的流对象都封装成了HttpCodec这个类,作用是编码Request,解码Response。

  • StreamAllocation:建立HTTP连接所需要的网络组件。

  • RealConnection:服务器与客户端的具体连接。

  • Request:网络请求。

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
    ...
    //标记1
    httpCodec.finishRequest();
    ...
}
复制代码

标记1处,调用httpCodec的finishRequest方法,表面网络请求的写入工作已经完成,具体网络请求的写入工作大家可以看源码,也就是标记1之上的代码。

网络请求的写入工作完成后,接下来就进行网络请求的读取工作。

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
    ...
    //网络请求一系列写入工作
    ...
    //向socket当中写入请求的body信息
    request.body().writeTo(bufferedRequestBody);
    ...
    //标记1:写入结束
    httpCodec.finishRequest();
    ...
    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      //读取网络写入的头部信息
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    ...
    //读取Response
    //标记1
    response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    ...
    return response;
}
复制代码

我们只取核心代码标记1,通过httpCodec的openResponseBody方法获取body,并通过build创建Response对象,最终返回Response对象。

到这里OkHttp的同步和异步请求、分发器,以及五个拦截器都已经介绍一边了,怎么说呢,OkHttp的源码实在太庞大了,要想全部理解需要花费很长时间,我只是整理出了OkHttp中几个比较重要的概念,了解它的整体脉络,这样你才能有条理的分析它的源码。


搜索微信“顾林海”公众号,定期推送优质文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值