谈谈OKHttp的几道面试题

来吧,今天说说经常使用的网络框架OKHttp,也是如今Android所用的原生网络框架(Android 4.4开始,HttpURLConnection的底层实现被Google改为了OkHttp),GOGOGO!java

  • OKHttp有哪些拦截器,分别起什么做用
  • OkHttp怎么实现链接池
  • OkHttp里面用到了什么设计模式

OKHttp有哪些拦截器,分别起什么做用

OKHTTP的拦截器是把全部的拦截器放到一个list里,而后每次依次执行拦截器,而且在每一个拦截器分红三部分:web

  • 预处理拦截器内容
  • 经过proceed方法把请求交给下一个拦截器
  • 下一个拦截器处理完成并返回,后续处理工做。

这样依次下去就造成了一个链式调用,看看源码,具体有哪些拦截器:设计模式

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

根据源码可知,一共七个拦截器:缓存

  • addInterceptor(Interceptor),这是由开发者设置的,会按照开发者的要求,在全部的拦截器处理以前进行最先的拦截处理,好比一些公共参数,Header均可以在这里添加。
  • RetryAndFollowUpInterceptor,这里会对链接作一些初始化工做,以及请求失败的充实工做,重定向的后续请求工做。跟他的名字同样,就是作重试工做还有一些链接跟踪工做。
  • BridgeInterceptor,这里会为用户构建一个可以进行网络访问的请求,同时后续工做将网络请求回来的响应Response转化为用户可用的Response,好比添加文件类型,content-length计算添加,gzip解包。
  • CacheInterceptor,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,并且若是本地有了可⽤的Cache,就能够在没有网络交互的状况下就返回缓存结果。
  • ConnectInterceptor,这里主要就是负责创建链接了,会创建TCP链接或者TLS链接,以及负责编码解码的HttpCodec
  • networkInterceptors,这里也是开发者本身设置的,因此本质上和第一个拦截器差很少,可是因为位置不一样,因此用处也不一样。这个位置添加的拦截器能够看到请求和响应的数据了,因此能够作一些网络调试。
  • CallServerInterceptor,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操做,经过socket读写数据。

OkHttp怎么实现链接池

  • 为何须要链接池?

频繁的进行创建Sokcet链接和断开Socket是很是消耗网络资源和浪费时间的,因此HTTP中的keepalive链接对于下降延迟和提高速度有很是重要的做用。keepalive机制是什么呢?也就是能够在一次TCP链接中能够持续发送多份数据而不会断开链接。因此链接的屡次使用,也就是复用就变得格外重要了,而复用链接就须要对链接进行管理,因而就有了链接池的概念。websocket

OkHttp中使用ConectionPool实现链接池,默认支持5个并发KeepAlive,默认链路生命为5分钟。cookie

  • 怎么实现的?

1)首先,ConectionPool中维护了一个双端队列Deque,也就是两端均可以进出的队列,用来存储链接。
2)而后在ConnectInterceptor,也就是负责创建链接的拦截器中,首先会找可用链接,也就是从链接池中去获取链接,具体的就是会调用到ConectionPool的get方法。网络

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

也就是遍历了双端队列,若是链接有效,就会调用acquire方法计数并返回这个链接。并发

3)若是没找到可用链接,就会建立新链接,并会把这个创建的链接加入到双端队列中,同时开始运行线程池中的线程,其实就是调用了ConectionPool的put方法。框架

public final class ConnectionPool {
    void put(RealConnection connection) {
        if (!cleanupRunning) {
        	//没有链接的时候调用
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
}

3)其实这个线程池中只有一个线程,是用来清理链接的,也就是上述的cleanupRunnablesocket

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) {
                        //在timeout时间内释放锁
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

这个runnable会不停的调用cleanup方法清理线程池,并返回下一次清理的时间间隔,而后进入wait等待。

怎么清理的呢?看看源码:

long cleanup(long now) {
    synchronized (this) {
      //遍历链接
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //检查链接是不是空闲状态,
        //不是,则inUseConnectionCount + 1
        //是 ,则idleConnectionCount + 1
        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;
        }
      }

      //若是超过keepAliveDurationNs或maxIdleConnections,
      //从双端队列connections中移除
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {      
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {      //若是空闲链接次数>0,返回将要到期的时间
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 链接依然在使用中,返回保持链接的周期5分钟
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

也就是当若是空闲链接maxIdleConnections超过5个或者keepalive时间大于5分钟,则将该链接清理掉。

4)这里有个问题,怎样属于空闲链接?

其实就是有关刚才说到的一个方法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虚引用列表allocations。每建立一个链接,就会把链接对应的StreamAllocationReference添加进该列表中,若是链接关闭之后就将该对象移除。

5)链接池的工做就这么多,并不负责,主要就是管理双端队列Deque<RealConnection>,能够用的链接就直接用,而后按期清理链接,同时经过对StreamAllocation的引用计数实现自动回收。

OkHttp里面用到了什么设计模式

  • 责任链模式

这个不要太明显,能够说是okhttp的精髓所在了,主要体现就是拦截器的使用,具体代码能够看看上述的拦截器介绍。

  • 建造者模式

在Okhttp中,建造者模式也是用的挺多的,主要用处是将对象的建立与表示相分离,用Builder组装各项配置。
好比Request:

public class Request {
  public static class Builder {
    @Nullable HttpUrl url;
    String method;
    Headers.Builder headers;
    @Nullable RequestBody body;
    public Request build() {
      return new Request(this);
    }
  }
}
  • 工厂模式

工厂模式和建造者模式相似,区别就在于工厂模式侧重点在于对象的生成过程,而建造者模式主要是侧重对象的各个参数配置。
例子有CacheInterceptor拦截器中又个CacheStrategy对象:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }
  • 观察者模式

以前我写过一篇文章,是关于Okhttp中websocket的使用,因为webSocket属于长链接,因此须要进行监听,这里是用到了观察者模式:

final WebSocketListener listener;
  @Override public void onReadMessage(String text) throws IOException {
    listener.onMessage(this, text);
  }
  • 单例模式

这个就不举例了,每一个项目都会有

  • 另外有的博客还说到了策略模式,门面模式等,这些你们能够网上搜搜,毕竟每一个人的想法见解都会不一样,细心找找可能就会发现。

拜拜

有一块儿学习的小伙伴能够关注下❤️个人公众号——码上积木,天天剖析一个知识点,咱们一块儿积累知识。

相关文章
相关标签/搜索