前言
这是 okhttp
源码解析系列的第3篇了,应该也是最后一篇了。本章我们顺着第2篇剩下的知识继续进行关于拦截器链的源码解析。本篇我们介绍的就是后面的两个拦截器:ConnectInterceptor
和 CallServerInterceptor
了。如果你对于对于拦截器的知识还比较陌生或者还不知道 okhttp
执行流程的话,可以先参考我的前两篇博客 okhttp源码解析(一) 以及 okhttp源码解析(二) 后再来进行本章的学习。废话不多说,我们现在就开始进入正题吧。
拦截器链图
为了防止大家忘记了前面的知识,我们把这个图再次搬出来:
因为我们的拦截器对请求的处理都是在各自的 intercept 方法中的,所以我们接下来就从 ConnectInterceptor
的 intercept
方法开始入手吧。
ConnectInterceptor
这个拦截器叫做连接拦截器,在这里我们除了要解析 intercept 方法的源码之外,我们还要对 connectionPool
,也就是连接池做一个解析,这是一个用于管理我们连接的一个机制,在我们的连接拦截器中有着非常重要的意义。
intercept
接下来我们先看到 intercept
方法的源码:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;Request request = realChain.request();StreamAllocation streamAllocation = realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.boolean doExtensiveHealthChecks = !request.method().equals("GET");HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);RealConnection connection = streamAllocation.connection();return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
这个方法很短,但是内容一点也不少,因为里面涉及到的知识非常的多,其中 StreamAllocation
和 ConnectionPool
是这里面的两大重点,我们来一行行分析它,梳理下这个方法的大致流程。
首先看到代码第4行,StreamAllocation
对象从 realChain
中获得,不知道大家还有没有印象,我们的 StreamAllocation
最早是在 RetryAndFollowUpInterceptor
中创建的,但是它一直没有被使用,而是沿着拦截器链一路传下来,直到在连接拦截器中才真正地被使用。
接下来我们看到第8行,这里我们创建了一个 HttpCodec
对象,HttpCodec
这个类主要是用于对我们的 Request
进行编码以及对我们的 Response
进行解码的,它是通过 StreamAllocation
的 newStream
方法赋值的。
然后接着看到代码第9行,在这里我们又创建了一个 RealConnection
对象,通过 StreamAllocation
的 connection
方法赋值,这个对象非常关键,因为它是我们进行实际网络 IO 流传输的一个对象,我们可以看看这个方法,它非常的短:
public synchronized RealConnection connection() {
return connection;
}
可以看到它直接返回的就是 StreamAllocation
内部的成员变量 connection
。所以我们对它的重心就应该放在 connection
这个对象是在何处被赋值的。其实它在 newStream
方法中就被赋值完毕了。我们继续回到 intercept
方法。
在 intercept
方法的最后我们再次看到了我们非常熟悉的 proceed
方法了,它就是将我们前面处理好的对象传给下一个拦截器来进行处理了。
在了解完 intercept
方法的大体逻辑之后,我们接下来从局部开始分析这个方法的内部实现。首先我们看到第8行中 StreamAllocation
的 newStream
方法,它的源码如下所示:
public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();int readTimeout = chain.readTimeoutMillis();int writeTimeout = chain.writeTimeoutMillis();int pingIntervalMillis = client.pingIntervalMillis();boolean connectionRetryEnabled = client.retryOnConnectionFailure();try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);synchronized (connectionPool) {
codec = resultCodec;return resultCodec;}} catch (IOException e) {
throw new RouteException(e);}
}
我们先看到第10行,在这里我们创建了一个 RealConnection
类型的对象 resultConnection
,然后通过 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);// If this is a brand new connection, we can skip the extensive health checks.synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;}}// Do a (potentially slow) check to confirm that the pooled connection is still good. If it// isn't, take it out of the pool and start again.if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();continue;}return candidate;}
}
这个方法从名字上我们也可以大概知道这是为我们找出一个健康的连接。在 while
循环中,我们首先创建了一个 RealConnection
类型的对象 candidate
,它通过 findConnection
方法进行赋值,紧接着看到第10行,在同步块中,我们会判断 candidate
的 successCount
是否为 0,如果为 0 的话说明它是一个新连接,不必进行健康性检查,直接返回 RealConnection
对象即可。
接下来看到第17行的 if
判断,这个 if
会调用 RealConnection
的 isHealthy
方法来判断这个连接是否是健康的,如果不健康的话,就会执行 noNewStreams
方法来释放相应的资源然后重新执行 while
循环重新获取连接,如果是健康的话就直接返回 RealConnection
对象。
在分析完 findHealthyConnection
的主体之后,我们接下来看看方法里面对 RealConnection
对象赋值的 findConnection
方法里面做了什么:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;RealConnection result = null;Route selectedRoute = null;Connection releasedConnection;Socket toClose;synchronized (connectionPool) {
......if (this.connection != null) {
// We had an already-allocated connection and it's good.result = this.connection;releasedConnection = null;}if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!releasedConnection = null;}if (result == null) {
// Attempt to get a connection from the pool.Internal.instance.get(connectionPool, address, this, null);if (connection != null) {
foundPooledConnection = true;result = connection;} else {
selectedRoute = route;}}}closeQuietly(toClose);if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);}if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);}if (result != null) {
// If we found an already-allocated or pooled connection, we're done.return result;}......synchronized (connectionPool) {
......if (!foundPooledConnection) {
......result = new RealConnection(connectionPool, selectedRoute);acquire(result, false);}}// If we found a pooled connection on the 2nd time around, we're done.if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);return result;}// Do TCP + TLS handshakes. This is a blocking operation.result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,connectionRetryEnabled, call, eventListener);......synchronized (connectionPool) {
......Internal.instance.put(connectionPool, result);......}return result;
}
因为这个方法比较长,所以我们这里只保留了关键部分的代码。这个方法首先会创建一个 RealConnection
类型的变量 result
,Connection
类型的变量 releasedConnection
。
看到代码第10行,在 if
判断中,如果 this.connection
不为空的话,说明我们目前是有存在可以复用的连接,那么我们的变量 result
就会直接赋值成 this.connection
。
接下来看到第20行的 if
判断,如果我们的 result
值为空的话,说明我们目前没有可复用的连接,那么在 if
中我们就会通过 get
方法从连接池 connectionPool
中尝试获取一个可复用的连接赋值给我们的 result
。还有一点要注意,在 get
方法中如果成功获取到一个可复用连接的话,那么它会将这个连接赋值给 StreamAllocation
的成员变量 connection
,这样在 intercept
方法中我们就能通过 StreamAllocation
的 connection
方法获取到连接对象了,get
方法的解析我们留在了后面线程池的分析部分。
继续看到第39行,这个 if
判断我们此时的 result
是否为空,不为空的话表明此时我们已经获得了可复用的连接了,此时我们直接返回 result
,否则的话就说明我们没有可复用的连接,那么接下来我们就要自己创建一个新连接。
看到第48行,这个 if
判断位于同步块中,如果 foundPooledConnection
值为 false
,表明我们没能在连接池中找到可复用的连接,那么他便会通过 RealConnection
的构造方法来给我们的 result
对象赋值,创建出一个新连接。
继续看到第62行,在这里调用 RealConnection
的 connect
方法真正地去执行连接操作。在连接操作执行完之后,我们会在下面的同步块中,通过 put
方法将我们本次新产生的连接存放到 connectionPool
中进行维护,然后再返回 result
,同样 put 方法的解析我们也留在了后面线程池的分析部分。
分析完了 findConnection
方法,它的作用总结地来说就是:首先尝试获取可复用的连接,如果没有可复用的连接的话它就会创建一个新连接,然后返回。新连接会被放进连接池中进行维护。
分析到这里之后,我们 ConnectInterceptor
的 intercepr
方法的局部就分析完了。结合我们前面对它的总体以及局部的分析,在这里简单总结下它的功能:
- 首先它会获取我们从上面的拦截器链中传递下来的
StreamAllocation
对象。 - 通过
StreamAllocation
的newStream
方法对HttpCodec
对象进行赋值,这个对象是负责对Response
的解码以及Request
的编码的,然后通过StreamAllocation
的connection
方法对RealConnection
进行赋值。StreamAllocation
的newStream
方法主要获取健康的连接,获取连接过程中会尝试复用连接,如果不存在可复用连接才会创建连接并把新创建的连接放入connectionPool
中等待下一次的复用。 - 在
intercept
方法的最后我们会将创建好的HttpCodec
对象和RealConnection
对象通过proceed
方法传到下一个拦截器进行处理。
介绍完了 intercept 方法之后,我们就来分析一下连接池吧。
connectionPool
connectionPool
对于我们的连接拦截器来说有着非常重要的意义,这个类主要是用于我们的连接复用,当我们的连接共享同一个 address
时,我们就可以进行连接复用,这样也会提高我们的连接效率。要注意的一点是:连接在连接池中存在也是有时间限制的,所以它里面也会做清理回收工作。
get
我们在前面介绍的 findConnection
方法中,有提到从连接池中获取连接和存入连接的操作,它们分别对应的就是 connectionPool
的 get
方法和 put
方法,我们首先来看到 get
方法里面做了什么:
@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
方法所做的工作相当简单,它采用了一个 for
循环遍历了我们的 connectionPool
,然后在里面查找是否有符合条件的连接,如果有的话我们会执行 StreamAllocation
的 acquire
方法,我们来看看这个方法里面做了什么:
public void acquire(RealConnection connection, boolean reportedAcquired) {
assert (Thread.holdsLock(connectionPool));if (this.connection != null) throw new IllegalStateException();this.connection = connection;this.reportedAcquired = reportedAcquired;connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}
我们可以看到,这个方法内部就是将我们符合条件的 RealConnection
赋值给我们 StreamAllocation
的 connection
,我们在前面的 intercept 方法中分析过,我们在 intercept 内部会通过 StreamAllocation
的 connection
方法来获取连接,而 connection
方法就是直接返回 StreamAllocation
的 connection
对象:
public synchronized RealConnection connection() {
return connection;
}
所以通过这种方式,我们就能在外部使用我们经过层层判断后最终决定使用的连接了。
最后我们看到 get
方法最后一句,可以看到它在 connection
的 allocations
中添加了一个 StreamAllocation
的弱引用。allocations
是一个 List
,它的作用是用于判断我们当前连接所持有的 StreamAllocation
的数目,然后我们就可以通过这个 List
的 size
来判断我们连接的网络负载是否达到上限。
我们继续返回到 get
方法,在执行完 acquire
方法后,我们就可以直接返回我们符合要求的连接了,如果没有符合要求的连接,我们就会直接返回 null
。
put
接下来我们看到 put
方法源码:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));if (!cleanupRunning) {
cleanupRunning = true;executor.execute(cleanupRunnable);}connections.add(connection);
}
put
方法的源码其实也很简单,但是在添加我们的连接之前,如果还没有清理过连接池,它会异步地通过 cleanupRunnable
来清理我们的连接池,然后再通过 add
方法将我们的 RealConnection
对象添加进 connections
中。
connectionPool 的清理
在前面的 get
方法中我们提到了,我们每成功获取一次复用连接时,都会向 RealConnection
的 allocations
这个 List
中添加一个 StreamAllocation
的弱引用。而在 put
方法中,我们会通过 cleanupRunnable
对 connectionPool
进行清理工作,那么我们什么时候会开始清理我们的连接呢?答案很显然是当连接空闲的时候就该被回收,空闲的时候也就意味着 allocations
的大小为0,而我们的回收机制也是通过它来进行判别的,接下来我们还是先来看看 cleanupRunnable
做了什么:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {
}}}}}
};
可以看到这是一个线程,在它的 run
方法中,开启了一个 while
循环来清除我们的闲置连接。首先看到 waitNanos
,它是用于通知我们下次清理的时间间隔的一个量,它是通过 cleanup
方法来赋值的。我们继续往下看到下面的 try
语块中,在这里我们会调用 wait
方法来等待释放锁,同时传入 waitNanos
通知下一次的清理时间。那么我们接下来就来看看 cleanup
方法里面做了什么:
long cleanup(long now) {
int inUseConnectionCount = 0;int idleConnectionCount = 0;RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;// Find either a connection to evict, or the time that the next eviction is due.synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();// If the connection is in use, keep searching.if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;continue;}idleConnectionCount++;// If the connection is ready to be evicted, we're done.long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside// of the synchronized block).connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.return keepAliveDurationNs;} else {
// No connections, idle or in use.cleanupRunning = false;return -1;}}closeQuietly(longestIdleConnection.socket());// Cleanup again immediately.return 0;}
这个方法主要是用于类似于 GC
标记算法来对我们的空闲连接进行清理,我们在这里只讲解重点部分。首先看到第9行的 for
循环,在这个 for
循环中,我们会对 connections
中的 RealConnection
的对象进行遍历。
接着看到第13行,这里我们调用了 pruneAndGetAllocationCount
方法来对我们的连接进行判断它是否是空闲的,我们来看看它的实现:
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
List<Reference<StreamAllocation>> references = connection.allocations;for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);if (reference.get() != null) {
i++;continue;}// We've discovered a leaked allocation. This is an application bug.StreamAllocation.StreamAllocationReference streamAllocRef =(StreamAllocation.StreamAllocationReference) reference;String message = "A connection to " + connection.route().address().url()+ " was leaked. Did you forget to close a response body?";Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);references.remove(i);connection.noNewStreams = true;// If this was the last allocation, the connection is eligible for immediate eviction.if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;return 0;}}return references.size();
}
在这个方法中,我们会对 RealConnection
的 allocations
这个队列进行遍历,我们前面说过它里面存放着我们 StreamAllocation
的弱引用。在第6行,如果我们调用 get
方法返回的值不为空的话,说明这个 StreamAllocation
的弱引用还被我们的 RealConnection
对象所持有,直接跳过;如果为空的话,就会在下面调用 remove
方法移除这个弱引用。如果到最后我们的 allocations
的大小为0,那么说明我们这个 RealConnection
对象就是闲置的了,否则的话就是依然活跃的。
我们继续回到 cleanup
方法中,回到我们刚才调用 pruneAndGetAllocationCount
方法的第13行处,为了方便观看我这里直接把它粘了过来:
long cleanup(long now) {
int inUseConnectionCount = 0;int idleConnectionCount = 0;RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;// Find either a connection to evict, or the time that the next eviction is due.synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();// If the connection is in use, keep searching.if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;continue;}idleConnectionCount++;// If the connection is ready to be evicted, we're done.long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside// of the synchronized block).connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.return keepAliveDurationNs;} else {
// No connections, idle or in use.cleanupRunning = false;return -1;}}closeQuietly(longestIdleConnection.socket());// Cleanup again immediately.return 0;}
这个 if
中,如果调用该方法返回值大于 0,说明这个连接是活跃的,直接跳过,否则的话就说明这个连接是不活跃的,那么就会继续执行下面的代码,下面的代码主要是根据不同的情况返回不同的返回值,这里不做具体介绍,值得注意的是 cleanup
方法的第39行,如果执行到这个 else
语句,说明已经不存在空闲或正在使用的连接了,那么我们的 cleanupRunning
会被置为 false
,这样我们的 put
方法就不会执行 cleanupRunnable
这个线程了,接着是返回值 -1,我们接着回到 cleanup
的上一个方法,也就是 cleanupRunnable
的 run
方法:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;waitNanos -= (waitMillis * 1000000L);synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);} catch (InterruptedException ignored) {
}}}}}
};
可以看到如果我们 cleanup
方法的返回值为 -1 的话,那么我们就会结束 while
循环,也就意味着清理任务结束。这就是我们 connectionPool
清理空闲连接的整个流程。
connectionPool 总结
接下来我们就从整体上来对 connectionPool
做一个总结:
- 首先是
get
方法,当我们成功地从连接池取得一次可复用的连接后,我们就会向这个连接内部的allocations
添加一个StreamAllocation
的弱引用。 - 在
put
方法中,当我们cleanupRunning
值为true
时,我们会开启一个清理线程,遍历连接池中的每一个连接,将连接中的无用StreamAllocation
弱引用移除,然后通过判断allocations
的大小是否为 0 来判断是否为闲置连接。 - 清理线程会一直运行,知道连接池中不存在任何连接了才会停止清理工作。
到这里,我们的 ConnectInterceptor
就分析完了,接下来我们就来分析最后一个拦截器—— CallServerInterceptor
。
CallServerInterceptor
这个拦截器叫做请求服务器,它是我们拦截器链的最后一个拦截器了,它所做的工作就是将 HTTP 请求写进网络的 IO 流中,并读取服务端返回给客户端的响应数据。我们接下来就来看看它的 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();long sentRequestMillis = System.currentTimeMillis();realChain.eventListener().requestHeadersStart(realChain.call());httpCodec.writeRequestHeaders(request);realChain.eventListener().requestHeadersEnd(realChain.call(), request);......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();int code = response.code();if (code == 100) {
// server sent a 100-continue even though we did not request one.// try again to read the actual responseresponseBuilder = httpCodec.readResponseHeaders(false);response = responseBuilder.request(request).handshake(streamAllocation.connection().handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build();code = response.code();}realChain.eventListener().responseHeadersEnd(realChain.call(), response);if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.response = response.newBuilder().body(Util.EMPTY_RESPONSE).build();} else {
response = response.newBuilder().body(httpCodec.openResponseBody(response)).build();}......if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException("HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());}return response;
}
首先我们看到这个方法的前6行,在这里我们通过拦截链的传递,获得了 HttpCodec
对象、StreamAllocation
对象、RealConnection
对象以及 Request
对象。
我们接下来看到第11行,在这里我们调用 writeRequestHeaders
方法向网络 IO 流中写入我们的请求头,紧接着在第15行,在这里我们会调用 finishRequest
完成我们的请求,这两行之间的省略代码主要是对一种特殊情况的处理,由于与主流程无关,所以这里就省略掉了。
接下来我们看待第17行的 if
判断中,如果 responseBuilder
为空的话,那么就会调用 HttpCodec
的 readResponseHeaders
方法来读取响应的头部信息,如果我们没经过特殊处理,那么我们的 responseBuilder
就是为空,这个方法就会得到执行。
接下来我们跳到第46行,在这里我们的有 Web Socket 的话并且状态码为101,那么就会构建一个响应主体为空的响应,如果这个 if
条件不符合的话,就会执行 else
中的语句,它就会构建出正常的响应 Response
出来。
接下来在第59行,如果状态码为204或者205并且响应的内容长度不为 0 的话,就会抛出异常,如果不抛出异常,我们最终就会返回我们的 Response
,然后一路向上传递返回给我们的用户进行读取。
至此,关于 okhttp
的主要内容我们都分析完了,okhttp
的源码在阅读难度上并不算太大,希望大家如果第一遍看不懂的话就多看几遍,学有所得。最后祝大家学习愉快,有问题的话可以在下方给我留言。