OkHttp源码读后感

xiaoxiao2021-02-27  260

这次是第二次看OkHttp的源码了,比起上一次,这次总算是理清了其中的脉络,这或许是随着工作经验的增加而发生的改变,说到底还是单身的锅,单身狗的周末只能玩代码消磨时间…….我是用SourceInsight作为工具的(之前装好的),有说用JetBrains的IDEA效果更好,无奈何用的是长城宽带,坑的一比….下半天没下好…

从平时使用Api的顺序来源码是个不错的选择,下面是OkHttp的一般用法:

OkHttpClient client = new OkHttpClient.Builder().build(); Request request = new Request.Builder() .url(url) .post(body) .build(); Response response = client.newCall(request).execute(); String result = response.body().string(); response.close();

上面是同步的请求,但是异步请求才是OkHttp的精髓所在,异步请求是这样的:

OkHttpClient okHttpClient = new OkHttpClient(); Request request = new Request.Builder() .url("http://www.qq.com") .build(); Call call = okHttpClient.newCall(request); //1.异步请求,通过接口回调告知用户 http 的异步执行结果 call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { System.out.println(e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { System.out.println(response.body().string()); } } });

异步请求使用了队列进行并发任务的分发(Dispatch)与回调,下面就先来看看OkHttpClient里面的newCall方法:

@Override public Call newCall(Request request) { return RealCall.newRealCall(this, request, false /* for web socket */); }

看来OkHttpClient只是一层皮,想知道newCall的具体实现还得去RealCall里面找才行:

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { // Safely publish the Call instance to the EventListener. RealCall call = new RealCall(client, originalRequest, forWebSocket); call.eventListener = client.eventListenerFactory().create(call); return call; }

这里实例化了一个RealCall对象,并且给这个RealCall添加一个EventListener,监听RealCall,这样就可以知道每次的网络请求具体进行到哪一步。

说了一大串好戏终于要开始了!在上面的代码中,不难发现execute()也是在RealCall里面的:

@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); } }

事不宜迟,马上来看看getResponseWithInterceptorChain():

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); return chain.proceed(originalRequest); } }

嗯,OkHttp的主要精髓之一就在这里了!这里有一个类型为Interceptor的List,这个List里面,有负责失败重试以及重定向的 RetryAndFollowUpInterceptor;负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的 BridgeInterceptor;负责读取缓存直接返回、更新缓存的 CacheInterceptor;负责和服务器建立连接的 ConnectInterceptor;配置 OkHttpClient 时设置的 networkInterceptors;负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor。

OkHttp把所有这些负责不同工作的拦截,用责任链的模式把它们串起来,让它们各自完成各自的工作,这样就很好的实现了类似Http协议的分层的思想,每个拦截都实现了Interceptor接口,重写intercept()方法,返回各自对应的Response。

到这里,要理清OkHttp的各种策略只需要去看对应的Interceptor就可以了,我只看了其中的复用连接池策略、缓存策略这两种….下面就逐一的来看看是怎样实现的吧。

复用连接池:

创建一个TCP连接需要3次握手,而释放连接则需要2次或4次握手,如果每个请求都重新走一遍创建和销毁的流程,那是很费时和很费资源的事情,所以OkHttp通过复用连接池的方式实现了Socket连接的重用。说到这,还得补充一下,OkHttp还支持SPDY黑科技,什么是SPDY呢?引述《图解HTTP》的原文:

使用SPDY后,HTTP协议额外获得以下功能: 多路复用流 通过单一的TCP连接,可以无限制处理多个HTTP请求。所有请求的处理都在一条TCP连接上完成,因此TCP的处理效率得到提高。 赋予请求优先级 SPDY不仅可以无限制地并发处理请求,还可以给请求逐个分配优先级顺序。这样主要是为了在发生多个请求时,解决因带宽低而导致响应变慢的问题。 压缩HTTP首部 压缩HTTP请求和响应的首部,这样一来,通信产生的数据包数量和发送的字节数就更少了。 推送功能 支持服务器主动向客户端推送数据的功能。这样,服务器可直接发送数据,而不必等待客户端的请求。 服务器提示功能 服务器可以主动提示客户端请求所需的资源。由于在客户端发现资源之前就可以获知资源的存在,因此在资源已缓存的情况下,可以避免发送不必要的请求。

还是说回复用连接池的实现,具体的实现是在ConnectionPool中:

public final class ConnectionPool { /** * Background threads are used to cleanup expired connections. There will be at most a single * thread running per connection pool. The thread pool executor permits the pool itself to be * garbage collected. */ //这个executor就是复用连接池(其实就是线程池) private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */, Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true)); /**这个Runnable就是专门用来淘汰末位的socket,当满足以下条件时,就会进行末位淘汰,非常像GC 1. 并发socket空闲连接超过5个 2. 某个socket的keepalive时间大于5分钟 **/ 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()里面:

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; } } //最大并发连接大于5,或者keepalive时间超过5分钟 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; }

复用连接池对于Socket的清理就类似于Java的引用计数法。

缓存策略:

先来说一下HTTP协议中的Cache-Control,服务器跟客户端的缓存都是通过这个HTTP的首部字段Cache-Control来实现的,下面引述《图解HTTP》原文:

举个栗子-> Cache-Control:private,max-age=0,no-cache 这是Cache-Control的一般用法,就是通过在Cache-Control中设定一些参数达到控制缓存的效果. 上面三个参数代表的意思分别是: private:响应只以特定的用户作为对象,缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发过来的请求,代理服务器则不会返回缓存;public则反之。 max-age:当客户端发送的请求中包含max-age指令时,如果判定缓存资源的缓存时间数值没有比指定时间的数值更小,那么客户端就接收缓存的资源,否则代理服务器向源服务器发送该请求;当max-age的值为0时,那么缓存服务器通常需要将请求转发给源服务器。 no-cache:使用no-cache指令的目的是为了防止从缓存中返回过期的资源。 除了上面三个还有下面几个常用的指令....... no-store:与no-cache容易混淆,当使用no-store指令时,暗示请求或响应中包含机密信息,所以该指令规定缓存不能在本地存储请求或响应的任一部分,no-store才是真正的不进行缓存。 max-stale:表示即使缓存过期也照常接收,max-stale后面是具体数值单位为秒 min-fresh:指令要求缓存服务器返回至少还未过指定时间的缓存资源。比如min-fresh=60(单位:秒),在这60秒以内如果有超过有效期的资源都无法作为响应返回了 .....还有几个指令就不介绍了,感兴趣的同学可以买本《图解HTTP》看看哦

好了,说了那么多,回到OkHttp的缓存实现中来,OkHttp的缓存是通过CacheControl来实现的:

private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds, boolean onlyIfCached, boolean noTransform, boolean immutable, @Nullable String headerValue) { this.noCache = noCache; this.noStore = noStore; this.maxAgeSeconds = maxAgeSeconds; this.sMaxAgeSeconds = sMaxAgeSeconds; this.isPrivate = isPrivate; this.isPublic = isPublic; this.mustRevalidate = mustRevalidate; this.maxStaleSeconds = maxStaleSeconds; this.minFreshSeconds = minFreshSeconds; this.onlyIfCached = onlyIfCached; this.noTransform = noTransform; this.immutable = immutable; this.headerValue = headerValue; } CacheControl(Builder builder) { this.noCache = builder.noCache; this.noStore = builder.noStore; this.maxAgeSeconds = builder.maxAgeSeconds; this.sMaxAgeSeconds = -1; this.isPrivate = false; this.isPublic = false; this.mustRevalidate = false; this.maxStaleSeconds = builder.maxStaleSeconds; this.minFreshSeconds = builder.minFreshSeconds; this.onlyIfCached = builder.onlyIfCached; this.noTransform = builder.noTransform; this.immutable = builder.immutable; }

在CacheControl的构造方法里,可以看到刚才上面介绍的几个cache-control指令的身影。通过构建一个CacheControl的Builder,然后再把这个CacheControl加入CacheInterceptor拦截中,就可以轻松的对缓存进行控制(至于具体怎么添加拦截就请自行百度或google了,细心的同学会发现CacheInterceptor也就是责任链的其中一环,所以在构建Request的时候加上自定义的CacheControl就可以了。 )

其实一般情况下我们都不会去给Request加自定义的CacheControl,但是如果服务器端是外包给别人的,而你却又找不到那些服务端开发的人,那么这时候就只能靠自己了……

其实只要抓住责任链这一主要的设计,看OkHttp的源码也就轻松很多了,因为每个Interceptor拦截都对应着一个Control,Control是具体实现这些拦截的地方,而每个Interceptor则是拦截各自负责的请求并返回处理结果,读后感就写这么多了。

参考文章: 《图解HTTP》这是一本好书! 感谢这位大神 感谢另一位大神

转载请注明原文地址: https://www.6miu.com/read-10486.html

最新回复(0)