okhttp源码学习

okhttp学习路线.png

本篇只是个人的学习笔记,建议移步面试官:听说你熟悉OkHttp原理?看大佬分析

源码分析

okhttp源码是4.10.0版本

我们先看看一次最简单的网络请求

val client = OkHttpClient()

val request = Request.Builder()
                .url(url)
                .build()

thread {
  val response = client.newCall(request).execute()
  Log.e(TAG, "onCreate: ${response.body}")
}

本文主要对response如何生成来解析,看看RealCall的execute方法

  override fun execute(): Response {
    check(executed.compareAndSet(false, true)) { "Already Executed" }

    timeout.enter()
    callStart()
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

直接看重点,也就是 client.dispatcher.executed(this)以及之后的,先看看dispatcher的executed方法,里面只有一句代码

    runningSyncCalls.add(call)

这里的runningSyncCalls是什么呢,是一个双端数组队列,表示的是正在运行的同步调用,包括已经取消但是还在运行的(Running synchronous calls. Includes canceled calls that haven't finished yet.),同样的队列还有runningAsyncCalls(正在异步调用),readyAsyncCalls(准备异步调用)。那么这里的意思就很明显了,就是加入队列里。

现在再看看getResponseWithInterceptorChain这个方法,这个算是okhttp的核心了。

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,
      exchange = null,
      request = originalRequest,
      connectTimeoutMillis = client.connectTimeoutMillis,
      readTimeoutMillis = client.readTimeoutMillis,
      writeTimeoutMillis = client.writeTimeoutMillis
  )

  var calledNoMoreExchanges = false
  try {
    //传入request启动责任链
    val response = chain.proceed(originalRequest)
    if (isCanceled()) {
      response.closeQuietly()
      throw IOException("Canceled")
    }
    return response
  } catch (e: IOException) {
    calledNoMoreExchanges = true
    throw noMoreExchanges(e) as Throwable
  } finally {
    if (!calledNoMoreExchanges) {
      noMoreExchanges(null)
    }
  }
}

暂时跳过前半部分的拦截器,这个我们在后面具体分析每一个拦截器的作用,可以看到生成response的是chain.proceed(originalRequest),那么先进去看一下具体怎么做到的

override fun proceed(request: Request): Response {
  //必须小于拦截器的数量
  check(index < interceptors.size)
  //当前拦截器调用proceed的次数
  calls++

  //exchage是对请求流的封装,在执行ConnectInterceptor前为空
  if (exchange != null) {
    //保证host和port没有被修改
    check(exchange.finder.sameHostAndPort(request.url)) {
      "network interceptor ${interceptors[index - 1]} must retain the same host and port"
    }
    //ConnectInterceptor之后每个拦截器只能调用proceed一次
    check(calls == 1) {
      "network interceptor ${interceptors[index - 1]} must call proceed() exactly once"
    }
  }

  // 创建下一个拦截器的责任链
  val next = copy(index = index + 1, request = request)
  val interceptor = interceptors[index]
  //调用intercept方法传入新建的责任链
  @Suppress("USELESS_ELVIS")
  val response = interceptor.intercept(next) ?: throw NullPointerException(
      "interceptor $interceptor returned null")

  if (exchange != null) {
    //保证ConnectInterceptor之后的拦截器都运行过proceed方法一次
    check(index + 1 >= interceptors.size || next.calls == 1) {
      "network interceptor $interceptor must call proceed() exactly once"
    }
  }
  
  check(response.body != null) { "interceptor $interceptor returned a response with no body" }

  return response
}

这里的check就是判断里面的条件,如果为false就会抛出错误。calls则是每个拦截器调用proceed方法的次数,需要注意的是exchange是ConnectInterceptor才不为空的,所以在ConnectInterceptor之前的拦截器都有可能走多次proceed方法,而在ConnectInterceptor之后的只有networkInterceptors和CallServerInterceptor,CallServerInterceptor是不会允许proceed方法的。所以这里的报错消息是network interceptor must call proceed() exactly once。
再看回copy方法,就是创建下一个拦截器的责任链,所以index要+1,然后再拿到下一个拦截器,调用他的intercept方法,这里就是每一个拦截器里面要具体实现的方法了,这保证了除了最后一个拦截器,前面的拦截器都会至少执行一次proceed方法(实际上第一个应用拦截器也可以不执行proceed方法)。
可以看到核心就是责任链模式,去调用每一个拦截器,最后会返回response,不用关心每个拦截器具体做了什么,只用把request发到责任链,然后得到需要的response就可以了。

那么拿到了response后,就只有最后一步了

      client.dispatcher.finished(this)

进去里面看一下,主要就是promoteAndExecute方法,以及会调用idleCallback,这里的idleCallback就是当dispatcher空闲下来之后会执行的回调

//调用的finished(runningSyncCalls, call)
private fun <T> finished(calls: Deque<T>, call: T) {
  val idleCallback: Runnable?
  synchronized(this) {
    if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
    //每次dispatcher变空闲的时候会进行的回调
    idleCallback = this.idleCallback
  }

  val isRunning = promoteAndExecute()

  if (!isRunning && idleCallback != null) {
    idleCallback.run()
  }
}

再看看promoteAndExecute方法

private fun promoteAndExecute(): Boolean {
  this.assertThreadDoesntHoldLock()

  val executableCalls = mutableListOf<AsyncCall>()
  val isRunning: Boolean
  synchronized(this) {
    val i = readyAsyncCalls.iterator()
    //遍历readyAsyncCalls
    while (i.hasNext()) {
      val asyncCall = i.next()

      if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
      if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

      i.remove()
      asyncCall.callsPerHost.incrementAndGet()
      //把准备运行的任务放入线程池和异步运行队列
      executableCalls.add(asyncCall)
      runningAsyncCalls.add(asyncCall)
    }
    isRunning = runningCallsCount() > 0
  }

  for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
    //放入线程池
    asyncCall.executeOn(executorService)
  }

  return isRunning
}

这里就是循环把readyAsyncCalls里面的任务取出来,然后放进线程池以及运行中的异步队列,这个方法在enqueue和finish方法中都会调用,所以有新的请求入队和当前请求完成后,都要重新把任务提交到线程池。

  • 题外话:为什么双端队列这里用数组而不是链表,因为每次有新的请求或者完成请求都会把readyAsyncCalls里面的转换成runningAsyncCalls,而如果用链表的话会分散在内存各个地方,cpu缓存无法带来便利,并且在垃圾回收上也没有数组好。

各级拦截器作用

  1. interceptors
    用户自定义的拦截器,主要就是添加自定义的header,参数。且这个拦截器只会调用一次,所以可以用来统计网络请求发起情况,但是他可以因为本地异常重试调用多次proceed方法,或者因为中断不调用proceed方法。
  2. RetryAndFollowUpInterceptor
    重定向拦截器,这个拦截器就是用来处理错误重试和重定向的,内部会开启一个循环,如果路由连接失败或者服务器连接失败就会重试,所以也会多次调用proceed方法。
  3. BridgeInterceptor
    应用层和网络层的桥梁,主要就是为你的请求头添加各种固定信息,然后如果请求头用了头部压缩算法的话(gzip),就会在这一层进行解压。
  4. CacheInterceptor
    缓存拦截器,里面有一个缓存策略,返回networkRequest和cacheResponse,如果两者都为空,就意味着禁止使用网络并且缓存不够充足,就会失败。如果命中缓存了,就直接返回缓存里面的,没有缓存才会继续往下走,执行proceed方法。需要注意okhttp只有get方法的缓存。
  5. ConnectInterceptor
    连接拦截器,这里主要就是初始化了之前提到的exchange,这里主要有一个连接池来管理我们的连接,不然每次走网络申请都要建立一次连接,太耗费资源了。像http后面就提出长连接和多路复用这些来优化,这里有五种方法来获取连接。

1.先会查询call中是否已经有连接
2.从连接池中找
3.计算路径,再次从连接池中寻找
4.自己建立一个新的连接,如果第五种方法没有成功就会把这个新连接放到连接池中
5.从连接池中找是否有多路复用的连接,如果找到那么第四种自己建立的连接就会关闭,返回多路复用的连接

  1. networkInterceptors
    自定义的网络拦截器,因为在RetryAndFollowUpInterceptor后面,所以如果网络发生错误或者重定向该拦截器有可能多次被调用,但是只能调用一次proceed方法,通常用于监控网络层的数据传输。
  2. CallServerInterceptor
    请求拦截器,最后一个拦截器,就是对服务器发起了网络请求,没有运行proceed方法。

addInterceptor与addNetworkInterceptor的区别

从调用顺序来看,addInterceptor在责任链的第一个,addNetworkInterceptor在倒数第二个。addInterceptor在应用层中,一般只能被调用一次,允许多次调用proceed或者不调用proceed,多用于统计网络请求发起情况。而addNetworkInterceptor则可以因为重定向的原因调用多次,但是只能走一次proceed方法。多用于统计网络传输的数据。
参考资料:
面试官:听说你熟悉OkHttp原理?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容