从设计模式看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

目录
相关文章
|
6月前
|
设计模式 Oracle Java
设计模式--- 桥接模式、JDBC 源码剖析(桥接)
设计模式--- 桥接模式、JDBC 源码剖析(桥接)
122 2
|
6月前
|
设计模式 API 开发者
ZStack源码剖析之设计模式鉴赏——三驾马车
随着ZStack的版本迭代,其可以掌管的资源也越来越多。但新增模块的结构却还是大致相同,此即是ZStack的经典设计模式——这套模式也被开发者称为ZStack三驾马车。
84 2
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
420 37
|
3月前
|
设计模式 存储 缓存
Guava 源码中7种设计模式的实现分析
V 哥在学习 Guava 源码中总结的7个设计模式的实现分析,欢迎关注威哥爱编程,做自己的技术,让别人去卷吧。
|
6月前
|
设计模式 安全 Java
【初学者慎入】Spring源码中的16种设计模式实现
以上是威哥给大家整理了16种常见的设计模式在 Spring 源码中的运用,学习 Spring 源码成为了 Java 程序员的标配,你还知道Spring 中哪些源码中运用了设计模式,欢迎留言与威哥交流。
336 3
|
6月前
|
设计模式 Java Go
[设计模式Java实现附plantuml源码~结构型]不兼容结构的协调——适配器模式
[设计模式Java实现附plantuml源码~结构型]不兼容结构的协调——适配器模式
|
6月前
|
设计模式 安全 Java
[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式
[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式
|
6月前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~结构型] 扩展系统功能——装饰模式
[设计模式Java实现附plantuml源码~结构型] 扩展系统功能——装饰模式
|
6月前
|
设计模式 JavaScript 前端开发
[设计模式Java实现附plantuml源码~结构型] 提供统一入口——外观模式
[设计模式Java实现附plantuml源码~结构型] 提供统一入口——外观模式
|
6月前
|
设计模式 JavaScript 前端开发
[设计模式Java实现附plantuml源码~创建型] 复杂对象的组装与创建——建造者模式
[设计模式Java实现附plantuml源码~创建型] 复杂对象的组装与创建——建造者模式

热门文章

最新文章

  • 1
    C++一分钟之-设计模式:工厂模式与抽象工厂
    43
  • 2
    《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
    50
  • 3
    C++一分钟之-C++中的设计模式:单例模式
    58
  • 4
    《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
    38
  • 5
    《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
    63
  • 6
    Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
    58
  • 7
    Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
    42
  • 8
    Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
    50
  • 9
    Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
    110
  • 10
    Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
    78
  • 下一篇
    无影云桌面