当前位置: 首页 > 科技观察

从设计模式看OkHttp源码

时间:2023-03-14 22:46:02 科技观察

前言说到源码,很多朋友都觉得复杂难懂。但是,如果是结构清晰、解耦彻底的优质源码库呢?OkHttp就是这样一个存在。对于这个原生的网络框架,你一定看过很多相关的源码分析。它的源代码易于阅读和清晰。所以今天我打算从设计模式的角度来重新阅读一下OkHttp的源码。主要内容分为两大类:OkHttp基本运行过程中涉及的设计模式(本文源码版本为okhttp:4.9.0,拦截器下期再讲)。开始:valokHttpClient=OkHttpClient()valrequest:Request=Request.Builder().url(url).build()okHttpClient.newCall(request).enqueue(object:Callback{overridefunonFailure(call:Call,e:IOException){Log.d(TAG,"onFailure:")}overridefunonResponse(call:Call,response:Response){Log.d(TAG,"onResponse:"+response.body?.string())}})从这个使用方法看,我提取了四个重要的信息:okHttpClientRequestnewCall(request)enqueue(Callback)大概意思我们先可以猜到:配置一个客户端实例okHttpClient和一个Request请求,然后这个请求被okHttpClient的newCall方法封装,最后,使用enqueue方法将其发送出去并收到Callback响应。接下来一个一个去认证,找里面的设计模式。okHttpClient首先看okhttp的客户端对象,也就是okHttpClient。OkHttpClientclient=newOkHttpClient.Builder().addInterceptor(newHttpLoggingInterceptor()).readTimeout(500,TimeUnit.MILLISECONDS).build();这里,我们实例化了一个HTTP客户端客户端,然后配置了它的一些参数,比如拦截器和超时时间。这样我们就可以通过一个统一的对象来调用接口或者方法来实现我们的需求,而内部各种复杂对象的调用和跳转就不需要我们去关心了。设计模式就是外观模式(facadepattern)。FacadePattern隐藏了系统的复杂性,为客户端提供了一个接口,客户端可以通过该接口访问系统。这类设计模式是一种结构模式,它在现有系统上增加一个接口,以隐藏系统的复杂性。重点是我们不需要了解系统和各个子系统之间的复杂关系,我们只需要调度这个门面,这里就是OkHttpClient。它就像接待员一样存在,我们告诉它我们的需求,要做的事情。然后接待员去内部处理,各种调度,终于完成了。外观模式的主要解决方案是降低访问复杂系统内部子系统的复杂度,简化客户端与它们之间的接口。这种模式也是三方库非常常见的设计模式。给你一个对象,你只需要调用这个对象就可以完成你的需求。当然,这里还有一个很明显的设计模式就是建造者模式,下面会提到。Requestvalrequest:Request=Request.Builder().url(url).build()//Request.ktopenclassBuilder{internalvarurl:HttpUrl?=nullinternalvarmethod:Stringinternalvarheaders:Headers.Builderinternalvarbody:RequestBody?=nullconstructor(){this.method="GET“this.headers=Headers.Builder()}openfunbuild():Request{returnRequest(checkNotNull(url){"url==null"},方法,headers.build(),body,tags.toImmutableMap())}}从Request的生成代码可以看出,使用了它的内部类Builder,然后通过Builder类组装了一个完整的带有各种参数的Request类。这是典型的建造者模式。Builder模式将复杂对象的构造与其表示分离,使得相同的构造过程可以创建不同的表示。我们可以使用Builder构造不同的Request请求。我们只需要传入不同的请求地址url、请求方法method、header信息headers、请求body即可。(这也是网络请求中请求报文的格式)这种通过构造可以形成不同表示的设计模式就是建造者模式,也用的比较多,主要是为了方便我们传入不同的参数给构造对象。再比如上面okHttpClient的构造。newCall(request)接下来就是调用OkHttpClient类的newCall方法,获取可以调用enqueue方法的接口。//使用valokHttpClient=OkHttpClient()okHttpClient.newCall(request)//OkHttpClient.ktopenclassOkHttpClientinternalconstructor(builder:Builder):Cloneable,Call.Factory,WebSocket.Factory{overridefunnewCall(request:Request):Call=RealCall,(this,forWebSocket=false)}//调用接口interfaceCall:Cloneable{funexecute():Responsefunenqueue(responseCallback:Callback)funinterfaceFactory{funnewCall(request:Request):Call}}newCall方法其实就是Call.Factory接口中的一个方法。即创建Call的过程是通过Call.Factory接口的newCall方法创建的,而该方法的实际实现则交给了该接口的子类OkHttpClient。定义统一创建对象的接口,然后子类决定实例化对象的设计模式就是工厂模式。在工厂模式下,我们在创建对象时不向客户端暴露创建逻辑,而是使用一个通用的接口指向新创建的对象。当然okhttp这边的工厂有点小,只有一条生产线,就是Call接口,只有一个产品RealCall。enqueue(Callback)下一个方法enqueue必须是okhttp源码的重中之重。刚才说了newCall方法其实是获取了RealCall对象,所以我就去RealCall的enqueue方法:overridefunenqueue(responseCallback:Callback){client.dispatcher.enqueue(AsyncCall(responseCallback))}然后转到dispatcher。//Dispatcher.ktvalexecutorService:ExecutorServiceget(){if(executorServiceOrNull==null){executorServiceOrNull=ThreadPoolExecutor(0,Int.MAX_VALUE,60,TimeUnit.SECONDS,SynchronousQueue(),threadFactory("$okHttpNameDispatcher",false))}returnexecutorServiceOrNull!!}internalfunenqueue(call:AsyncCall){promoteAndExecute()}privatefunpromoteAndExecute():Boolean{//通过线程池切换线程for(iin0untilexecutableCalls.size){valasyncCall=executableCalls[i]asyncCall.executeOn(executorService)}returnisRunning}//RealCall.ktfunexecuteOn(executorService:ExecutorService){try{executorService.execute(this)success=true}}这里使用了一个新的类Dispatcher,调用的方法是asyncCall.executeOn(executorService)。executorService这个参数大家应该不陌生吧,线程池。最后调用executorService.execute方法执行线程池任务。线程池的概念实际上使用了一种称为享元模式的设计模式。FlyweightPattern主要用于减少创建对象的数量,以减少内存使用,提高性能。这类设计模式是一种结构模式,它提供了一种方法来减少对象的数量,从而改善应用程序所需的对象结构。它的核心在于共享对象,很多池对象,比如线程池,连接池,都采用享元模式的设计模式。当然,okhttp中不仅有线程池,还有连接池,提供连接多路复用,管理所有socket连接。回到Dispatcher,这个类是做什么用的?是用来切换线程的,因为我们调用的enqueue是一个异步方法,所以最后会使用线程池来切换线程和执行任务。继续看execute(this)中的this任务。execute(this)overridefunrun(){threadName("OkHttp${redactedUrl()}"){try{//获取响应消息并回调Callbackvalresponse=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方法:overridefunexecute():Response{//...returngetResponseWithInterceptorChain()}果然通过execute方法直接返回了getResponseWithInterceptorChain,也就是responsemessage。至此,okhttp的大致流程就结束了。这部分的流程大致是:设置请求消息->配置客户端参数->根据同步还是异步判断使用子线程->发起请求并获取响应消息->回调结果的剩余内容通过Callback接口都在getResponseWithInterceptorChain方法中,是okhttp的核心。getResponseWithInterpectionspectectereptereptresponseponsewithWithInterRchain():响应{//buildafullstackofienters.valinterpeptors.valinterpeptors=mutablepteptenteptionportof()interpectors>()){interceptors+=client.networkInterceptors}interceptors+=CallServerInterceptor(forWebSocket)valchain=RealInterceptorChain(interceptors=interceptors//...)valresponse=chain.proceed(originalRequest)}代码不是很复杂,添加拦截器即可,然后组装形成链式类,调用proceed方法,获取响应消息response。overridefunproceed(request:Request):Response{//寻找下一个拦截器valnext=copy(index=index+1,request=request)valinterceptor=interceptors[index]valresponse=interceptor.intercept(next)returnresponse}简化了代码,主要逻辑是获取下一个拦截器(index+1),然后调用拦截器的intercept方法。那么拦截器中的代码统一为这样的格式:overridefunintercept(chain:Interceptor.Chain):Response{//做事Aresponse=realChain.proceed(request)//做事B}结合两段代码,就会形成一个组织所有连接器工作的链。像这样:拦截器1做事A->拦截器2做事A->拦截器3做事A->拦截器3做事B->拦截器2做事B->拦截器1做事B应该很容易理解,每个拦截器通过proceed方法连接。而最后一个拦截器ConnectInterceptor,将事物A和事物B进行了拆分,其作用是与服务器通信,向服务器发送数据,解析读取到的响应数据。那么A和B是什么意思呢?实际上,它们代表了通信之前的事物和通信之后的事物。另一个动画:这种思路是不是有点像..递归?没错,就是递归,先循序渐进地执行A事,然后返回做B事。而这个递归循环其实就是用了设计模式中的责任链模式。责任链模式为请求创建一个接收者对象链。在给定请求类型的情况下,此模式将请求的发送方和接收方解耦。简单的说,就是让每个对象都有机会处理这个请求,然后完成自己的事务,直到事件处理完毕。Android中的事件分发机制也采用了这种设计模式。接下来就是了解各个拦截器的作用,然后就可以了解okhttp的整个流程,也就是下一期的内容了。首先注意:addInterceptor(Interceptor),由开发者设置,会根据开发者的要求,在所有拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header可以在这里添加。RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,还有失败请求的重试工作,后续请求重定向的工作。BridgeInterceptor,这里会构造一个用户访问网络的请求,同时后续工作会将网络请求返回的响应Response转换为用户可用的Response,比如添加文件类型,添加内容-长度计算和解压gzip。CacheInterceptor,这里主要处理缓存相关的处理。它会根据OkHttpClient对象的配置和缓存策略缓存请求值,如果有本地缓存??可用,无需网络交互即可返回缓存结果。ConnectInterceptor,这里主要负责建立连接,会建立TCP连接或者TLS连接,HttpCodec负责编解码。networkInterceptors,这里也是开发者自己设置的,所以本质上和第一个拦截器类似,但是因为位置不同,用途也不同。这个位置添加的拦截器可以看到请求和响应的数据,所以可以做一些网络调试。CallServerInterceptor,这里是网络数据的请求和响应,也就是实际的网络I/O操作,通过socket读写数据。看完okhttp的源码,感觉就一个字:舒服。好的代码应该是这样的。这些模块通过各种设计模式解耦。读者可以分别阅读和理解每个模块,而不是模块之间纠缠不清、杂乱无章。最后总结一下okhttp涉及的设计模式:外观模式。利用okHttpClient的外观实现各种内部功能。建设者模式。构造不同的Request对象。工厂模式。产品RealCall是通过OkHttpClient产生的。享元模式。通过线程池和连接池共享对象。责任链模型。形成一个具有不同功能的拦截器链。其实还有一些设计模式没有提到,比如websocket中使用的观察者模式。与缓存集合相关的迭代器模式。以后遇到了再补充。参考https://www.runoob.com/design-pattern/design-pattern-tutorial.htmlhttps://www.jianshu.com/p/ae2fe5481994https://juejin.cn/post/6895369745445748749本文为转载自微信公众号“码积木”,可通过以下二维码关注。转载本文请联系代码学习公众号。