关于android:知识点OkHttp-原理-8-连问

2次阅读

共计 8234 个字符,预计需要花费 21 分钟才能阅读完成。

前言

OkHttp能够说是 Android 开发中最常见的网络申请框架,OkHttp使用方便,扩展性强,功能强大,OKHttp源码与原理也是面试中的常客
然而 OKHttp 的源码内容比拟多,想要学习它的源码往往千头万绪,一时抓不住重点.
本文从几个问题登程梳理 OKHttp 相干知识点,以便疾速构建 OKHttp 常识体系,如果对你有用,欢送点赞~

本文次要包含以下内容

  1. OKHttp申请的整体流程是怎么的?
  2. OKHttp散发器是怎么工作的?
  3. OKHttp拦截器是如何工作的?
  4. 利用拦截器和网络拦截器有什么区别?
  5. OKHttp如何复用 TCP 连贯?
  6. OKHttp闲暇连贯如何革除?
  7. OKHttp有哪些长处?
  8. OKHttp框架中用到了哪些设计模式?

1. OKHttp申请整体流程介绍

首先来看一个最简略的 Http 申请是如何发送的。

   val okHttpClient = OkHttpClient()
   val request: Request = Request.Builder()
       .url("https://www.google.com/")
       .build()

   okHttpClient.newCall(request).enqueue(object :Callback{override fun onFailure(call: Call, e: IOException) { }

       override fun onResponse(call: Call, response: Response) {}})

这段代码看起来比较简单,OkHttp申请过程中起码只须要接触 OkHttpClientRequestCallResponse,然而框架外部会进行大量的逻辑解决。
所有网络申请的逻辑大部分集中在拦截器中,然而在进入拦截器之前还须要依附散发器来调配申请工作。
对于散发器与拦截器,咱们在这里先简略介绍下,后续会有更加具体的解说

  • 散发器: 外部保护队列与线程池,实现申请调配;
  • 拦截器: 五大默认拦截器实现整个申请过程。

整个网络申请过程大抵如上所示

  1. 通过建造者模式构建 OKHttpClientRequest
  2. OKHttpClient通过 newCall 发动一个新的申请
  3. 通过散发器保护申请队列与线程池,实现申请调配
  4. 通过五大默认拦截器实现申请重试,缓存解决,建设连贯等一系列操作
  5. 失去网络申请后果

2. OKHttp散发器是怎么工作的?

散发器的次要作用是保护申请队列与线程池, 比方咱们有 100 个异步申请,必定不能把它们同时申请,而是应该把它们排队分个类,分为正在申请中的列表和正在期待的列表,等申请实现后,即可从期待中的列表中取出期待的申请,从而实现所有的申请

而这里同步申请各异步申请又略有不同

同步申请

synchronized void executed(RealCall call) {runningSyncCalls.add(call);
}

因为同步申请不须要线程池,也不存在任何限度。所以散发器仅做一下记录。后续依照退出队列的程序同步申请即可

异步申请

synchronized void enqueue(AsyncCall call) {
    // 申请数最大不超过 64, 同一 Host 申请不能超过 5 个
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost)       {runningAsyncCalls.add(call);
        executorService().execute(call);
    } else {readyAsyncCalls.add(call);
    }
}

当正在执行的工作未超过最大限度 64,同时同一 Host 的申请不超过 5 个,则会增加到正在执行队列,同时提交给线程池。否则先退出期待队列。
每个工作实现后,都会调用散发器的 finished 办法, 这外面会取出期待队列中的工作继续执行

3. OKHttp拦截器是怎么工作的?

通过下面散发器的工作散发,上面就要利用拦截器开始一系列配置了

# RealCall
  override fun execute(): Response {
    try {client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()} finally {client.dispatcher.finished(this)
    }
  }

咱们再来看下 RealCallexecute办法,能够看出,最初返回了getResponseWithInterceptorChain, 责任链的构建与解决其实就是在这个办法外面

internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {interceptors += client.networkInterceptors}
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(call = this,interceptors = interceptors,index = 0)
    val response = chain.proceed(originalRequest)
  }

如上所示,构建了一个 OkHttp 拦截器的责任链
责任链,顾名思义,就是用来解决相干事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)解决申请事务,如果某个节点解决完了就能够依据理论业务需要传递给下一个节点持续解决或者返回处理完毕。
如上所示责任链增加的程序及作用如下表所示:

拦截器 作用
利用拦截器 拿到的是原始申请,能够增加一些自定义 header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor 处理错误重试和重定向
BridgeInterceptor 应用层和网络层的桥接拦截器,次要工作是为申请增加 cookie、增加固定的 header,比方 Host、Content-Length、Content-Type、User-Agent 等等,而后保留响应后果的 cookie,如果响应应用 gzip 压缩过,则还须要进行解压。
CacheInterceptor 缓存拦截器,如果命中缓存则不会发动网络申请。
ConnectInterceptor 连贯拦截器,外部会保护一个连接池,负责连贯复用、创立连贯(三次握手等等)、开释连贯以及创立连贯上的 socket 流。
networkInterceptors(网络拦截器) 用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor 申请拦截器,在前置筹备工作实现后,真正发动了网络申请。

咱们的网络申请就是这样通过责任链一级一级的递推上来,最终会执行到 CallServerInterceptorintercept办法,此办法会将网络响应的后果封装成一个 Response 对象并 return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain 办法的返回, 如下图所示:

4. 利用拦截器和网络拦截器有什么区别?

从整个责任链路来看,利用拦截器是最先执行的拦截器,也就是用户本人设置 request 属性后的原始申请,而网络拦截器位于 ConnectInterceptorCallServerInterceptor之间,此时网络链路曾经筹备好,只期待发送申请数据。它们次要有以下区别

  1. 首先,利用拦截器在 RetryAndFollowUpInterceptorCacheInterceptor之前,所以一旦产生谬误重试或者网络重定向,网络拦截器可能执行屡次,因为相当于进行了二次申请,然而利用拦截器永远只会触发一次。另外如果在 CacheInterceptor 中命中了缓存就不须要走网络申请了,因而会存在短路网络拦截器的状况。
  2. 其次,除了 CallServerInterceptor 之外,每个拦截器都应该至多调用一次 realChain.proceed 办法。实际上在利用拦截器这层能够屡次调用 proceed 办法(本地异样重试)或者不调用 proceed 办法(中断),然而网络拦截器这层连贯曾经筹备好,可且仅可调用一次 proceed 办法。
  3. 最初,从应用场景看,利用拦截器因为只会调用一次,通常用于统计客户端的网络申请发动状况;而网络拦截器一次调用代表了肯定会发动一次网络通信,因而通常可用于统计网络链路上传输的数据。

5. OKHttp如何复用 TCP 连贯?

ConnectInterceptor的次要工作就是负责建设 TCP 连贯,建设 TCP 连贯须要经验三次握手四次挥手等操作,如果每个 HTTP 申请都要新建一个 TCP 耗费资源比拟多
Http1.1 曾经反对 keep-alive, 即多个Http 申请复用一个 TCP 连贯,OKHttp也做了相应的优化,上面咱们来看下 OKHttp 是怎么复用 TCP 连贯的

ConnectInterceptor中查找连贯的代码会最终会调用到 ExchangeFinder.findConnection 办法,具体如下:

# ExchangeFinder
// 为承载新的数据流 寻找 连贯。寻找程序是 已调配的连贯、连接池、新建连贯
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {synchronized (connectionPool) {
    // 1. 尝试应用 已给数据流调配的连贯.(例如重定向申请时,能够复用上次申请的连贯)releasedConnection = transmitter.connection;
    result = transmitter.connection;

    if (result == null) {
      // 2. 没有已调配的可用连贯,就尝试从连接池获取。(连接池稍后具体解说)if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {result = transmitter.connection;}
    }
  }

  synchronized (connectionPool) {if (newRouteSelection) {
      //3. 当初有了 IP 地址,再次尝试从连接池获取。可能会因为连贯合并而匹配。(这里传入了 routes,下面的传的 null)routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
        foundPooledConnection = true;
        result = transmitter.connection;
      }
    }

  // 4. 第二次没胜利,就把新建的连贯,进行 TCP + TLS 握手,与服务端建设连贯. 是阻塞操作
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);

  synchronized (connectionPool) {
    // 5. 最初一次尝试从连接池获取,留神最初一个参数为 true,即要求 多路复用(http2.0)// 意思是,如果本次是 http2.0,那么为了保障 多路复用性,(因为下面的握手操作不是线程平安)会再次确认连接池中此时是否已有同样连贯
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // 如果获取到,就敞开咱们创立里的连贯,返回获取的连贯
      result = transmitter.connection;
    } else {
      // 最初一次尝试也没有的话,就把刚刚新建的连贯存入连接池
      connectionPool.put(result);
    }
  }
 
  return result;
}

下面精简了局部代码,能够看出,连贯拦截器应用了 5 种办法查找连贯

  1. 首先会尝试应用 已给申请调配的连贯。(已调配连贯的状况例如重定向时的再次申请,阐明上次曾经有了连贯)
  2. 若没有 已调配的可用连贯,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:address统一——hostport、代理等统一,且匹配的连贯能够承受新的申请。
  3. 若从连接池没有获取到,则传入 routes 再次尝试获取,这次要是针对 Http2.0 的一个操作,Http2.0能够复用 square.comsquare.ca的连贯
  4. 若第二次也没有获取到,就创立 RealConnection 实例,进行 TCP + TLS 握手,与服务端建设连贯。
  5. 此时为了确保 Http2.0 连贯的多路复用性,会第三次从连接池匹配。因为新建设的连贯的握手过程是非线程平安的,所以此时可能连接池新存入了雷同的连贯。
  6. 第三次若匹配到,就应用已有连贯,开释刚刚新建的连贯;若未匹配到,则把新连贯存入连接池并返回。

以上就是连贯拦截器尝试复用连贯的操作,流程图如下:

6. OKHttp闲暇连贯如何革除?

下面说到咱们会建设一个 TCP 连接池,但如果没有工作了,闲暇的连贯也应该及时革除,OKHttp是如何做到的呢?

  # RealConnectionPool
  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {override fun runOnce(): Long = cleanup(System.nanoTime())
  }

  long cleanup(long now) {
    int inUseConnectionCount = 0;// 正在应用的连接数
    int idleConnectionCount = 0;// 闲暇连接数
    RealConnection longestIdleConnection = null;// 闲暇工夫最长的连贯
    long longestIdleDurationNs = Long.MIN_VALUE;// 最长的闲暇工夫

    // 遍历连贯:找到待清理的连贯, 找到下一次要清理的工夫(还未到最大闲暇工夫)synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext();) {RealConnection connection = i.next();

        // 若连贯正在应用,continue,正在应用连接数 +1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
        // 闲暇连接数 +1
        idleConnectionCount++;

        // 赋值最长的闲暇工夫和对应连贯
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
      // 若最长的闲暇工夫大于 5 分钟 或 闲暇数 大于 5,就移除并敞开这个连贯
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // else,就返回 还剩多久达到 5 分钟,而后 wait 这个工夫再来清理
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 连贯没有闲暇的,就 5 分钟后再尝试清理.
        return keepAliveDurationNs;
      } else {
        // 没有连贯,不清理
        cleanupRunning = false;
        return -1;
      }
    }
    // 敞开移除的连贯
    closeQuietly(longestIdleConnection.socket());

    // 敞开移除后 立即 进行下一次的 尝试清理
    return 0;
  }

思路还是很清晰的:

  1. 在将连贯退出连接池时就会启动定时工作
  2. 有闲暇连贯的话,如果最长的闲暇工夫大于 5 分钟 或 闲暇数 大于 5,就移除敞开这个最长闲暇连贯;如果 闲暇数 不大于 5 且 最长的闲暇工夫不大于 5 分钟,就返回到 5 分钟的剩余时间,而后期待这个工夫再来清理。
  3. 没有闲暇连贯就等 5 分钟后再尝试清理。
  4. 没有连贯不清理。

流程如下图所示:

7. OKHttp有哪些长处?

  1. 应用简略,在设计时应用了外观模式,将整个零碎的复杂性给暗藏起来,将子系统接口通过一个客户端 OkHttpClient 对立裸露进去。
  2. 扩展性强,能够通过自定义利用拦截器与网络拦截器,实现用户各种自定义的需要
  3. 功能强大,反对 SpdyHttp1.XHttp2、以及WebSocket 等多种协定
  4. 通过连接池复用底层TCP(Socket),缩小申请延时
  5. 无缝的反对 GZIP 缩小数据流量
  6. 反对数据缓存, 缩小反复的网络申请
  7. 反对申请失败主动重试主机的其余ip,主动重定向

8. OKHttp框架中用到了哪些设计模式?

  1. 构建者模式:OkHttpClientRequest 的构建都用到了构建者模式
  2. 外观模式:OkHttp应用了外观模式, 将整个零碎的复杂性给暗藏起来,将子系统接口通过一个客户端 OkHttpClient 对立裸露进去。
  3. 责任链模式: OKHttp的外围就是责任链模式,通过 5 个默认拦截器形成的责任链实现申请的配置
  4. 享元模式: 享元模式的外围即池中复用,OKHttp复用 TCP 连贯时用到了连接池,同时在异步申请中也用到了线程池

总结

本文次要梳理了 OKHttp 原理相干知识点,并答复了以下问题:

  1. OKHttp申请的整体流程是怎么的?
  2. OKHttp散发器是怎么工作的?
  3. OKHttp拦截器是如何工作的?
  4. 利用拦截器和网络拦截器有什么区别?
  5. OKHttp如何复用 TCP 连贯?
  6. OKHttp闲暇连贯如何革除?
  7. OKHttp有哪些长处?
  8. OKHttp框架中用到了哪些设计模式?

如果对您有所帮忙,欢送点赞,谢谢~

相干视频:

【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili

Android OkHttp 原理解读——带你深刻把握 OkHttp 散发器与拦截器开发_哔哩哔哩_bilibili

【Android 进阶教程】——基于 Okhttp 的可用网络框架原理解析_哔哩哔哩_bilibili

本文转自 https://juejin.cn/post/7020027832977850381,如有侵权,请分割删除。

正文完
 0