深入理解OkHttp源码及设计思想

作者: 蓝灰_q | 来源:发表于2017-12-02 00:41 被阅读392次

    用OkHttp很久了,也看了很多人写的源码分析,在这里结合自己的感悟,记录一下对OkHttp源码理解的几点心得。

    整体结构

    网络请求框架虽然都要做请求任务的封装和管理,但是最大的难点在于网络请求任务的多样性,因为网络层情况复杂,不仅要考虑功能性的建立Socket连接、文件流传输、TLS安全、多平台等,还要考虑性能上的Cache复用、Cache过期、连接池复用等,这些功能如果交错在一起,实现和维护都会有很大的问题。

    为了解决这个问题,OkHttp采用了分层设计的思想,使用多层拦截器,每个拦截器解决一个问题,多层拦截器套在一起,就像设计模式中的装饰者模式一样,可以在保证每层功能高内聚的情况下,解决多样性的问题。

    OkHttp使用了外观模式,开发者直接操作的主要就是OkHttpClient,其实如果粗略划分的话,整个OkHttp框架从功能上可以分为三部分:
    1.请求和回调:具体的类就是Call、RealCall(及其内部类AsyncCall)、Callback等。
    2.分发器及线程池:具体的类就是Dispatcher、ThreadPoolExecutor等。
    3.拦截器:实现了分层设计+链式调用,具体的类就是Interceptor+RealInterceptorChain。

    至于更具体的操作,均由拦截器实现,包括应用层拦截器、网络层拦截器等,开发者也可以自己扩展新的拦截器。

    请求

    网络请求其实可以分为数据和行为两部分,数据即我们的请求数据和返回数据,行为则是发起网络请求,以及得到处理结果。
    数据(Request和Response)
    在OkHttp中,用Request定义请求数据,用Response定义返回数据,这两个类都使用了建造者模式,把对象的创建和使用分离开,但这两个类更接近于数据模型,主要用来读写数据,不做请求动作。
    行为(Call/RealCall/AsyncCall和Callback)
    在OkHttp中,用Call和Callback定义网络请求,用Call去发起网络请求,用Callback去接收异步返回,(如果是同步请求,就直接返回Response数据)。
    其中,Call是个接口,真正的实现类是RealCall,RealCall如果需要异步处理,还会先包装为RealCall的内部类AsyncCall,然后再把AsyncCall交给线程池。

    在具体执行过程中,把数据对象交给行为对象去操作:
    在RealCall行为中调用enqueue去发起异步网络请求,此时需要传参Request数据对象;返回的Callback会传递Response数据对象。
    如果RealCall行为中调用的是execute同步网络请求,就直接返回Response数据对象。

    RealCall只是对请求做了封装,真正处理请求的是分发器Dispatcher。

    分发器及线程池

    对于网络请求RealCall来说,需要可并行、可回调、可取消,因为OkHttp统一使用Dispatcher分发器来分发所有的Call请求,分发给多个线程进行执行(所以Dispatcher也叫反向代理),所以,这几个问题就需要交给Dispatcher来处理,对于Dispatcher来说,可并行、可回调、可取消的问题可以进一步被分解为以下几个问题,并分别处理:

    1.有没有必要管理所有的请求

    不论是同步请求还是异步请求,都是耗时操作,所以是个需要观测的行为,比如请求结束需要处理,请求本身可能取消等,都需要管理起来。
    而且,不论是正在运行的,还是等待运行的,都需要管理。

    2.如何管理所有的请求

    为了管理所有的请求,Dispatcher采用了队列+生产+消费的模式。
    为同步执行提供了runningSyncCalls来管理所有的同步请求;
    为异步执行提供了runningAsyncCalls和readyAsyncCalls来管理所有的异步请求。
    其中readyAsyncCalls是在当前可用资源不足时,用于缓存请求的。

    由于这三个队列的使用场景类似于栈,偶尔需要删除功能,所以OkHttp使用了ArrayDeque双端队列来管理,ArrayDeque的设计和实现非常精妙,感兴趣的可以深入了解一下。

    3.如何确保多个队列之间能顺畅地调度

    对于多线程情况下的队列调度,其实就是数据移动和失败阻塞的这两个问题。
    对于数据移动来说,就是要考虑多线程下队列数据移动的问题。
    对于同步请求来说,只有1个队列,不存在数据移动,数据移动的场景在两个异步队列,每当有一个异步请求finish了,就需要从待处理readyAsyncCalls队列移动到runningAsyncCalls队列,这在多线程场景下并不安全,需要加锁:

        synchronized (this) {//加锁操作
          if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
          if (promoteCalls) promoteCalls();
          runningCallsCount = runningCallsCount();
          idleCallback = this.idleCallback;
        }
    

    在promoteCalls时,会把call从ready队列转移到running队列:

     private void promoteCalls() {
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        ...
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
          AsyncCall call = i.next();
    
          if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            runningAsyncCalls.add(call);//添加队列
            executorService().execute(call);//交给线程池
          }
    
          if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
        }
      }
    

    另外这个移动的操作放在finish函数里,会存在另一个问题,就是如何确保会执行这个finish函数,避免造成失败阻塞

    对于失败阻塞来说,因为网络请求失败是很常见的场景,必须能在失败时避免阻塞队列。
    OkHttp的处理是为Call对象的execute函数写try finally,在RealCall的execute函数里,在finally中调用client.dispatcher.finish(call),确保队列不阻塞。
    这其实类似AsyncTask的处理方式,AsyncTask也是使用了try finally,在finally中scheduleNext,确保队列不阻塞。

    4.如何实现多线程

    io是个耗时但是不耗CPU的操作,是典型的需要并行处理的场景。
    OkHttp不出意外地采用了线程池实现并行,这一点类似于AsyncTask,但不像AsyncTask使用了全局唯一的线程池,每个OkHttpClient都有自己的线程池。
    不过,与AsyncTask不同的是,OkHttp的同步执行不进线程池,在RealCall执行同步execute任务时,只是在Dispatcher的runningSyncCalls中记录这个call,然后直接在当前线程执行了拦截器的操作。
    至于异步执行,就是在RealCall中enqueue时调用Dispatcher的enqueue,然后调用线程池executeService().execute(call),这里面的call是RealCall的内部类AsyncCall,实现异步调用。

    5.在这个过程中,用哪些方式提升效率

    OkHttp主要针对队列和线程池做了优化:
    循环数组
    因为Dispatcher中的三个队列需要频繁出栈和入栈,所以采用了性能良好的循环数组ArrayDeque管理队列。

    阻塞队列
    因为Dispatcher自己用队列管理了排队的请求,所以Dispatcher中的线程池其实不需要缓存队列,那么这个线程池的任务其实是尽快地把元素转交给线程池中的io线程,所以采用了容量为0的阻塞队列SynchronousQueue,SynchronousQueue与普通队列不同,不是数据等线程,而是线程等数据,这样每次向SynchronousQueue里传入数据时,都会立即交给一个线程执行,这样可以提高数据得到处理的速度。

    控制线程数量
    因为线程本身也会消耗资源,所以每个线程池都需要控制线程数量,OkHttp的线程池更进一步,会针对每个Host主机的请求(避免全都卡死在某个Host上),分别控制线程数上限(5个),具体方法就是遍历所有runningAsyncCall队列中的每个Call,查询每个Call的Host,并做计数。

    拦截器原理

    在前面的步骤中,不管是同步请求还是异步请求,最终都会调用拦截器来处理网络请求。

    //RealCall源码
    Response result = getResponseWithInterceptorChain();
    

    这就是OkHttp的核心,Interceptor拦截器。
    在OkHttp中,Call、Callback和Dispatcher虽然很有用,但对于解决复杂的网络请求没有太多作用,使用了分层设计的拦截器Interceptor才是解决复杂网络请求的核心,这也是OkHttp的核心设计。

    分层设计

    我们都知道,真实情况中的网络行为其实非常复杂,纵跨软件、协议、数据包、电信号、硬件等,所以网络层的第一个基础知识就是IOS七层模型,明确了各层的功能范围,每一层各司其职,层与层依次依赖,实际上降低了开发和维护的难度与成本。

    OkHttp也采用了分层设计思想,每层Interceptor的输入都是Request,输出都是Response,所以可以一层层地加工Request,再一层层地加工Response。

    由于各个Interceptor之间不是组合关系,不能像ViewTree那样递归调用,所以需要一个链把这些拦截器全部串起来,为此,入口RealCall会执行网络请求的getResponseWithInterceptorChain函数,主要就是一层层地组织Interceptor,组成一个链,然后用chain.proceed去调用它。

      Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        interceptors.addAll(client.interceptors());//自定义应用拦截器
        interceptors.add(retryAndFollowUpInterceptor);//重试/重定向
        interceptors.add(new BridgeInterceptor(client.cookieJar()));//应用请求转网络请求
        interceptors.add(new CacheInterceptor(client.internalCache()));//缓存
        interceptors.add(new ConnectInterceptor(client));//连接
        if (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());//自定义网络拦截器
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));//服务端连接
    
        Interceptor.Chain chain = new RealInterceptorChain(//组成链
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);//从RealCall的Request开始链式处理
      }
    

    如何实现链式处理

    我们看到,链式处理的入口是RealInterceptorChain的proceed函数:

      public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
          RealConnection connection) throws IOException {
        ...
        RealInterceptorChain next = new RealInterceptorChain(//在chain中前进一步
            interceptors, streamAllocation, httpCodec, connection, index + 1, request);
        Interceptor interceptor = interceptors.get(index);
        Response response = interceptor.intercept(next);//调用拦截器
        ...
        return response;
      }
    

    而拦截器在执行过程中,会再调用chain

      @Override 
      public Response intercept(Chain chain) throws IOException {
      ...
      Response networkResponse = chain.proceed(requestBuilder.build());
      ...
    

    这样,就形成一个chain.process(intreceptor)-->interceptor.intercept(chain)-->chainprocess(intreceptor)-->interceptor.intercept(chain)的循环,这个过程中,chain不断消费,直至最后一个拦截器,最后这个拦截器一定是CallServerInterceptor,CallServerInterceptor不再调用chain.process,链式调用结束。

    拦截器的层次设计

    了解过拦截器和链式反应的基本原理,我们再来看看各拦截器的层次设计和具体实现,有很多可以借鉴的地方。
    我们先回到RealCall中,看看拦截器的层次和分类:

      Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        interceptors.addAll(client.interceptors());//自定义应用拦截器
        interceptors.add(retryAndFollowUpInterceptor);//重试/重定向
        interceptors.add(new BridgeInterceptor(client.cookieJar()));//应用请求转网络请求
        interceptors.add(new CacheInterceptor(client.internalCache()));//缓存
        interceptors.add(new ConnectInterceptor(client));//连接
        if (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());//自定义网络拦截器
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));//实现在线网络连接
    
        Interceptor.Chain chain = new RealInterceptorChain(//组成链
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);//从RealCall的Request开始链式处理
      }
    

    我们可以看到,OkHttp中拦截器的层次是这样的:
    1.自定义应用拦截器
    2.重试、重定向拦截器
    3.应用/网络桥接拦截器
    4.缓存拦截器
    5.连接拦截器
    6.自定义网络拦截器
    7.在线网络请求拦截器

    我们看到,我们开发者可以添加两种自定义Interceptor,一种是client.interceptors()应用层拦截器,一种是client.networkInterceptors()网络层拦截器
    但其实这两种都是Interceptor,为什么可以分成是应用层和网络层呢?
    因为在网络层拦截器上方,是ConnectionInterceptor连接拦截器,这个拦截器里会提供Address、ConnectionPool等资源,可以用于处理网络连接,networkInterceptors是添加在这之后的,可以参与真正的网络层数据的处理。
    接下来,我们自顶向下,依次看看每层拦截器的实现

    拦截器——自定义应用拦截器

    OkHttp在最外围允许添加自定义的应用拦截器,我们可以拦截Request和Response,分别进行加工,例如在Request时统一添加Header和Url参数:

    Request.Builder builder = chain.request().newBuilder();
    builder.addHeader("Accept-Charset", "UTF-8");
    builder.addHeader("Accept", " application/json");
    builder.addHeader("Content-type", "application/json");
    
    HttpUrl url=builder.build().url().newBuilder()
                      .addQueryParameter("mac", EquipmentUtils.getMac())
                      .build();
    Request request = builder.url(url).build();
    

    还可以拦截Response内容,打印返回数据的日志:

    long t1 = System.nanoTime();
    Request request = chain.request();
    Response response = chain.proceed(request);
    long t2 = System.nanoTime();
    
    //直接复制字节流,获取response的数据内容
    BufferedSource sr = response.body().source();
    sr.request(Long.MAX_VALUE);
    Buffer buf = sr.buffer().clone();//copy副本读取,不能读取原文
    String content = buf.readString(Charset.forName("UTF-8"));
    buf.clear();
    
    Log.i(TAG, "net layer received response of url: " + request.url().url().toString()
              + "\nresponse: " + content
              + "\nspent time: " + (t2 - t1) / 1e6d);
    

    开发者可以扩展针对请求数据和返回数据,自由开发功能。

    拦截器——重试/重定向

    虽然前面有开发者自定义的应用拦截器,但是真正准备处理网络连接,是从OkHttp自己定义的RetryAndFollowUpInterceptor开始的,因为OkHttp正是把这个拦截器作为真正的入口,创建StreamAllocation对象,在StreamAllocation对象中准备了网络连接的Address、连接池等资源,后续的拦截器,使用的都是这个StreamAllocation对象。

    StreanAllocation

    StreamAllocation是OkHttp中用来定义和传递网络资源,并建立网络连接的对象,内部包含:
    Address:规定如何连接服务器,包括DNS、协议、URL等。
    Route:存储建立连接的目标IP和端口InetSocketAddress,以及代理服务器。
    ConnectionPool:存储和复用已存在的连接,复用时根据Address查找对应的连接。
    StreamAllocation会通过findConnection创建连接,或复用已存在的连接,期间会调用RealConnection,根据设置建立TLS连接、处理握手协议等,最底层是根据当前运行的平台,直接操作Socket。
    每个Host不超过5个连接,每个连接不超过5分钟。

    重试/重定向

    网络环境本质上是不稳定的,已建立的连接可能突然不可用,或者连接可用但是服务器报错,这就需要重试/重定向功能,这也是RetryAndFollowUpInterceptor拦截器的分层功能。
    重试
    如果整个链式调用出现了RouteException或IOException,就会调用recover函数重新建立连接;
    重定向
    如果服务器返回错误码如301,要求重定向,就会调用followUpRequest函数,新建一个Request,然后重定向,再走一遍整个调用链。
    while
    intercept函数中的这些主要逻辑都在while(true)循环中,最大循环上限是20。

    拦截器——应用转网络的桥接功能

    BridgeInterceptor是个桥梁,这主要是指他会自动处理一些网络层特有的Header信息,例如Host属性,是HTTP1.1必须的,但应用层并不关心这个属性,这就是由BridgeInterceptor自动处理的。
    BridgeInterceptor中处理的Header属性包括Host、Connection的Keep-Alive、gzip透明压缩、User-Agent描述、Cookie策略等。
    当然,因为OkHttp采用了外观模式,所以很多属性需要通过client设置和获取。

    拦截器——缓存功能

    在网络请求中使用缓存是非常必要提速手段,OkHttp专门用了CacheInterceptor拦截器来处理这个功能。
    缓存的使用注意包括存储、查询和有效性检查,在OkHttp中:
    存储,使用client外观模式来设置存储Cache数据的InternalCache实现类,在走请求链获取Response时记录cache。
    查询,在存储Cache数据的InternalCache实现类中,根据Request过滤,来查找Cache。
    有效性检查,利用工具类CacheStrategy的getCandidate函数,来判断Cache数据的各项指标是否达到条件。

    拦截器——连接功能

    在RetryAndFollowUpInterceptor入口处,我们已经分析过,在OkHttp中,连接功能由StreamAlloc实现,提供Address地址、Route路由、RealConnection连接、ConnectionPool线程池复用、身份验证、协议、握手、平台、安全等功能。

    在ConnectionInterceptor这一层,其实还没有真正连接网络,它的具体功能很简单,就是准备好request请求、streamAllocation连接资源、httpCodec传输工具、connection连接,为最底层的网络连接服务。

    其中,httpCodec通过sink提供了OKio封装过的基于socket的OutputStream,通过source提供了OKio封装的基于socket的InputStream,最终就是通过这个sink提交Request,用这个source获取Response。

    拦截器——自定义网络拦截器

    主要区别

    自定义的网络层拦截器相比应用层拦截器,能直接监测到在线网络请求的数据交换过程。
    例如,Http有url重定向机制,如果Http返回码为301,就需要根据Header中Location字段的新url,重新发起一次请求,这样的话,总共会有两次请求。

    在应用层的拦截器看来,第一次请求并没有返回有效数据,它只会抓到一次请求,也就是第二次的请求。
    但是在网络层的拦截器看来,两次都是网络请求,所以它会抓到两次请求。

    用途扩展

    根据网络层拦截器的特点,我们可以扩展如下功能:
    1.模拟各种网络情况
    网络接口不只是可用不可用的问题,还存在速度波动的问题,一个稳健的App应该能hold住波动的甚至是断断续续的网络,但是这样的网络非常不好模拟,我们可以在网络拦截器层自由设定网络返回值和返回时间,辅助我们检查App在处理网络数据时的健壮性。
    2.模拟多个备用地址切换
    无论是为了灾备,还是为了节省DNS解析时间,App都会有多个备用地址,有些就是ip地址,当网络出现问题时,要自动切换到备用地址,就可以在网络层模拟出301返回,直接重定向到备用地址。
    3.模拟数据辅助开发/测试
    在开发过程中,我们可以用gradle多环境的方法,增加一个mock的productFlavor,在这个环境下添加一个mockInterceptor,把指向官网的地址重定向为指向开发测试网址,甚至直接mock返回数据,换掉在线数据,这样可以检测整个网络层的全部功能(编码、缓存、切换、报错等),把mock数据的内容和App的反馈结合的话,还可以做到针对网络数据的半自动/自动化的测试验证。

    拦截器——在线网络请求功能

    前面所有的拦截器,都是在准备或处理网络连接前后的数据,只有CallServerInterceptor这个拦截器,是真正连接在线服务的。
    它使用ConnectionInterceptor提供的HttpCodec传输工具来发出Request,获取Response,然后用ResponseBuilder生成最终的Response,再层层传递给外层的拦截器。
    HttpCodec本身是一个接口,实例是StreamAllocation利用RealConnection生产的,RealConnection根据连接池中的可用连接,利用Okio生产source和sink:

      private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
        Proxy proxy = route.proxy();
        Address address = route.address();
    
        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.socketFactory().createSocket()
            : new Socket(proxy);
    
        rawSocket.setSoTimeout(readTimeout);
        ...
          //用Okio生产
          source = Okio.buffer(Okio.source(rawSocket));
          sink = Okio.buffer(Okio.sink(rawSocket));
        ...
      }
    

    Okio的source是socket.inputStream,sink是socket.outputStream。
    所以,真正在传输数据时,就是用Okio的sink去传socket,用source去取socket,底层其实也是socket操作。

    其他特性

    以上是OkHttp的主要内容,此外,OkHttp还有一些很有意思的特性。

    1.返回数据阅后即焚

    在OkHttp中,如果要拦截ResponseBody的数据内容(比如写日志),会发现该数据读过一次就会被情况,相当于是“阅后即焚”:

      //ResponseBody源码
      public final String string() throws IOException { //底层不能自己消化异常,应该向上层抛出异常
        BufferedSource source = source();
        try {
          Charset charset = Util.bomAwareCharset(source, charset());
          return source.readString(charset);
        //不做catch,异常全部抛出给上层
        } finally { //确保原始字节数据得到处理
          Util.closeQuietly(source); //阅后即焚,这样可以迅速腾出内存空间来
        }
      }
    

    如果一定要拦截出数据内容,我们就不能直接读ResponseBody中的source,需要copy一个副本才行:

    BufferedSource sr = response.body().source();
    sr.request(Long.MAX_VALUE);
    Buffer buf = sr.buffer().clone();//copy副本读取,不能读取原文
    String content = buf.readString(Charset.forName("UTF-8"));
    buf.clear();
    

    Response也提供了专门获取ResponsBody数据的函数peekBody,实现原理也是copy“:

      //Response源码
      public ResponseBody peekBody(long byteCount) throws IOException {
        BufferedSource source = body.source();
        source.request(byteCount);
        Buffer copy = source.buffer().clone();
        ...
        return ResponseBody.create(body.contentType(), result.size(), result);
      }
    

    参考

    深入解析OkHttp3
    OkHttp3源码分析[综述]
    Okhttp-wiki 之 Interceptors 拦截器

    相关文章

      网友评论

      • 微凉一季:总结的非常好,感觉楼主真是理解透彻了ok框架,都是程序员,人家怎么能够写的这么好啊
      • 字字珠玑:总结的非常好,感觉楼主真是理解透彻了ok框架,都是程序员,人家怎么能够写的这么好啊

      本文标题:深入理解OkHttp源码及设计思想

      本文链接:https://www.haomeiwen.com/subject/gtirbxtx.html