OkHttp 深度解析(一) : 从一次完整请求看 OkHttp 整体架构

这是 OkHttp 深度解析系列文章

OkHttp深度解析(一) : 从一次完整请求看 OkHttp整体架构

OkHttp深度解析(二) : OkHttpClient 你没见过的那些属性

OkHttp深度解析(三) : 拦截器?连接复用&合并?Http2?

前言

OkHttp 是一个非常优秀的网络请求框架,几乎每个 Android 开发人员应该都使用过它。

本文深入介绍了 Okhttp 的整体框架结构,OkhttpClient重点属性深度解释,并且深度分析一次完整的的 Http 请求在Okhttp中是如何实现的 ,我个人习惯系统性了解一项技术而不是单纯的罗列源码通篇讲解,因为只有建立出系统,才能理解的更透彻,这样记忆会非常深刻。因此我侧重使用画图的方式来分析 Okhttp(只有关键源码),图是重点,文字是辅助(画图好纍😅)。

这篇文章应该是你从未见过的OkHttp 全流程网络请求的深入解读,它同时也可能解惑你在其它博客中找不到答案的疑问。

OkHttp 发展背景

  • 诞生背景

    在OkHttp出现之前,Android开发者主要使用Apache的HttpClient和Google自家的HttpURLConnection进行网络请求。但这两者多少都会存在一些问题。美国的移动支付公司Square在开发过程中,深感这些原生网络库难以满足其需求,便在2013年开源了OkHttp

  • 发展之路: 从包装到替代

    OkHttp并非一开始就完全另起炉灶,其发展经历了几个阶段:

    1. 初期包装 :最初,OkHttp是对原生HttpClientHttpURLConnection的封装,旨在提供更友好、统一的API。
    2. 剥离与独立 :随着Apache宣布弃用HttpClient,OkHttp也移除了对其的支持。后来,Square团队认为HttpURLConnection也不够好用,索性去掉了对它的依赖,转而直接从 Socket 层完全重写整个 Http 网络请求**,实现了真正的独立。
    3. 反向影响 :OkHttp因其出色的设计和性能,受到了广大开发者的欢迎。Google也注意到了这一点,并在Android 4.4 (KitKat) 系统中,将HttpURLConnection的底层实现替换成了OkHttp**。这意味着,即使你在代码中使用的是标准的HttpURLConnection,其底层实际使用的 Okhttp的代码。
    4. 向Kotlin迈进:从OkHttp 4版本开始,Square开始使用Kotlin语言进行重写

OkHttp 整体框架

首先我们先看一下使用 Okhttp 如何完成一个网络请求:

kotlin 复制代码
//okhttp 网络请求示例(异步)
val client = OkHttpClient()
val request = Request.Builder()
    .url("")
    .build()
client.newCall(request)
    .enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            //处理错误
        }
        override fun onResponse(call: Call, response: Response) {
            //处理 response
        }
    })

代码非常简单,这得益于 Okhttp 优秀的框架设计,以下是以一次网络请求流程的视角来系统的了解 Okhttp 的整体结构:

文字说明:(请在看完文字说明后回来再看一次上面的图,理解会更深刻,而且不容易忘记)

  1. 通过 OkHttpClient.Builder 来构造出一个 OkHttpClient 实例(这个实例在绝大部分场景都应该是单例的),后面会详细介绍 OkHttpClient 中各个属性配置的作用

  2. 通过 Request.Builder 来构造出一个 Request 实例,这表示一个 originalRequest(原始请求),开发者需要配置一个请求的基础参数

  3. 通过 newCall 方法(携带 request 参数)创建一个 Call 对象,实际为 RealCall 实例,它包含了以下参数:

    kotlin 复制代码
    class RealCall(
      val client: OkHttpClient,//开发者创建出来的 OkHttp Client 实例
      /** 一个未经重定向或者没有添加认证头的原始请求,
    	  也就是未经OkHttp内部处理的请求,并不是实际发给服务器的请求 
      */
      val originalRequest: Request,
      val forWebSocket: Boolean //发起的是否是一个WebSocket请求,因为WebSocket也是基于Http的协议
    ) : Call {
    	......
    }

    也就是说我们创建的这个原始请求对象,被转化为 了一个 RealCall,后面的每个 RealCall 就代表了一个请求

  4. 开始使用 execute(同步)/enqueue(异步)的方式发起一个 Http 请求

  5. enqueue 方式: enqueue 需要传入一个 callback 对象来接收返回结果,整体逻辑如下:

    kotlin 复制代码
    override fun enqueue(responseCallback: Callback) {
    	//检查请求的执行状态
      check(executed.compareAndSet(false, true)) { "Already Executed" }
      //事件记录,标记开始执行请求
      callStart()
      //初始化一个异步的 Call,并入队
      client.dispatcher.enqueue(AsyncCall(responseCallback))
    }

    💡 PS: 关于check 方法,这是 Kotlin 标准库中的方法,它内部使用的是 Kotlin Contract 这个高级特性

    此处简单介绍下这个方法,因为在 Okhttp 中大量使用到了它

    kotlin 复制代码
    @kotlin.internal.InlineOnly
    
    public inline fun check(value: Boolean, lazyMessage: () -> Any): Unit {
    
    contract {
    
    returns() implies value
    
    }
    
    if (!value) {
    
    val message = lazyMessage()
    
    throw IllegalStateException(message.toString())
    
    }
    
    }

    contract 表示契约,implies 表示 暗示,这个方法的作用是 开发者 告诉 编译器 如果 check 方法能够正常返回(也就是不会抛出异常),则暗示检查成功,否则抛出异常信息。

    enqueue 方法中 check 的作用就是请求执行状态的检查

    • **executed**是一个 AtomicBoolean,用于标记这个 Call是否已被执行过。compareAndSet(false, true)尝试将其值从 false设置为 true

    • check函数 :如果传入的表达式结果为 false(即设置失败,说明 executed已经是 true,意味着这个 Call已经被执行过),check函数就会抛出一个 IllegalStateException,异常信息为 "Already Executed"。

    • 这保证了 一个 Call实例只能被执行一次,这是 OkHttp 防止重复请求的重要设计。

    🔑 关于 Contract 的更深入理解,请移步此处

    keep on :

    kotlin 复制代码
    internal fun enqueue(call: AsyncCall) {
        synchronized(this) {
    	    //将这个请求添加到准备执行的队列中
          readyAsyncCalls.add(call)
    			/**下面这两行代码的意思是:找到与当前的这个请求主机名相同的Call,
    				然后使它们共享「同一个主机请求个数」这个变量。
    				为何共享?因为 Okhttp 要求同一个主机的请求个数不能大于5个(默认)
    			*/
          if (!call.call.forWebSocket) {
            val existingCall = findExistingCallWithHost(call.host)
            if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
          }
        }
        //开始推举并执行请求
        promoteAndExecute()
      }
    kotlin 复制代码
    //promote 表示推动,意思就是对readyAsyncCalls中 Call 进行进一步的挑选,然后执行
    private fun promoteAndExecute(): Boolean {
        this.assertThreadDoesntHoldLock()
    		//遍历 readyAsyncCalls,挑选出不会导致超负载的 Call 来添加至 executableCalls
    		//同时也会添加到 runningAsyncCalls中(runningAsyncCalls中也包含已取消单但未执行完的Call)
    		//runningAsyncCalls 主要用于记录正在运行的 Call
        val executableCalls = mutableListOf<AsyncCall>()
        val isRunning: Boolean
        synchronized(this) {
          val i = readyAsyncCalls.iterator()
          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
        }
    		//开始遍历 executableCalls 然后执行每个 AsyncCall
        for (i in 0 until executableCalls.size) {
          val asyncCall = executableCalls[i]
          asyncCall.executeOn(executorService)
        }
        return isRunning
      }

    由于 AsyncCall 继承了 Runnable ,因此它表示一个需要放进线程池中执行的具体任务,这是异步请求的核心(此处仅贴出核心代码)

    kotlin 复制代码
    internal inner class AsyncCall(
        private val responseCallback: Callback
      ) : Runnable {
        @Volatile var callsPerHost = AtomicInteger(0)
          private set
    
        fun reuseCallsPerHostFrom(other: AsyncCall) {
          this.callsPerHost = other.callsPerHost
        }
        ...
        
        fun executeOn(executorService: ExecutorService) {
          ...
          executorService.execute(this)
          ...
        }
    
        override fun run() {
          ...
          //这是异步任务的核心,通过拦截器链的链式调用方式执行一个请求并返回结果
          val response = getResponseWithInterceptorChain()
          ...
          //把返回的 response 传给 callback
          responseCallback.onResponse(this@RealCall, response)
          ...
        }
      }
相关推荐
v***91301 小时前
Spring+Quartz实现定时任务的配置方法
android·前端·后端
wilsend2 小时前
Android Studio 2024版新建java项目和配置环境下载加速
android
兰琛2 小时前
Android Compose展示PDF文件
android·pdf
走在路上的菜鸟3 小时前
Android学Dart学习笔记第四节 基本类型
android·笔记·学习
百锦再3 小时前
第21章 构建命令行工具
android·java·图像处理·python·计算机视觉·rust·django
skyhh5 小时前
Android Studio 最新版汉化
android·ide·android studio
路人甲ing..5 小时前
Android Studio 快速的制作一个可以在 手机上跑的app
android·java·linux·智能手机·android studio
携欢8 小时前
PortSwigger靶场之Web shell upload via path traversal靶场通关秘籍
android
曹绍华15 小时前
okhttp建造者模式详解
okhttp·建造者模式