OkHttp源码读后感

这次是第二次看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 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(), 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 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》这是一本好书!
感谢这位大神
感谢另一位大神

你可能感兴趣的:(OkHttp源码读后感)