阅读 777
像白话文一样,深入理解OkHttp源码

像白话文一样,深入理解OkHttp源码

前言

本人待在一家普通公司很多年了。做着一些非常普通的项目。一直知道IT行业,不是在学习的路上,就是在被淘汰的路上。有一颗进大厂的心,想寻找有梦想的兄弟,一起进阶学习,让我们的孤独少一些。这是我14-19年的历程,也是开源控件ShadowLayout(star2.2k)作者

19后我觉得T型发展很重要,期间学会了web开发,后端入门,以及简单学习了unity。但最近收到很多大厂的内推,和一些19年的小伙伴都进大厂了给了我很大触发,为什么!?为什么我差一步要放弃呢。从9月开始,我回头认真收拾Android知识,及用最通俗的白话文去理解他们,并整理一份全面的面试质料。这次我一定不会输。如果你和我有共同目标,一起放手去干吧。这是我的Android交流群:209010674。如果你需要的话。

一、OkHttp简单介绍

电视剧里一般能成为大侠,都从一本简单的武功秘籍开始。那我们先来看段简单的代码 一个简单OkHttp的get请求。

//创建OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .readTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10,TimeUnit.SECONDS)
        //添加一个自定义的网络日志拦截器
        .addInterceptor(new HttpLogInterceptor())
        .build();

//创建网络请求对象Request对象及配置参数
Request request = new Request.Builder()
        .url("url")
        .header("headKey","headValue")
        .get()
        .build();

//进行网络请求
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {}

    @Override
    public void onResponse(Call call, Response response) throws IOException {}
});
复制代码

想必这些大家都非常的清楚,那怎么让这些印在脑海里呢,还不用死记硬背呢,接下来看看下面这张图(这里借用了别的博主一张图。觉得他比喻成快递行业很形象,接下来会以我的思路去讲解):

75ac96d529294fb79f825abe4e22aea4_tplv-k3u1fbpfcp-watermark.png

这里用退货的比喻加上快递员形容的很形象。从这里再加上OkHttp的简单请求,我们很容知道有哪些类:

  • OkHttpClient:进入快递点,构建OkHttpClient对象
  • Request:网络请求参数配置,就像寄快递填快递单号,才知道用什么快递,寄送到哪里去
  • Call:快递小哥的角色
  • Callback:网络回调,相当于,你告诉了他电话号码,等发货成功或丢失都会回调告诉你
  • enqueue:使用异步方法进行网络请求。则相当于,快递小哥进行了货物运送

二、OkHttpClient

看到OkHttpClient.Builder就知道是建筑者模式,也有人称之为是生成器模式。看下源码就知道了,我知道大家都不喜欢看代码,那就来张图片吧(参数太多,就截前面几个。反正我也知道大家不想看)

1.png 参数实在太多,这也是为什么用建造者模式的原因。比如我们定义一个方法,方法有10个参数,那么我们调用这个方法时,要严格按照方法里参数的类型和顺序去调用,真的比较麻烦。使用建造者模式,要传参只需要点一下,不点的话就使用默认值。
这里介绍几个参数:

  • Dispatcher:调度器
  • List interceptors:拦截器
  • List networkInterceptors:网络拦截器
  • int readTimeout:读取的超时时间

先别管他们是干嘛的,后面会介绍。这里只要有个概念就行了。

三、Request

看到 Request.Builder也是建筑者模式,从上面简单的get请求,就知道他是用来网络请求的配置参数,那么接下来介绍几个主要参数:

  • .url("url"):请求的网络url
  • .header("key","value"):头部参数,一般登录者的token都会放在这里
  • .get():确定是get请求

题外话: 当你想自己封装一个OkHttp网络请求,你越把细节搞清楚,越能封装的好用,这里稍微提下header

  • 我们经常会提到断点续传和下载,其实就是利用了header
// 通过以下设置,这里currentLength就是你已经下载的文件length
.header("RANGE", "bytes=" + currentLength + "-");
// 然后在下载文件流里使用true,就能达到断点下载了。不使用true就是从新下载。
fos = new FileOutputStream(file, true);
复制代码
  • 网络缓存
//1、
// 有网络的时候的在线缓存
// temp 在线缓存时间,
// 场景:比如并发量大的项目,而且是首页banner不长换的,那么获取banner这个接口的数据
// 会被缓存下来。比如temp = 3600,那么在接下1小时都会读取缓存数据
.header("Cache-Control", "public, max-age=" + temp)

//2、
// 无网络时的离线缓存
// 场景:很多新闻类的app,当没有网络打开时,还是会显示上一次打开的数据
.header("Cache-Control", "public, only-if-cached, max-stale=" + temp)
复制代码

这里提到这吧,毕竟是源码分析,跑题了。

四、Call

call 就是我们之前讲的“快递小哥”,看源码Call是个接口,我们从newCall点进去,其实是生成了Call接口的实现类RealCall

public Call newCall(Request request) {
    return new RealCall(this, request, false);
}
复制代码

看看Call接口,我们就知道它的具体作用,老样子,上图片。看到图片后,我们就知道,它其实就是处理网络请求的方法和网络请求的一些状态。

2.png

五、异步发送请求 RealCall.enqueue

5.1、call.enqueue

这里就是异步请求,相信大家都知道这段代码啥意思

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});
复制代码

点进enqueue,咋一看,并不复杂

public void enqueue(Callback responseCallback) {
    synchronized(this) {
        //executed:中文翻译为已执行的。从代码可以看出这里是防止一次调用进行多次请求
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }

        this.executed = true;
    }
    
    this.captureCallStackTrace();
    this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
复制代码

this.captureCallStackTrace();

capture:捕捉
StackTrace:堆栈踪迹
从字面上大概能猜想到这个方法是捕捉请求的堆栈踪迹的

继续点进去

private void captureCallStackTrace() {
    Object callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()");
    this.retryAndFollowUpInterceptor.setCallStackTrace(callStackTrace);
}
复制代码

从代码可以看到,他获取当前接口的堆栈踪迹对象放进了retryAndFollowUpInterceptor,拦截器里。那么接下来我们看看retryAndFollowUpInterceptor拦截器。

5.2、RetryAndFollowUpInterceptor

我们都知道okHttp有拦截器,这里则是使用设计模式里的责任链模式。责任链模式:是一种处理请求的模式,让多个处理器都有机会处理该请求。在这里拦截器只处理与自己相关的业务逻辑。

题外话:  要实现拦截器只要继承Interceptor类,并重写intercept(Chain chain)拦截方法就行了,最后在okHttpClient.addInterceptor()下就好了。比如开发中,登录成功后,会有登录者token,此时就可以使用拦截器,为以后每个接口头部参数添加token。再比如,开发测试中,我们可以打印每一个网络请求的详细情况。比如实现一个网络日志打印拦截器,伪代码如下:

@Override
public synchronized okhttp3.Response intercept(Chain chain) throws IOException {
    Request request = chain.request();

    long startTime = SystemClock.elapsedRealtime();
    okhttp3.Response response = chain.proceed(chain.request());
    long endTime = SystemClock.elapsedRealtime();
    long duration = endTime - startTime;

    //url
    String url = request.url().toString();
    log("┌───────Request Start────────────────────────────");
    log("请求方式 -->>" + request.method() + ": " + url);
    log("请求方式 -->>"+"Time:" + duration + " ms");
    //headers
    Headers headers = request.headers();
    if (null != headers) {
        for (int i = 0, count = headers.size(); i < count; i++) {
            if (!headerIgnoreMap.containsKey(headers.name(i))) {
                log("请求头部 -->>" + headers.name(i) + ": " + headers.value(i));
            }
        }
    }

    //param
    RequestBody requestBody = request.body();
    String paramString = readRequestParamString(requestBody);
    if (!TextUtils.isEmpty(paramString)) {
        log("请求参数 -->>" + paramString);
    }

    //response
    ResponseBody responseBody = response.body();
    String responseString = "";
    if (null != responseBody) {
        if (isPlainText(responseBody.contentType())) {
            responseString = readContent(response);
        } else {
            responseString = "other-type=" + responseBody.contentType();
        }
    }
    log("请求返回 -->>" + responseString);
    log("└───────Request End─────────────────────────────" + "\n" + "-");
    return response;
}
复制代码

以上拦截器无需看太仔细,你只要知道在拦截方法里,你可以获得Request和ResponseBody,那还有什么不能做的呢。哎呀,又啰嗦了。跑题了。回到我们的主题上。

RetryAndFollowUpInterceptor就是负责重试,和请求重定向的的拦截器,在这里用到StreamAllocation,主要是获取网络连接的RealConnection,如果需要重定向就release,关闭套接字等资源,开启重试和重定向的“新”的请求。我们继续往里看其重写的intercept方法:

public Response intercept(Chain chain) throws IOException {
    //获取请求体
    Request request = chain.request();
    //这个StreamAllocation是个核心类。这里简单介绍,后面会讲
    //参数1:OkHttpClient 的连接池
    //参数2:请求地址
    //参数3:堆栈追踪的对象
    //StreamAllocation的作用:
    //1、处理不同Connection的复用功能,协议流的可复用性和ConnectionPool连接池的处理
    //2、给请求HttpCodec流处理输入输出
    //3、StreamAllocation关联了一个Call请求的所有周期,判断Call请求的Connection是否被占用,其他Call能不能使用该Connection
    this.streamAllocation = new StreamAllocation(this.client.connectionPool(), this.createAddress(request.url()), this.callStackTrace);
    //重定向次数
    int followUpCount = 0;
    Response priorResponse = null;
    
    //这里的条件一直为true,除非调用
    //call.cancle(),此方法里调用了-->retryAndFollowUpInterceptor.cancel()-->canceled = true(进而条件为false,不会往下进行)。这里还调用了streamAllocation.cancel();代码继续追踪下去,最终是调用了-->codecToCancel.cancel();其实就是调用了RealConnection的取消方法

    while(!this.canceled) {
        Response response = null;
        boolean releaseConnection = true;

        try {
            //把网络返回响应体Response拦截下来
            response = ((RealInterceptorChain)chain).proceed(request, this.streamAllocation, (HttpCodec)null, (RealConnection)null);
            //释放Connection的标识,在try catch后面的finally里,在这里是不释放
            releaseConnection = false;
        } catch (RouteException var13) {
            //recover的其实就是判断是否需要重定向,比如你把接口都写错了,就不需要了。可以理解为里面是判断这个错造成的原因
            if (!this.recover(var13.getLastConnectException(), false, request)) {
                throw var13.getLastConnectException();
            }

            releaseConnection = false;
            continue;
        } catch (IOException var14) {
            boolean requestSendStarted = !(var14 instanceof ConnectionShutdownException);
            if (!this.recover(var14, requestSendStarted, request)) {
                throw var14;
            }

            releaseConnection = false;
            continue;
        } finally {
            if (releaseConnection) {
                this.streamAllocation.streamFailed((IOException)null);
                this.streamAllocation.release();
            }

        }
        //第一次循环priorResponse 必定为空。那么response则是try{}里获取的放入了streamAllocation的response
        if (priorResponse != null) {
            //如果是第2次及以后到达这里的,则是以先前的priorResponse生成新的response。这里可以理解为就是要重定向的response。其实在这个方法的最后,是赋值了的priorResponse = response;所以还是第一次放入streamAllocation的response,也就是说这里只要条件达到,那么重定向会一直循环下去
            response = response.newBuilder().priorResponse(priorResponse.newBuilder().body((ResponseBody)null).build()).build();
        }
        //followUpRequest下方有其实就是判断是否有重连或者重定向条件的response,比如这里获取code码等条件进行判断
        Request followUp = this.followUpRequest(response);
        //对返回需要重试或重定向的Request 进行判断,如果为空,说明这次请求是成功的,那么返回当前的response
        if (followUp == null) {
            if (!this.forWebSocket) {
                this.streamAllocation.release();
            }

            return response;
        }
        //关闭当前response资源,内部其实调用的是Util.closeQuietly(this.source());source其实就是BufferedSource,看到这些我们就可以联想到流和字节资源
        Util.closeQuietly(response.body());
        //每一次进行重试或重定向都++
        ++followUpCount;
        //当次数超过20次时,失败
        if (followUpCount > 20) {
            this.streamAllocation.release();
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }
        
        //以下都是达不到重试或重定向的条件,那么我们把错误抛出去
        if (followUp.body() instanceof UnrepeatableRequestBody) {
            this.streamAllocation.release();
            throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
        }

        if (!this.sameConnection(response, followUp.url())) {
            this.streamAllocation.release();
            this.streamAllocation = new StreamAllocation(this.client.connectionPool(), this.createAddress(followUp.url()), this.callStackTrace);
        } else if (this.streamAllocation.codec() != null) {
            throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?");
        }

        request = followUp;
        priorResponse = response;
    }

    this.streamAllocation.release();
    throw new IOException("Canceled");
}
复制代码

followUpRequest方法伪代码:

private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) {
        throw new IllegalStateException();
    } else {
        ...
        switch(responseCode) {
        case 307:
        case 308:
                ...
                return null;
        case 300:
        case 301:
        case 302:
        case 303:
            if (!this.client.followRedirects()) {
                return null;
            } else {
                String location = userResponse.header("Location");
                if (location == null) {
                    return null;
                } else {
                    ...
                    return requestBuilder.url(url).build();
                    }
                }
            }
        case 401:
            return this.client.authenticator().authenticate(route, userResponse);
        case 407:
            ...
            return this.client.proxyAuthenticator().authenticate(route, userResponse);
        case 408:
            if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
                return null;
            }
            return userResponse.request();
        default:
            return null;
        }
    }
}
复制代码

5.3、StreamAllocation

StreamAllocation是在重试和重定向拦截器RetryAndFollowUpInterceptor里初始化的。其实这个类主要是在拦截器ConnectInterceptor发起真正网络请求时用到,大致我们可以看成网络请求优化,

先在这里了解这个类更容易理解全文。在了解这个类的时候,我们来看看题外话(兄弟们,这是我查阅大量资料找来的题外话)。这个可是划重点的知识点,(指黑板)

  • http2.0 使用 多路复用 的技术,多个 stream 可以共用一个 socket 连接。每个 tcp连接都是通过一个 socket 来完成的,socket 对应一个 host 和 port,如果有多个stream(即多个 Request) 都是连接在一个 host 和 port上,那么它们就可以共同使用同一个 socket ,这样做的好处就是 可以减少TCP的一个三次握手的时间。
在OkHttp里负责连接的就是RealConnection,上文也说道了,我们取消请求可以用call.cancle()。
其实最终就是调用了StreamAllocation里的cancle(),也就是调用了realConnection.cancle()
复制代码
  • Http通信执行网络请求Call, 需要在连接上建立新的流Stream(OkHttp里面的流是HttpCodec)执行,我们将StreamAllocation看做是桥梁,作用是为一次请求寻找连接并建立,进而完成通信

那么接下来我们来看看StreamAllocation的伪代码。

public final class StreamAllocation {
    public final Address address;
    private Route route;
    private final ConnectionPool connectionPool;
    private final Object callStackTrace;
    private final RouteSelector routeSelector;
    private int refusedStreamCount;
    private RealConnection connection;
    private boolean released;
    private boolean canceled;
    private HttpCodec codec;


    public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
        ...
        RealConnection resultConnection = this.findHealthyConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
        //建立OkHttp连接流,点进去其实new的是一个Http2Codec
        HttpCodec resultCodec = resultConnection.newCodec(client, this);
    }

    //寻找一个健康的RealConnection,
    private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
        while(true) {
            ...
            //findHealthyConnection其实调用了findConnection,只不过判断了是否健康
            //判断RealConnection candidate是否健康.isHealthy点进去也就是判断当前socket是否自愿关闭,等健康因素
            if (candidate.isHealthy(doExtensiveHealthChecks)) {
                return candidate;
            }
            //如果RealConnection是不健康的,那么把它当前的socket关闭资源
            this.noNewStreams();
        }
    }

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
        Route selectedRoute;
        synchronized(this.connectionPool) {
            if (this.released) {
                throw new IllegalStateException("released");
            }

            if (this.codec != null) {
                throw new IllegalStateException("codec != null");
            }

            if (this.canceled) {
                throw new IOException("Canceled");
            }

            RealConnection allocatedConnection = this.connection;
            //如果当前赋值的this.connection不为null;且安全的话,那么返回当前RealConnection
            //this.connection是全局变量,第一次这里肯定为空。
            if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
                return allocatedConnection;
            }
            //1、第一次获取Connection
            //从连接池中获取一个可用连接。这里的路由Route传的是null,表示从当前已解析过的连接池中寻找。
            Internal.instance.get(this.connectionPool, this.address, this, (Route)null);
            //Internal.instance.get()最终调用的是OkHttpClient的pool.get(),即连接池ConnectionPool里的get(),代码如下:
//            @Nullable RealConnection get(Address address, okhttp3.internal.connection.StreamAllocation streamAllocation, Route route) {
//                ...
//                for (RealConnection connection : connections) {
//                    if (connection.isEligible(address, route)) {
//                        这里把connection传给了streamAllocation,我们可以当成就是赋值给我们的this.connection
//                        streamAllocation.acquire(connection);
//                        return connection;
//                    }
//                }
//                return null;
//            }

            if (this.connection != null) {
                return this.connection;
            }

            selectedRoute = this.route;
        }

        if (selectedRoute == null) {
            selectedRoute = this.routeSelector.next();
        }

        RealConnection result;
        synchronized(this.connectionPool) {
            if (this.canceled) {
                throw new IOException("Canceled");
            }

            //2、第二次获取Connection
            //这里传入了Router,先看下Router的概念
            //Router路由:来确定具体要怎么连接。一个Router包含proxy代理,ip地址和端口port。而同样的端口和同样的代理类型下有不同ip的多个Router组合
            //才结合http2.0的多路复用的概念。这里一直循环this.routeSelector.next()寻找一个可复用的Connection
            Internal.instance.get(this.connectionPool, this.address, this, selectedRoute);
            if (this.connection != null) {
                return this.connection;
            }

            this.route = selectedRoute;
            this.refusedStreamCount = 0;

            //3、第三次获取Connection
            //上面2种方式都获取不到,说明当前连接池没有可复用的,多路路由里也找不到复用的,那么我们创建新的Connection
            result = new RealConnection(this.connectionPool, selectedRoute);
            this.acquire(result);
        }

        result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
        this.routeDatabase().connected(result.route());
        Socket socket = null;
        synchronized(this.connectionPool) {
            //把新建的Connecttion放入到连接池中
            Internal.instance.put(this.connectionPool, result);
            if (result.isMultiplexed()) {
                socket = Internal.instance.deduplicate(this.connectionPool, this.address, this);
                result = this.connection;
            }
        }

        Util.closeQuietly(socket);
        return result;
    }

    ...

    public void noNewStreams() {
        Socket socket;
        synchronized(this.connectionPool) {
            socket = this.deallocate(true, false, false);
        }
        //这里就是之前如果不是健康连接,找到连接套接字,并关闭资源
        Util.closeQuietly(socket);
    }

    ...
    
}
复制代码

5.4、dispatcher

说完了 this.captureCallStackTrace();再来看看this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));还记得那些简单代码吗?如下:

public void enqueue(Callback responseCallback) {
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }

        this.executed = true;
    }
    
    this.captureCallStackTrace();
    this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
复制代码

首先this.client.dispatcher()获取的是Dispatcher调度器的对象。用来调度,管理这些请求的。来看看Dispatcher,伪代码如下:

public final class Dispatcher {
  //最大请求数,意思一家快递公司有64辆车,如果还需要车,就只能等待其他车空闲了
  private int maxRequests = 64;
  //一个主机最大允许请求数。意思前往同一城市运送快递的车辆为5量,如果还有快递要运往,只能等5量中有空闲
  private int maxRequestsPerHost = 5;
  //线程池,懒加载即被调用时才创建
  private @Nullable ExecutorService executorService;
  //准备队列,也就是超过了maxRequests 和 maxRequestsPerHost 的请求将被添加到这里
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  //异步队列,也就是没超过最大请求数,将放在这里
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //同步队列,没超过最大请求数,将添加在这里
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }

  public Dispatcher() {
  }

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      //创建一个线程池,60秒是keepAliveTime。即线程干完活不会立即消失,有活干的时候会复用还存活的线程,性能优化
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }
...
}
复制代码

5.5、this.client.dispatcher().enqueue()

synchronized void enqueue(AsyncCall call) {
  //可以看到在比较了最大请求数之后,如果超过最大请求数,加入到readyAsyncCalls里等待请求,否则加入到runningAsyncCalls进行请求。
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}
复制代码

接下来看看readyAsyncCalls在什么地方被执行呢?点开源码:

private void promoteCalls() {
  //如果请求数大于等于最大请求数,说明没有空闲。那readyAsyncCalls必须继续等待
  if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
  //如果readyAsyncCalls自身为空,那么不需要执行,直接返回
  if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

  for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
    AsyncCall call = i.next();

    if (runningCallsForHost(call) < maxRequestsPerHost) {
      i.remove();
      runningAsyncCalls.add(call);
      //最终在这里被请求。
      executorService().execute(call);
    }
    if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
  }
}
复制代码

那么看到这里你会想promoteCalls()方法在什么时候去执行呢?让我们试想一下在什么条件下会触发他请求呢。那肯定得满足条件的时候啊,哈哈哈

  • 条件1:请求数不超过最大请求数。那我们很容易想到修改最大请求数maxRequests或maxRequestsPerHost
  • 条件2:等到了空闲的请求路线,即runningAsyncCalls里有请求被执行完毕了

点开源码也确实是在3个方法里被执行了,分别是setMaxRequests()、setMaxRequestsPerHost()和finished(AsyncCall call)

5.6、executorService().execute(call)

从5.5的源码我们知道,这里的call是AsyncCall,是RealCall里的内部类,初始化属性也都是RealCall里的属性,代码如下,他其实是个被命名的Runnable(意思会给这个线程setThreadName):

final class AsyncCall extends NamedRunnable {
  //请求结果的Callback回调
  private final Callback responseCallback;
  ...
  //请求地址
  String host() {
    return originalRequest.url().host();
  }

  //请求体
  Request request() {
    return originalRequest;
  }

  RealCall get() {
    return RealCall.this;
  }
}
复制代码

从5.4可以看到executorService()生成的是ThreadPoolExecutor线程池,并通过execute方法加入到workQueue工作队列中,并执行。这里是线程池的概念就略过了。直接看execute()方法

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}
复制代码

因为我们知道AsyncCall 就是runnable,其实执行的就是他里面的run方法。通过父类NamedRunnable我们知道。其实现的是NamedRunnable里定义的抽象方法execute(),并由AsyncCall 去实现,如下:

final class AsyncCall extends NamedRunnable {
...
  @Override protected void execute() {
    boolean signalledCallback = false;
    try {
      //通过getResponseWithInterceptorChain()获得了最终请求返回的Response
      Response response = getResponseWithInterceptorChain();
      if (retryAndFollowUpInterceptor.isCanceled()) {
        signalledCallback = true;
        responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
      } else {
        signalledCallback = true;
        responseCallback.onResponse(RealCall.this, response);
      }
    } catch (IOException e) {
      if (signalledCallback) {
        // Do not signal the callback twice!
        Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
      } else {
        responseCallback.onFailure(RealCall.this, e);
      }
    } finally {
      //不管接口成功或失败,都会将这个请求Call移除
      client.dispatcher().finished(this);
    }
  }
}
复制代码

我们看到这里都是通过getResponseWithInterceptorChain()得到Response的。那么我们看看这个方法都做了什么。

Response getResponseWithInterceptorChain() throws IOException {
  // 可以看到这里加里很多拦截器
  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));
  //最后带上请求数据,通过RealInterceptorChain以及这些拦截器的作用,最终网络请求返回了我们的Response
  Interceptor.Chain chain = new RealInterceptorChain(
      interceptors, null, null, null, 0, originalRequest);
  return chain.proceed(originalRequest);
}
复制代码

致此,异步请求就将完了。以上讲解的是异步请求。用的方法是call.enqueue()。同步请求是怎么样的呢?我相信你看懂了上面写的我只要放2段代码,你就焕然大悟了

//同步请求够简单吧。再看看RealCall
Response execute = call.execute();
复制代码

RealCall的execute();同步的实现方法基本和异步的AsyncCall里的实现方法一模一样

@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    client.dispatcher().finished(this);
  }
}
复制代码

题外话又如约而至了,题外话:  看到了调度器的知识,如果我们自己封装一个OkHttp该怎么取消网络请求呢?
这里放上一段我之前封装的取消网络请求方法:

//tag取消网络请求
public void cancleOkhttpTag(String tag) {
    //封装,当然okHttpClient要使用单例咯,获取调度器。
    Dispatcher dispatcher = okHttpClient.dispatcher();
    synchronized (dispatcher) {
        //dispatcher.queuedCalls()获得的就是readyAsyncCalls
        for (Call call : dispatcher.queuedCalls()) {
            //获取到Request,当然在请求网络的时候,okHttp带了个点tag的参数。这个时候传入的tag也就和那个tag对比
            if (tag.equals(call.request().tag())) {
                call.cancel();
            }
        }
        //看runningCalls也知道获取到什么了吧
        for (Call call : dispatcher.runningCalls()) {
            if (tag.equals(call.request().tag())) {
                call.cancel();
            }
        }
    }
}
复制代码

六、拦截器

前面为了源码分析更清楚已经把RetryAndFollowUpInterceptor放在5.2小结讲过了。这里我们看到了getResponseWithInterceptorChain()方法里,有那么多拦截器。接下来我们一个一个看吧:

6.1、BridgeInterceptor

其实我们运用OkHttp进行网络请求,是作者在内部高度封装,简化我们的操作。实现了真正的网络请求。先来简单说下桥接拦截器的概念和作用:

  • 把应用请求转换成网络请求(就是添加网络请求必要的响应头,在web上按F12,选中network监听网络可以看到网络请求带很多头部信息)
  • 把网络响应转换成应用响应(就是转换成对用户友好的响应,支持gzip压缩也有提高性能的意思)
  • 因为Cookie也在头部信息里,系统是个空实现,如果自定义cookieJar,这里也算成一个作用吧分辨用户信息
//为什么说他是空实现呢,首先BridgeInterceptor里有段代码是:
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());

//我们看看系统的cookies得到的是什么呢?首先OkHttpClient里的默认值是cookieJar = CookieJar.NO_COOKIES;
//再来看CookieJar接口loadForRequest默认实现是

CookieJar NO_COOKIES = new CookieJar() {
  @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
  }

  @Override public List<Cookie> loadForRequest(HttpUrl url) {
    //没看错这里就是一个空数组
    return Collections.emptyList();
  }
};
复制代码

现在来看看我们的BridgeInterceptor的源码:

public final class BridgeInterceptor implements Interceptor {
    private final CookieJar cookieJar;

    public BridgeInterceptor(CookieJar cookieJar) {
        this.cookieJar = cookieJar;
    }

    @Override public Response intercept(Chain chain) throws IOException {
        Request userRequest = chain.request();
        Request.Builder requestBuilder = userRequest.newBuilder();

        //首先获取到用户应用请求的请求体
        RequestBody body = userRequest.body();
        if (body != null) {
            MediaType contentType = body.contentType();
            if (contentType != null) {
                //添加头部信息Content-Type,请求类型
                requestBuilder.header("Content-Type", contentType.toString());
            }

            long contentLength = body.contentLength();
            //对contentLength进行判断用哪种请求解析方式,Content-Length或Transfer-Encoding
            if (contentLength != -1) {
                requestBuilder.header("Content-Length", Long.toString(contentLength));
                requestBuilder.removeHeader("Transfer-Encoding");
            } else {
                requestBuilder.header("Transfer-Encoding", "chunked");
                requestBuilder.removeHeader("Content-Length");
            }
        }

        //如果头部没有添加请求的主机站点,添加上
        if (userRequest.header("Host") == null) {
            requestBuilder.header("Host", hostHeader(userRequest.url(), false));
        }

        //保持请求长连接。这里的意思是tcp连接要经过三次握手,四次挥手。长连接可以理解为多次网络请求可以减少握手和挥手的次数。不但能减少耗时而且还能提高性能
        if (userRequest.header("Connection") == null) {
            requestBuilder.header("Connection", "Keep-Alive");
        }


        //接收gzip压缩。就像我们传大型文件,压缩完是不是传递快些,占用的资源少
        boolean transparentGzip = false;
        if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
            transparentGzip = true;
            requestBuilder.header("Accept-Encoding", "gzip");
        }

        List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
        if (!cookies.isEmpty()) {
            //Cookie:分辨用户信息
            requestBuilder.header("Cookie", cookieHeader(cookies));
        }

        //请求的用户信息,在什么操作系统,或什么浏览器等用户平台上
        if (userRequest.header("User-Agent") == null) {
            requestBuilder.header("User-Agent", Version.userAgent());
        }

        //经过以上这些操作,将用户请求转换成我们的网络请求
        Response networkResponse = chain.proceed(requestBuilder.build());

        HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
        Response.Builder responseBuilder = networkResponse.newBuilder()
                .request(userRequest);

        if (transparentGzip
                && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
                && HttpHeaders.hasBody(networkResponse)) {
            GzipSource responseBody = new GzipSource(networkResponse.body().source());
            Headers strippedHeaders = networkResponse.headers().newBuilder()
                    .removeAll("Content-Encoding")
                    .removeAll("Content-Length")
                    .build();
            responseBuilder.headers(strippedHeaders);
            responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
        }
        
        //经过以上操作最终生成对用户友好的请求响应
        return responseBuilder.build();
    }
    ...
}
复制代码

6.2、ConnectInterceptor

这个拦截器非常简洁,主要是调用StreamAllocation,这个类我们已经在“5.3、StreamAllocation”讲过了,而大部分工作都让StreamAllocation给做了

public final class ConnectInterceptor implements Interceptor {
    public final OkHttpClient client;

    public ConnectInterceptor(OkHttpClient client) {
        this.client = client;
    }

    @Override public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        //获取网络请求的请求体
        Request request = realChain.request();
        //获取我们核心的网络请求类StreamAllocation,也是在RetryAndFollowUpInterceptor中初始化的
        StreamAllocation streamAllocation = realChain.streamAllocation();
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        //调用streamAllocation的newStream()方法。大家可以去看下5.3的StreamAllocation,这个方法就是找findHealthConnect,
        //就是多路复用,3次寻找,有复用的Connection就复用,没有的话就新建,并且把新建的加入连接池ConnectionPool中
        HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
        RealConnection connection = streamAllocation.connection();
        
        //交给下个拦截器使用
        return realChain.proceed(request, streamAllocation, httpCodec, connection);
    }
}
复制代码

题外话(重点):  可能这里大家会有点懵逼,包括我自己,为什么RetryAndFollowUpInterceptor中已经进行了网络请求,ConnectInterceptor怎么还来一遍,为什么RetryAndFollowUpInterceptor里的网络请求不多路复用呢?别急,我们直接看一张网络请求拦截器的图:

1.png

从这张图看,我们来总结下:
首先是从左边开始进行网络请求的方向

  • 首先是进入到RetryAndFollowUpInterceptor拦截器,但是我们看源码,知道这个拦截器是等Response回来之后才知道是重试还是重定向,在请求的时候是不会走这里的拦截的
  • 其次进入BridgeInterceptor拦截器,把我们的用户请求,添加头部信息,变成真正的网络请求
  • CacheInterceptor拦截器后面会讲,这里先理清这个逻辑
  • 最后进入到ConnectInterceptor拦截器里,调用StreamAllocation的newstream,寻找健康的RealConnection。进行真正的网络请求

到这里网络请求就结束了,来看看右边的网络响应Response返回的方向

  • 首先是在ConnectInterceptor返回网络请求的Response给下一级拦截器,这里说的下一级,肯定就是CacheInterceptor了。
  • 如果配置了缓存走完后就到了BridgeInterceptor里,将网络返回的Response处理后返回友好的用户Response给用户。
  • 最后到了Response到了RetryAndFollowUpInterceptor拦截器里,如果请求是成功的,那么就会直接返回Response,无限循环停止。否知就会判断是否要重试或者重定向,满足条件后,再次进行网络请求。

大概就是这样的流程了。本小节的题外话到此为止了,是不是有种没听够的感觉

6.3、CacheInterceptor

之前在"三、Request"部分讲了我们通过配置头部参数head可以达到在线缓存和离线缓存2种方式,接下来来看看我们的缓存拦截器,伪代码如下:

public final class CacheInterceptor implements Interceptor {
    //使用DisLruCache封装的一个Cache.Cache在OkHttp源码的okhttp3包下,不在cache包下
    final InternalCache cache;

    public CacheInterceptor(InternalCache cache) {
        this.cache = cache;
    }

    @Override public Response intercept(Chain chain) throws IOException {
        //用当前请求去获取缓存的Response,点进去可以发现其实用的是request.url作为key获取的
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;

        long now = System.currentTimeMillis();
        //缓存策略,它将确定是使用网络还是缓存,还是两者都使用
        CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        Request networkRequest = strategy.networkRequest;
        Response cacheResponse = strategy.cacheResponse;

        if (cache != null) {
            cache.trackResponse(strategy);
        }
        //如果缓存Response不为null,但是缓存策略的结果是缓存不适用,那么关闭缓存
        if (cacheCandidate != null && cacheResponse == null) {
            closeQuietly(cacheCandidate.body());
        }

        //如果网络和缓存都为null,那么我们直接返回code504,超时
        if (networkRequest == null && cacheResponse == null) {
            return new Response.Builder()
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_1)
                    .code(504)
                    .message("Unsatisfiable Request (only-if-cached)")
                    .body(Util.EMPTY_RESPONSE)
                    .sentRequestAtMillis(-1L)
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();
        }

        //如果网络为null,缓存有效的话,那使用缓存
        if (networkRequest == null) {
            return cacheResponse.newBuilder()
                    .cacheResponse(stripBody(cacheResponse))
                    .build();
        }

        Response networkResponse = null;
        try {
            networkResponse = chain.proceed(networkRequest);
        } finally {
            //如果我们在I/O其他地方崩溃不要泄露缓存,及时关闭它
            if (networkResponse == null && cacheCandidate != null) {
                closeQuietly(cacheCandidate.body());
            }
        }


        if (cacheResponse != null) {
            //返回HTTP_NOT_MODIFIED304的意思是服务器判断此接口数据是否更新过,没更新过的话,那么使用缓存
            //否则关闭缓存
            if (networkResponse.code() == HTTP_NOT_MODIFIED) {
                Response response = cacheResponse.newBuilder()
                        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                        .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                        .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                        .cacheResponse(stripBody(cacheResponse))
                        .networkResponse(stripBody(networkResponse))
                        .build();
                networkResponse.body().close();

                // Update the cache after combining headers but before stripping the
                // Content-Encoding header (as performed by initContentStream()).
                cache.trackConditionalCacheHit();
                cache.update(cacheResponse, response);
                return response;
            } else {
                closeQuietly(cacheResponse.body());
            }
        }

        //使用网络响应
        Response response = networkResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();

        if (cache != null) {
            if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
                // 将数据缓存到本地缓存到本地
                CacheRequest cacheRequest = cache.put(response);
                return cacheWritingResponse(cacheRequest, response);
            }

            if (HttpMethod.invalidatesCache(networkRequest.method())) {
                try {
                    cache.remove(networkRequest);
                } catch (IOException ignored) {
                    // The cache cannot be written.
                }
            }
        }

        return response;
    }

    ...
}
复制代码

这里大致总结以下:

  • 读取候选缓存。
  • 根据候选缓存创建缓存策略。
  • 根据缓存策略,如果不进行网络请求,而且没有缓存数据时,报错返回错误码 504。
  • 根据缓存策略,如果不进行网络请求,缓存数据可用,则直接返回缓存数据。
  • 缓存无效,则继续执行网络请求。
  • 通过服务端校验后,缓存数据可以使用(返回 304),则直接返回缓存数据,并且更新缓存。
  • 读取网络结果,构造 response,对数据进行缓存。

这里其实有很多知识点,不懂的地方我们可以去好好学习下;DiskLruCache完全解析,可以看这篇文章。说了缓存拦截器这么多,为什么我们在使用OkHttp的时候都没有使用过缓存呢,因为这里和CookieJar也是个空实现,看下面代码:

//首先在RealCall里,添加了
interceptors.add(new CacheInterceptor(client.internalCache()));

//然后来到OkHttpClient里:
InternalCache internalCache() {
  //这个cache是在new OkHttpClient().cache() 传入的,显然,用的时候大家都没定义过
  //cache为空的话,最终就会用internalCache,其实这个变量就是Builder里传入的。没传默认为空
  return cache != null ? cache.internalCache : internalCache;
}

//都为空的情况下,来看看缓存拦截器的一段代码,client.internalCache()为null,那么下方的cache就是null
Response cacheCandidate = cache != null
     ? cache.get(chain.request())
      : null;
复制代码

又到了题外话的环节了:
还记得我们之前提过的在线缓存和离线缓存吗?代码如下

//在设置之前我们还要提供一个缓存文件就是初始化我们的OkHttpClient的时候使用.cache()
//这是我封装EasyOk时的代码
//设置缓存文件路径,和文件大小
.cache(new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 50 * 1024 * 1024))



//1、
// 有网络的时候的在线缓存
// temp 在线缓存时间,
// 场景:比如并发量大的项目,而且是首页banner不长换的,那么获取banner这个接口的数据
// 会被缓存下来。比如temp = 3600,那么在接下1小时都会读取缓存数据
.header("Cache-Control", "public, max-age=" + temp)

//2、
// 无网络时的离线缓存
// 场景:很多新闻类的app,当没有网络打开时,还是会显示上一次打开的数据
.header("Cache-Control", "public, only-if-cached, max-stale=" + temp)
复制代码

注意我们使用头部文件去设置了缓存,其实不管是Request还是Response都使用了一个类,CacheControl。这个类里有一个方法,是获取头部信息Cache-Control里的设置的

//代码还是比较长,还是伪代码吧
public static CacheControl parse(Headers headers) {}
复制代码

知道这些以后,除了在头部信息设置之外,我们还可以利用拦截器,获取到Request进行设置,看段代码就明白了了

     Request request = new Request.Builder()
         .cacheControl(new CacheControl.Builder()
             .onlyIfCached()
             .build())
         .url("http://publicobject.com/helloworld.txt")
         .build();
复制代码

还记得CacheIntereptor里的CacheStrategy吗,即是缓存策略。OkHttp有2种缓存策略:

强制缓存:  需要服务器参与,服务器返回时间标识,如果时间不失效,则利用缓存。强制缓存有2个标识

  • Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
  • Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。

对比缓存:  需要服务器参与,服务器返回资源是否被修改,未修改返回code304使用缓存,否则不使用。对比缓存也有2个标识

  • Last-Modified:表示资源上次修改的时间。
  • ETag:是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码。如果再次请求,不同标识资源被修改

在这里我们大致对缓存策略有个了解吧

七、ConnectionPool连接池

我们都知道网络请求要经过3次握手和4次挥手,为了加快网络请求访问速度和提高性能,这个时候就要用到Http2.0的多路复用了。我们可以在一个请求head里添加keepalive,从而这次请求的RealConnection可以被复用并加入到ConnectionPool连接池中。

//ConnectionPool是用Deque保存RealConnection的
private final Deque<RealConnection> connections = new ArrayDeque<>();

//Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。
public ConnectionPool() {
  this(5, 5, TimeUnit.MINUTES);
}
复制代码

我们来看下他是怎么加入到Deque里的

void put(RealConnection connection) {
  assert (Thread.holdsLock(this));
  if (!cleanupRunning) {
    cleanupRunning = true;
    //如果清除闲置的线程还没走完则不运行这段代码,否则主动调用一次清除闲置线程
    executor.execute(cleanupRunnable);
  }
  //加入到Deque里
  connections.add(connection);
}
复制代码

好了到这里大概就完结了。不会再有题外话了。相信,你从头看下来会跟我有同样的感觉,不再害怕面试官问你OkHttp源码了。感谢你的浏览,和我一起并肩作战吧!

文章分类
Android
文章标签