OkHttp可以说是Android开发中最常用的网络请求框架了。OkHttp简单易用,扩展性强,功能强大。OKHttp的源码和原理也是面试的常客。但是,OKHttp的源代码内容很多。想要学习它的源码,往往线索很多,一时抓不住重点。本文从几个问题入手,梳理OKHttp的相关知识点,快速搭建OKHttp知识体。本文主要包括以下内容一个OKHttp请求的整体流程是怎样的?OKHttp分发器如何工作?OKHttp拦截器如何工作?应用程序拦截器和网络拦截器有什么区别?OKHttp是如何重用TCP连接的?OKHttp如何清除空闲连接?OKHttp有什么优势?OKHttp框架使用了哪些设计模式?一、OKHttp请求的整体流程介绍首先我们来看一下最简单的Http请求是如何发送的。valokHttpClient=OkHttpClient()valrequest:RequestRequest=Request.Builder().url("https://www.google.com/").build()okHttpClient.newCall(request).enqueue(object:Callback{overridefunonFailure(调用:Call,e:IOException){}overridefunonResponse(call:Call,response:Response){}})这段代码看起来比较简单,OkHttp的请求过程至少只需要联系OkHttpClient,Request,Call,Response,但是里面的framework会做很多逻辑处理。所有网络请求的大部分逻辑都集中在拦截器中,但是在进入拦截器之前,需要依赖调度器来分配请求任务。关于Distributor和Interceptor,这里简单介绍一下,后面会详细讲解Distributor:内部维护队列和线程池,完成请求部署;拦截器:五个默认的拦截器完成整个请求过程。整个网络请求流程大致如上图。通过builder模式构建OKHttpClient和Request。OKHttpClient通过newCall发起新的请求。通过dispatcher维护请求队列和线程池,完成请求分配。通过默认的五个拦截器完成请求重试和缓存处理。2、OKHttp分发器是如何工作的?分发器的主要作用是维护请求队列和线程池。比如我们有100个异步请求,一定不能同时请求,而是要分门别类地排队,分为请求列表和等待列表。请求完成后,可以从等待列表中取出等待的请求,完成所有请求。这里,同步请求是异步请求,与同步请求synchronizedvoidexecuted(RealCallcall){runningSyncCalls.add(call);}略有不同,因为同步请求不需要线程池,没有任何限制。所以分销商只是做了一个记录。后续请求可以按照加入队列的先后顺序异步进行。synchronizedvoidenqueue(AsyncCallcall){//最大请求数不能超过64个,同一个Host请求不能超过5个.execute(call);}else{readyAsyncCalls.add(call);}}当正在执行的任务不超过最大限制64个,且同一Host的请求不超过5个时,会添加到正在执行的入队并同时提交到线程池。否则先加入等待队列。每个任务完成后,会调用分发器的finished方法,取出等待队列中的任务继续执行。3、OKHttp拦截器是如何工作的?上述分发器的任务分发后,会使用拦截器启动一个系列,配置有#RealCalloverridefunexecute():Response{try{client.dispatcher.executed(this)returnetResponseWithInterceptorChain()}finally{client.dispatcher.finished(this)}}我们再来看一下RealCall的execute方法。可以看出最后返回了getResponseWithInterceptorChain,责任链的构建和处理其实就是在这个方法中。cookieJar)拦截器+=CacheInterceptor(client.cache)拦截器+=ConnectInterceptorif(!forWebSocket){拦截器+=client.networkInterceptors}拦截器+=CallServerInterceptor(forWebSocket)valchain=RealInterceptorChain(call=this,interceptorsinterceptors=interceptors,index=0)inresponse.chainproceed(originalRequest)}如上图,构建了OkHttp拦截器的责任链责任链。顾名思义,就是用处理相关交易责任的执行链。执行链上有多个节点。每个节点都有机会(条件匹配)处理请求事务。如果一个节点处理完了,可以根据实际业务需要,交由下一个节点继续处理。处理或返回后,如上图添加责任链的顺序和作用如下表所示:添加等RetryAndFollowUpInterceptor处理错误重试和重定向BridgeInterceptor应用层和网络层的桥接拦截器,主要工作是给请求添加cookies,添加固定的header,比如Host,Content-Length,Content-Type,User-Agent等,然后是保存响应结果的cookie。如果响应是用gzip压缩的,则需要解压。CacheInterceptor缓存拦截器,如果缓存命中,则不会发起网络请求。ConnectInterceptor是一个连接拦截器,内部维护一个连接池,负责连接重用、连接创建(三次握手等)、连接释放、连接上创建socket流。networkInterceptors(网络拦截器)用户自定义的拦截器,通常用于监控网络层的数据传输。CallServerInterceptor请求拦截器是在前期准备工作完成后才真正发起网络请求的。我们的网络请求就是这样递归的走责任链,最终执行到CallServerInterceptor的拦截方法。该方法会将网络响应的结果封装成一个Response对象并返回。然后沿着责任链逐级回溯,最后回到getResponseWithInterceptorChain方法的返回,如下图:4、应用拦截器和网络拦截器有什么区别?从整个职责环节来看,应用拦截器是最先执行的拦截器,即用户设置请求属性后的原始请求,网络拦截器位于ConnectInterceptor和CallServerInterceptor之间。这时候网络链接就绪,只等发送请求数据了。它们主要有以下区别1.首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦出现错误重试或者网络重定向,网络拦截器可能会执行多次,因为相当于第二次请求,但是应用拦截器只会触发一次。另外,如果在CacheInterceptor中命中了缓存,网络请求是不需要经过的,所以会出现短路网络拦截器的情况。2.其次,除了CallServerInterceptor之外的每个拦截器都应该至少调用一次realChain.proceed方法。其实proceed方法在应用拦截器层可以多次调用(局部异常重试)或者不调用(中断),但是网络拦截器层的connection已经准备好,proceed方法只能叫过一次。3、最后,从使用场景来看,应用拦截器通常用于统计客户端发起的网络请求,因为它只会被调用一次;而网络拦截器调用则意味着将发起一次网络通信,因此通常可以用来统计链路上传输的网络数据。5、OKHttp是如何重用TCP连接的?ConnectInterceptor的主要工作是建立TCP连接。建立一个TCP连接,需要经过三次握手和四次挥手。如果每个HTTP请求都需要创建一个新的TCP,会消耗更多的资源,并且已经支持Http1.1Keep-alive,即多个Http请求复用一个TCP连接,OKHttp也做了相应的优化。让我们看看OKHttp是如何复用TCP连接的。ConnectInterceptor中查找连接的代码最终会调用ExchangeFinder的.findConnection方法,具体如下:#ExchangeFinder//查找承载新数据流的连接。查找顺序为分配的连接、连接池、新建连接privateRealConnectionfindConnection(intconnectTimeout,intreadTimeout,intwriteTimeout,intpingIntervalMillis,booleanconnectionRetryEnabled)throwsIOException{synchronized(connectionPool){//1。尝试使用已分配给数据流的连接。(比如重定向请求时,可以复用上次请求的连接)releasedConnection=transmitter.connection;result=transmitter.connection;if(result==null){//2。如果没有分配的可用连接,尝试从连接池中获取。(后面会详细讲解连接池)if(connectionPool.transmitterAcquirePooledConnection(address,transmitter,null,false)){result=transmitter.connection;}}synchronized(connectionPool){if(newRouteSelection){//3.现在有IP地址了,再尝试从连接池中获取。由于加入合并可能会匹配。(这里传入routes,上面pass为null)routes=routeSelection.getAll();if(connectionPool.transmitterAcquirePooledConnection(address,transmitter,routes,false)){foundPooledConnection=true;result=transmitter.connection;}}//4。如果第二次失败,新创建的连接会进行TCP+TLS握手,与服务器建立连接。它是一个阻塞操作result.connect(connectTimeout,readTimeout,writeTimeout,pingIntervalMillis,connectionRetryEnabled,call,eventListener);synchronized(connectionPool){//5。最后一次尝试从连接池获取,注意最后一个参数为true,需要多路复用(http2.0)//意思是,如果这个是http2.0,那么为了保证多路复用,(因为上面的握手操作不是线程安全的)会重新确认此时连接池中是否有相同的连接if(connectionPool.transmitterAcquirePooledConnection(address,transmitter,routes,true)){//如果获取到,关闭我们创建的连接,并返回得到的连接result=transmitter.connection;}else{//如果没有最后一次尝试,将新创建的连接存入连接池connectionPool.put(result);}}returnresult;}部分代码已经上面简化了。可以看出,连接拦截器使用了5种方法来寻找连接:首先,它会尝试使用已经分配给请求的连接。(如果是已分配的连接,比如重定向时重新请求,则说明上次已经有连接)如果没有已分配的可用连接,则尝试从连接池中获取。因为此时没有路由信息,匹配条件:地址相同——主机、端口、代理等相同,匹配的连接可以接受新的请求。如果不是从连接池中获取,则传入routes,重新尝试获取。这主要是针对Http2.0的操作。Http2.0如果没有第二次获取到square.com和square.ca之间的连接,可以重用,创建RealConnection实例,进行TCP+TLS握手,与服务器建立连接。这时为了保证Http2.0连接的多路复用,会第三次从连接池中匹配。因为新建立的连接的握手过程不是线程安全的,所以此时连接池中可能新存了同一个连接。如果第三次匹配,则使用已有的连接,释放新创建的连接;如果不是,则将新连接存储在连接池中并返回。以上就是连接拦截器试图重用连接的操作。流程图如下:6、如何清除OKHttp空闲连接?上面说了,我们会建立一个TCP连接池,但是如果没有任务,空闲的连接也要及时清除。OKHttp是怎么做到的?#RealConnectionPoolprivatevalcleanupQueue:TaskQueue=taskRunner.newQueue()privatevalcleanupTask=object:Task("$okHttpNameConnectionPool"){overridefunrunOnce():Long=cleanup(System.nanoTime())}longcleanup(longnow){intinUseConnectionCount=0;//数量connectionsinuseintidleConnectionCount=0;//空闲连接数RealConnectionlongestIdleConnection=null;//空闲时间最长的连接longestIdleDurationNs=Long.MIN_VALUE;//空闲时间最长的连接//遍历连接:找到要清理的连接起来,找到下一次清理的时间(最大空闲时间还没到)synchronized(this){for(Iterator
