从设计模式看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是什么意思呢?其实就代表了通信之前的事情和通信之后的事情。


再来个动画:


16.gif


这种思想是不是有点像..递归?没错,就是递归,先递进执行事情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.htmlhttps://www.jianshu.com/p/ae2fe5481994https://juejin.cn/post/6895369745445748749

目录
相关文章
|
12天前
|
设计模式 存储 Java
「全网最细 + 实战源码案例」设计模式——责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,允许将请求沿着处理者链进行发送。每个处理者可以处理请求或将其传递给下一个处理者,从而实现解耦和灵活性。其结构包括抽象处理者(Handler)、具体处理者(ConcreteHandler)和客户端(Client)。适用于不同方式处理不同种类请求、按顺序执行多个处理者、以及运行时改变处理者及其顺序的场景。典型应用包括日志处理、Java Web过滤器、权限认证等。
48 13
「全网最细 + 实战源码案例」设计模式——责任链模式
|
12天前
|
设计模式 存储 算法
「全网最细 + 实战源码案例」设计模式——命令模式
命令模式(Command Pattern)是一种行为型设计模式,将请求封装成独立对象,从而解耦请求方与接收方。其核心结构包括:Command(命令接口)、ConcreteCommand(具体命令)、Receiver(接收者)和Invoker(调用者)。通过这种方式,命令的执行、撤销、排队等操作更易扩展和灵活。 适用场景: 1. 参数化对象以操作。 2. 操作放入队列或远程执行。 3. 实现回滚功能。 4. 解耦调用者与接收者。 优点: - 遵循单一职责和开闭原则。 - 支持命令组合和延迟执行。 - 可实现撤销、恢复功能。 缺点: - 增加复杂性和类数量。
49 14
「全网最细 + 实战源码案例」设计模式——命令模式
|
14天前
|
设计模式 算法 开发者
「全网最细 + 实战源码案例」设计模式——策略模式
策略模式(Strategy Pattern)是一种行为型设计模式,用于定义一系列可替换的算法或行为,并将它们封装成独立的类。通过上下文持有策略对象,在运行时动态切换算法,提高代码的可维护性和扩展性。适用于需要动态切换算法、避免条件语句、经常扩展算法或保持算法独立性的场景。优点包括符合开闭原则、运行时切换算法、解耦上下文与策略实现、减少条件判断;缺点是增加类数量和策略切换成本。示例中通过定义抽象策略接口和具体策略类,结合上下文类实现动态算法选择。
49 8
「全网最细 + 实战源码案例」设计模式——策略模式
|
14天前
|
设计模式 SQL 算法
「全网最细 + 实战源码案例」设计模式——模板方法模式
模板方法模式是一种行为型设计模式,定义了算法的骨架并在父类中实现不变部分,将可变部分延迟到子类实现。通过这种方式,它避免了代码重复,提高了复用性和扩展性。具体步骤由抽象类定义,子类实现特定逻辑。适用于框架设计、工作流和相似算法结构的场景。优点包括代码复用和符合开闭原则,缺点是可能违反里氏替换原则且灵活性较低。
60 7
「全网最细 + 实战源码案例」设计模式——模板方法模式
|
16天前
|
设计模式 存储 安全
「全网最细 + 实战源码案例」设计模式——组合模式
组合模式(Composite Pattern)是一种结构型设计模式,用于将对象组合成树形结构以表示“部分-整体”的层次结构。它允许客户端以一致的方式对待单个对象和对象集合,简化了复杂结构的处理。组合模式包含三个主要组件:抽象组件(Component)、叶子节点(Leaf)和组合节点(Composite)。通过这种模式,客户端可以统一处理简单元素和复杂元素,而无需关心其内部结构。适用于需要实现树状对象结构或希望以相同方式处理简单和复杂元素的场景。优点包括支持树形结构、透明性和遵循开闭原则;缺点是可能引入不必要的复杂性和过度抽象。
72 22
|
16天前
|
设计模式 存储 缓存
「全网最细 + 实战源码案例」设计模式——享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在减少大量相似对象的内存消耗。通过分离对象的内部状态(可共享、不变)和外部状态(依赖环境、变化),它有效减少了内存使用。适用于存在大量相似对象且需节省内存的场景。模式优点包括节省内存和提高性能,但会增加系统复杂性。实现时需将对象成员变量拆分为内在和外在状态,并通过工厂类管理享元对象。
148 83
|
18天前
|
设计模式 前端开发 数据库
「全网最细 + 实战源码案例」设计模式——桥接模式
桥接模式(Bridge Pattern)是一种结构型设计模式,通过将抽象部分与实现部分分离,使它们可以独立变化,从而降低代码耦合度,避免类爆炸,提高可扩展性。其结构包括实现类接口、具体实现类、抽象类和精确抽象类。适用于多维度扩展类、隐藏实现细节、简化庞杂类以及运行时切换实现方法的场景。优点包括高扩展性、隐藏实现细节、遵循开闭原则和单一职责原则;缺点是可能增加代码复杂度。示例中展示了不同操作系统播放不同格式视频文件的实现。
47 19
|
18天前
|
设计模式 存储
「全网最细 + 实战源码案例」设计模式——装饰者模式
装饰者模式(Decorator Pattern)是一种结构型设计模式,通过“包装”现有对象来为其添加额外功能,而无需修改原有代码。它通过创建装饰类来扩展对象的功能,而非继承。该模式由抽象构件、具体构件、抽象装饰者和具体装饰者组成,允许在运行时动态组合功能。穿衣服的例子很好地解释了装饰者模式:你可以根据需要一层层添加衣物,如毛衣、夹克和雨衣,每件衣物都扩展了基本行为,但不是你的一部分,可以随时脱掉。 优点包括灵活性、避免子类爆炸和符合开闭原则;缺点是可能增加复杂性和难以理解。适用于希望在不修改代码的情况下为对象新增行为的场景,尤其当继承难以实现或不可行时。
51 15
|
20天前
|
设计模式 Java 开发者
「全网最细 + 实战源码案例」设计模式——适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,通过引入适配器类将一个类的接口转换为客户端期望的另一个接口,使原本因接口不兼容而无法协作的类能够协同工作。适配器模式分为类适配器和对象适配器两种,前者通过多重继承实现,后者通过组合方式实现,更常用。该模式适用于遗留系统改造、接口转换和第三方库集成等场景,能提高代码复用性和灵活性,但也可能增加代码复杂性和性能开销。
68 28
|
20天前
|
设计模式 缓存 Java
「全网最细 + 实战源码案例」设计模式——代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,通过代理对象控制对目标对象的访问并添加额外功能。它分为静态代理和动态代理,后者包括JDK动态代理和CGLIB动态代理。JDK动态代理基于接口反射生成代理类,而CGLIB通过继承目标类生成子类。代理模式适用于延迟初始化、访问控制、远程服务、日志记录和缓存等场景,优点是职责分离、符合开闭原则和提高安全性,缺点是增加系统复杂性。
72 25

热门文章

最新文章