zoukankan      html  css  js  c++  java
  • 从设计模式角度看OkHttp源码

    前言

    说到源码,很多朋友都觉得复杂,难理解。

    但是,如果是一个结构清晰且完全解耦的优质源码库呢?

    OkHttp就是这样一个存在,对于这个原生网络框架,想必大家也看过很多很多相关的源码解析了。

    它的源码好看,易读,清晰,所以今天我准备从设计模式的角度再来读一遍 OkHttp的源码。

    主要内容就分为两类:

    • okhttp的基本运作流程
    • 涉及到的设计模式

    (本文源码版本为okhttp:4.9.0,拦截器会放到下期再讲)

    使用

    读源码,首先就要从它的使用方法开始:

    	val okHttpClient = OkHttpClient()
        val request: Request = Request.Builder()
            .url(url)
            .build()
        okHttpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.d(TAG, "onFailure: ")
            }
    
            override fun onResponse(call: Call, response: Response) {
                Log.d(TAG, "onResponse: " + response.body?.string())
            }
        })
    

    从这个使用方法来看,我抽出了四个重要信息:

    • okHttpClient
    • Request
    • newCall(request)
    • enqueue(Callback)

    大体意思我们可以先猜猜看:

    配置一个客户端实例okHttpClient和一个Request请求,然后这个请求通过okHttpClientnewCall方法封装,最后用enqueue方法发送出去,并收到Callback响应。

    接下来就一个个去认证,并找找其中的设计模式。

    okHttpClient

    首先看看这个okhttp的客户端对象,也就是okHttpClient

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new HttpLoggingInterceptor()) 
            .readTimeout(500, TimeUnit.MILLISECONDS)
            .build();
    

    在这里,我们实例化了一个HTTP的客户端client,然后配置了它的一些参数,比如拦截器、超时时间

    这种我们通过一个统一的对象,调用一个接口或方法,就能完成我们的需求,而起内部的各种复杂对象的调用和跳转都不需要我们关心的设计模式就是外观模式(门面模式)

    外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。

    其重点就在于系统内部和各个子系统之间的复杂关系我们不需要了解,只需要去差遣这个门面 就可以了,在这里也就是OkHttpClient

    它的存在就像一个接待员,我们告诉它我们的需求,要做的事情。然后接待员去内部处理,各种调度,最终完成。

    外观模式主要解决的就是降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。

    这个模式也是三方库很常用的设计模式,给你一个对象,你只需要对这个对象使唤,就可以完成需求。

    当然,这里还有一个比较明显的设计模式是建造者模式,下面会说到。

    Request

    val request: Request = Request.Builder()
        .url(url)
        .build()
    
    //Request.kt
    open class Builder {
        internal var url: HttpUrl? = null
        internal var method: String
        internal var headers: Headers.Builder
        internal var body: RequestBody? = null
    
        constructor() {
          this.method = "GET"
          this.headers = Headers.Builder()
        }
    
        open fun build(): Request {
          return Request(
              checkNotNull(url) { "url == null" },
              method,
              headers.build(),
              body,
              tags.toImmutableMap()
          )
        }
    }
    
    

    Request的生成代码中可以看到,用到了其内部类Builder,然后通过Builder类组装出了一个完整的有着各种参数的Request类

    这也就是典型的 建造者(Builder)模式

    建造者(Builder)模式,将一个复杂的对象的构建与它的表示分离,是的同样的构建过程可以创建不同的表示。

    我们可以通过Builder,构建了不同的Request请求,只需要传入不同的请求地址url,请求方法method,头部信息headers,请求体body即可。
    (这也就是网络请求中的请求报文的格式)

    这种可以通过构建形成不同的表示的 设计模式 就是 建造者模式,也是用的很多,主要为了方便我们传入不同的参数进行构建对象。

    又比如上面okHttpClient的构建。

    newCall(request)

    接下来是调用OkHttpClient类的newCall方法获取一个可以去调用enqueue方法的接口。

    //使用
    val okHttpClient = OkHttpClient()
    okHttpClient.newCall(request)
    
    //OkHttpClient.kt
    open class OkHttpClient internal constructor(builder: Builder) : Cloneable, Call.Factory, WebSocket.Factory {
      override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
    }
    
    //Call接口
    interface Call : Cloneable {
      fun execute(): Response
    
      fun enqueue(responseCallback: Callback)
    
      fun interface Factory {
        fun newCall(request: Request): Call
      }
    }
    

    newCall方法,其实是Call.Factory接口里面的方法。

    也就是创建Call的过程,是通过Call.Factory接口的newCall方法创建的,而真正实现这个方法交给了这个接口的子类OkHttpClient

    那这种定义了统一创建对象的接口,然后由子类来决定实例化这个对象的设计模式就是 工厂模式

    在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

    当然,okhttp这里的工厂有点小,只有一条生产线,就是Call接口,而且只有一个产品,RealCall

    enqueue(Callback)

    接下来这个方法enqueue,肯定就是okhttp源码的重中之重了,刚才说到newCall方法其实是获取了RealCall对象,所以就走到了RealCall的enqueue方法:

      override fun enqueue(responseCallback: Callback) {
        client.dispatcher.enqueue(AsyncCall(responseCallback))
      }
    

    再转向dispatcher。

    //Dispatcher.kt
    
      val executorService: ExecutorService
        get() {
          if (executorServiceOrNull == null) {
            executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
                SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
          }
          return executorServiceOrNull!!
        }
    
    
      internal fun enqueue(call: AsyncCall) {
        promoteAndExecute()
      }
    
    
      private fun promoteAndExecute(): Boolean {
        //通过线程池切换线程
        for (i in 0 until executableCalls.size) {
          val asyncCall = executableCalls[i]
          asyncCall.executeOn(executorService)
        }
    
        return isRunning
      }
    
    
    //RealCall.kt
      fun executeOn(executorService: ExecutorService) {
    
          try {
            executorService.execute(this)
            success = true
          } 
        }
    

    这里用到了一个新的类Dispatcher,调用到的方法是asyncCall.executeOn(executorService)

    这个executorService参数大家应该都熟悉吧,线程池。最后是调用executorService.execute方法执行线程池任务。

    而线程池的概念其实也是用到了一种设计模式,叫做享元模式

    享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

    其核心就在于共享对象,所有很多的池类对象,比如线程池、连接池等都是采用了享元模式 这一设计模式。当然,okhttp中不止是有线程池,还有连接池提供连接复用,管理所有的socket连接。

    再回到Dispatcher,所以这个类是干嘛的呢?就是切换线程用的,因为我们调用的enqueue是异步方法,所以最后会用到线程池切换线程,执行任务。

    继续看看execute(this)中的this任务。

    execute(this)

    override fun run() {
          threadName("OkHttp ${redactedUrl()}") {
            try {
              //获取响应报文,并回调给Callback
              val response = getResponseWithInterceptorChain()
              responseCallback.onResponse(this@RealCall, response)
            } catch (e: IOException) {
              if (!signalledCallback) {
                responseCallback.onFailure(this@RealCall, e)
              } 
            } catch (t: Throwable) {
              cancel()
              if (!signalledCallback) {
                
                responseCallback.onFailure(this@RealCall, canceledException)
              }
            } 
          }
    
    

    没错,这里就是请求接口的地方了,通过getResponseWithInterceptorChain方法获取响应报文response,然后通过Callback的onResponse方法回调,或者是有异常就通过onFailure方法回调。

    那同步方法是不是就没用到线程池呢?去找找execute方法:

      override fun execute(): Response {
        //...
        return getResponseWithInterceptorChain()
      }
    

    果然,通过execute方法就直接返回了getResponseWithInterceptorChain,也就是响应报文。

    到这里,okhttp的大体流程就结束了,这部分的流程大概就是:

    设置请求报文 -> 配置客户端参数 -> 根据同步或异步判断是否用子线程 -> 发起请求并获取响应报文 -> 通过Callback接口回调结果

    剩下的内容就全部在getResponseWithInterceptorChain方法中,这也就是okhttp的核心。

    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(
            interceptors = interceptors
            //...
        )
    
        val response = chain.proceed(originalRequest)
      }
    

    代码不是很复杂,就是 加加加 拦截器,然后组装成一个chain类,调用proceed方法,得到响应报文response。

      override fun proceed(request: Request): Response {
    
        //找到下一个拦截器
        val next = copy(index = index + 1, request = request)
        val interceptor = interceptors[index]
    
       
        val response = interceptor.intercept(next)
        return response
      }
    

    简化了下代码,主要逻辑就是获取下一个拦截器(index+1),然后调用拦截器的intercept方法。

    然后在拦截器里面的代码统一都是这种格式:

      override fun intercept(chain: Interceptor.Chain): Response {
        //做事情A
    
        response = realChain.proceed(request)
    
        //做事情B
      }
    

    结合两段代码,会形成一条链,这条链组织了所有连接器的工作。类似这样:

    拦截器1做事情A -> 拦截器2做事情A -> 拦截器3做事情A -> 拦截器3做事情B -> 拦截器2做事情B -> 拦截器1做事情B

    应该是好理解的吧,通过proceed方法把每个拦截器连接起来了。

    而最后一个拦截器ConnectInterceptor就是分割事情A和事情B,其作用就是进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

    所以事情A和事情B是什么意思呢?其实就代表了通信之前的事情和通信之后的事情。

    再来个动画:

    这种思想是不是有点像..递归?没错,就是递归,先递进执行事情A,再回归做事情B。

    而这种递归循环,其实也就是用到了设计模式中的 责任链模式

    责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。

    简单的说,就是让每个对象都能有机会处理这个请求,然后各自完成自己的事情,一直到事件被处理。Android中的事件分发机制也是用到了这种设计模式。

    接下来就是了解每个拦截器到底做了什么事,就可以了解到okhttp的整个流程了,这就是下期的内容了。

    先预告一波:

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

    总结

    读完okhttp的源码,感觉就一个字:舒服

    一份好的代码应该就是这样,各模块之间通过各种设计模式进行解耦,阅读者可以每个模块分别去去阅读了解,而不是各个模块缠绵在一起,杂乱无章。

    最后再总结下okhttp中涉及到的设计模式:

    • 外观模式。通过okHttpClient这个外观去实现内部各种功能。
    • 建造者模式。构建不同的Request对象。
    • 工厂模式。通过OkHttpClient生产出产品RealCall。
    • 享元模式。通过线程池、连接池共享对象。
    • 责任链模式。将不同功能的拦截器形成一个链。

    其实还是有一些设计模式没说到的,比如

    • websocket相关用到的观察者模式
    • Cache集合相关的迭代器模式

    以后遇到了再做补充吧。

    参考

    https://www.runoob.com/design-pattern/design-pattern-tutorial.html
    https://www.jianshu.com/p/ae2fe5481994
    https://juejin.cn/post/6895369745445748749

    拜拜

    感谢大家的阅读,有一起学习的小伙伴可以关注下我的公众号——码上积木❤️❤️
    每日一个知识点,积少成多,建立知识体系架构。
    这里有一群很好的Android小伙伴,欢迎大家加入~

  • 相关阅读:
    丑数系列
    452. 用最少数量的箭引爆气球
    406. 根据身高重建队列
    763. 划分字母区间
    所有二叉树题目记录
    二叉树前中后序遍历非递归(迭代)解法
    二叉树的层序遍历题目汇总
    442. 数组中重复的数据&&448. 找到所有数组中消失的数字
    225. 用队列实现栈(Easy)
    使用ClosedXML读写excel
  • 原文地址:https://www.cnblogs.com/jimuzz/p/14536105.html
Copyright © 2011-2022 走看看