OkHttp原理分析<一>

前言

OkHttp,相信做开发的都熟悉它。它是现在Android使用最频繁的网络请求框架,也是由Square公司开源。Google在Android4.4以后,也将源码中的HttpURLConnection底层实现替换为OkHttp。我们熟悉的流行的Retrofit框架底层也是使用OKHttp的,所以不管怎么样了解OkHttp原理还是有必要的。

目录

  • 使用流程
  • 请求流程
    • RealCall#enqueue()
    • RealCall#execute()
  • 分发机制
    • 同步请求
    • 异步请求(异步请求流程)
    • 分发器里的线程池
  • 了解默认的五大拦截器

使用流程

来,上一般我们平时经常使用的代码。

//创建OkHttpClient请求对象
OkHttpClient okHttpClient = new OkHttpClient.Builder()
    //设置连接超时时间
    .connectTimeout(120, TimeUnit.SECONDS)
    //添加自定义OkHttp3的拦截器
    .addInterceptor(httpLoggingInterceptor)
    //添加网络缓存拦截器
    //.addNetworkInterceptor(new CacheInterceptor())
    //设置读写超时时间
    .writeTimeout(120, TimeUnit.SECONDS)
    .readTimeout(120, TimeUnit.SECONDS)
    //设置缓存位置,大小
    //.cache(new Cache(sdcache.getAbsoluteFile(), cacheSize))
    .build();

//创建Request
Request request = new Request.Builder()
    .url(url)
    .build();
//得到Call对象
Call call = okHttpClient.newCall(request);
//执行异步请求
call.enqueue(callback);

从上述代码看出,OkHttp发起一次请求时,对于使用者最少存在OkHttpClientRequestCall三个角色。

OkHttpClient、Request都是使用它们提供的Builder(建造者模式)。

  • OkHttpClient的建造者模式:当然我们还可以在OkHttpClient#Builder()里构建自定义拦截器、缓存、分发器等等,一般使用默认的就可以了,除非有特殊需求,那么就可以自定义处理业务需求。
  • Request的建造者模式:我们除了设置一些基础的属性如url,还可以根据接口的定义是否添加header信息,请求方式(GET/POST),缓存控制等等

建造者模式

将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。在实例化OKHttpClient和Request的时候,属性值的配置,根据需求,存在各种各样的配置,使用建造者模式可以让用户按需配置,建造者会帮助我们按部就班的初始化表示对象。Builder模式的目的在于减少对象创建过程中引用的多个重载构造方法、可选参数和setters过度使用导致的不必要的复杂性。

请求流程

Call则是把requst交给了OkHttpClient返回一个一切就绪的请求,call发起执行请求。Call本身是一个接口,它的实现类为:RealCall,那么执行的请求就是RealCall中进行。

 @Override public Call newCall(Request request) {
    return new RealCall(this, request);
  }

  protected RealCall(OkHttpClient client, Request originalRequest) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client);
  }

Call接口里有两个重要的请求函数,第一个是execute()函数表示同步请求,返回Response;第二个是enqueue()函数表示异步请求,传入是Callback参数,监听响应数据的回调。既然Call是接口,那么函数的调用则在它的实现类RealCall中执行。

enqueue()

我们进入RealCall#enqueue()看看里面做那些事情。

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {//做安全处理,执行过了不能再次执行
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    //调用分发器执行异步,创建一个AsyncCall对象交给Dispatcher.enqueue()函数
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

看到源码是不是好简单,首先做了安全处理,如果RealCall执行过了,不能再次执行,反之,创建一个AsyncCall任务,并且交给Dispatcher.enqueue()函数执行。执行任务的结果通过Callback来回调来监听,从而根据onFailure()或onResponse()处理自己相关业务操作。关于分发器Dispatcher放一旁,下面再讨论分析。

AsyncCall

AsyncCall就是一个Runnable的子类,使用线程启动一个Runnable时会执行run函数,在AsyncCall中被重定向到execute()函数:

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    private AsyncCall(Callback responseCallback) {
      super("OkHttp %s", redactedUrl().toString());
      this.responseCallback = responseCallback;
    }

    String host() {//获取到域名
      return originalRequest.url().host();
    }

    Request request() {
      return originalRequest;
    }

    RealCall get() {
      return RealCall.this;
    }
    //由线程池来执行
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        // 执行的核心
        Response response = getResponseWithInterceptorChain();
        //重试、重定向 是否取消
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          //请求响应失败 回调
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          //请求响应成功 回调
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          //请求响应失败 回调
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
          //执行完成
        client.dispatcher().finished(this);
      }
    }
  }

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

当线程执行请求时,调用getResponseWithInterceptorChain()来执行请求并且返回Response,根据重试/重定向拦截器,是否被取消,如果取消了调用callback.onFailure()表示响应失败,反之调用callback.onResponse()表示请求成功,当然抛出异常时,也调用callback.onFailure()表示请求响应异常,最后调用分发器finished()函数,表示任务执行完成。

execute()

我们进入RealCall#execute()看看,同步请求又做那些事情。

@Override public Response execute() throws IOException {
    synchronized (this) {//也是做安全处理
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    try {
      client.dispatcher().executed(this);//调用分发器
      //执行请求 
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

RealCall#execute()里面做的事情也是好简单的,首先做一个安全处理,如果执行过,再次执行抛出异常,反之,接着调用分发器,然后调用getResponseWithInterceptorChain()来执行请求并且返回Response,最后调用分发器finished()函数,表示任务执行完成。

不管同步还是异步请求,都执行了getResponseWithInterceptorChain(),真正执行的工作也是这在是个函数中。这个函数就是整个OkHttp的核心。这此函数先放一旁,下面再讨论分析。

分发器(Dispatcher)

请求的任务是由分发器包装请求的,直接进入Dispatcher类的源码

public final class Dispatcher {
  private int maxRequests = 64;//最大请求数(异步请求)
  private int maxRequestsPerHost = 5;//同一域名同时存在的请求数(异步)
  private Runnable idleCallback;//闲置任务
  //异步请求  线程池
  private ExecutorService executorService;
  //异步请求 等待执行的任务队列(双端队列)
  private final Deque readyAsyncCalls = new ArrayDeque<>();
  //异步请求 正在执行的任务队列(双端队列)
  private final Deque runningAsyncCalls = new ArrayDeque<>();
  //同步请求 正在执行的任务队列(双端队列)
  private final Deque runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }
  public Dispatcher() {
  }
}

分发器内部维持一些双端队列,一些限制变量和一个线程池。Dispatcher就是用来调配请求任务的,这个分发器不需要使用者调用,内部自动调用的。当然OkHttpClient在创建时,提供了创建自定义的线程池来创建一个分发器的设置。

同步请求(Dispatcher#executed())

  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

分发器的同步函数也没有做什么事情,仅仅是把任务添加到正在同步请求的队列中,对,就这么简单。

异步请求(Dispatcher#enqueue())

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

从源码上看,代码很少,如果正在执行的异步任务列表小于最大请求数,并且正在执行的Host不能超过同一域名的最大Host数,那么把任务添加到正在执行的异步请求队列中,同时交给了线程池执行。反之,直接添加到异步请求的等待队列中。

为什么设计了一个最大请求数,一个同域名的最大Host数呢?在异步请求中,正在执行的队列数不能超过最大请求数,说明最大并发数被限制了,最大并发量是64个(默认64,如果自定义自己的分发器是可以修改这个最大请求数);同一域名正在请求的访问个数也被限制了,说明同一域名正在请求的个数不能超过5个(默认是5,如果自定义分发器可以修改这个值)。这两个条件的作用应该是最少服务器的压力,如果不限制,当APP的活跃用户量达到百万级、千万级别,是不是服务器压力特别大,所以说同一个APP存在多个域名,每个域名最多同时存在5个请求,整个APP最多同时存在64个请求,这样可以更好的减少服务器压力。(最大请求数和同一域名最大请求数为5,与下面的线程池也有关系,了解到下面的线程池就会明白这两个最大值的作用了。当然为什么最大并发量是64个,同一域名最多是5个请求数,这请去问OkHttp开发人员了)

Dispatcher#finished()

每次执行完一个同步或异步请求后,分发器调配finished()函数,表示一个任务执行完毕。

  /** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }

  /** Used by {@code Call#execute} to signal completion. */
  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
  }

  private  void finished(Deque calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
        //无论是同步还是异步都从队列中移除
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();//异步执行完,则调用此函数重新调配执行
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }
    //没有正在执行的任务,执行闲置任务
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

//异步执行完才会调配此函数,重新调配
private void promoteCalls() {
    //正在执行任务的队列数 大于 最大请求数  就不调配了 直接返回
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
     //异步等待队列没有任务 也不调配了  直接返回
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    //遍历等待异步请求队列
    for (Iterator 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.
    }
  }

调用finished()函数后,无论是同步还是异步都从队列中移除,重置正在执行请求数量,重置闲置任务,如果还有正在执行任务,且也存在闲置任务,则执行闲置任务,反之什么也不做。

当然如果是一个异步请求执行完后,会执行promoteCalls()函数,重新调配执行任务,当然如果是同步就不需要重新调配了,执行完了就完了。

promoteCalls()函数里是怎么调配的:

  • 依然校验了正在执行请求队列的最大并发数作出最大并发量处理;异步请求等待队列没有任务直接返回。
  • 异步请求等待队列有等待任务,遍历队列,并且判断正在执行的同一域名最大请求个数不超过同一域名最大个数,则添加到正在执行异步请求队列中,并交给线程池执行。

通过一张图片,更加清楚认识异步请求流程:

分发器里的线程池

我们都知道Dispatcher维持着一个线程池,当异步请求时,会将请求任务交给线程池来执行。看看分发器中的默认线程池是怎么创建的。

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(
          0, //核心线程数
          Integer.MAX_VALUE, //最大线程数
          60, //非核心线程,空闲时存活时间
          TimeUnit.SECONDS,
          new SynchronousQueue(), //线程等待队列
          Util.threadFactory("OkHttp Dispatcher", false)//线程工厂
      );
    }
    return executorService;
}

如果对线程池比较熟悉的,一眼就可以看出,OkHttp的线程池创建,其实就和Executors.newCachedThreadPool()创建的线程一样。

我们分析一下线程池里面的各个参数的意义:

  • 参数一:0

    核心线程为0,表示线程池不会为我们缓存线程(没有核心线程),也就是当任务来,就马上执行任务。

  • 参数二:Integer.MAX_VALUE

    最大线程数为Integer.MAX_VALUE

  • 参数三四:60 - TimeUnit.SECONDS

    线程池中非核心线程,线程处于空闲状态,在60秒内没有执行任务就会被回收。由于核心线程数为0,所以线程池中的所有线程在60秒内没有执行任务就会被回收。

  • 参数五:new SynchronousQueue()

    SynchronousQueue是一个无容量的队列。结合Integer.MAX_VALUE参数,表示可以获得最大并发量。

  • 参数六:Util.threadFactory("OkHttp Dispatcher", false)

    通过线程工厂创建线程,给也这个线程起一个有意义的名称,如果出现并发问题,也方便查找问题原因。

关于线程池更加详细的原理与实现

在使用线程池时,核心线程数一般都设置为CPU的核心数,但在OkHttp里的线程池中核心线程为0,表示线程池不会一直为我们缓存线程,来任务就执行,因为网络请求也不需要缓存。

由于没有核心线程,线程池中所有线程都是在60s内没有工作就会被回收,释放线程资源,达到优化目的。

SynchronousQueue是一个无容量的队列,也是阻塞队列的一种。最大线程Integer.MAX_VALUE与等待队列SynchronousQueue的组合能够得到最大的并发量。即当需要线程池执行任务时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。一般来说,等待队列BlockingQueue有:ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue

假设向线程池提交任务时,核心线程都被占用的情况下:

  • ArrayBlockingQueue:基于数组的阻塞队列,初始化需要指定固定大小。

    当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入队列就会失败,这时就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。所以最终可能出现后提交的任务先执行,而先提交的任务一直在等待。

  • LinkedBlockingQueue:基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。

    当指定大小后,行为就和ArrayBlockingQueu一致。而如果未指定大小,则会使用默认的Integer.MAX_VALUE作为队列大小。这时候就会出现线程池的最大线程数参数无用,因为无论如何,向线程池提交任务加入等待队列都会成功。最终意味着所有任务都是在核心线程执行。如果核心线程一直被占,那就一直等待。

  • SynchronousQueue : 无容量的队列。

    使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合Integer.MAX_VALUE就实现了真正的无等待。

注意: 我们都知道,进程的内存是存在限制的,而每一个线程都需要分配一定的内存。所以线程并不能无限个数。那么OkHttp线程中当设置最大线程数为Integer.MAX_VALUESynchronousQueue时,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。但是OkHttp肯定也考虑到这方面,所以OkHttp设置了最大请求任务执行个数64个,有了这个限制。这样即解决了这个问题同时也能获得最大并发量。

拦截器

在OkHttp中,核心的工作就是在getResponseWithInterceptorChain()函数中进行的。这个函数利用责任链模式完成一步步的请求。

责任链模式

在责任链模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

  private Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List 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 (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));//请求服务器拦截器

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

在OkHttp中,为请求创建了一个接收者对象(Response)的责任链。责任链模式给予请求的类型,对请求的发送者和接收者进行解耦。请求会被交给责任链中的一个个拦截器处理请求。默认情况下有五大拦截器:

  1. RetryAndFollowUpInterceptor重试/重定向拦截器

    主要完成两件事情:重试与重定向。(负责判断是否需要重新发起整个请求)

  2. BridgeInterceptor桥接拦截器

    桥接拦截器就是应用程序和服务器的连接桥梁,主要工作就是将发出的请求将会经过桥接拦截器处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。(补全请求,并对响应进行额外处理。)

  1. CacheInterceptor缓存拦截器

    请求前查询缓存,判断是否存在缓存。如果存在缓存则可以不请求,直接使用缓存的响应。

  1. ConnectInterceptor连接拦截器

    为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。(即与服务器完成TCP连接。)

  1. CallServerInterceptor请求服务器拦截器

    封装请求数据与服务器进行通信,获到响应数据后,解析响应数据(如:HTTP报文)并生成Response。

整个OkHttp功能的实现,其实就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制条件。这五个拦截器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。这些拦截器使用了责任链模式进行包装,每一个拦截器负责的工作不一样,拦截每次发起请求前会做一些事情,然后交给下一个拦截器,在获得响应结果后,又会做一些事情,每个拦截都是请求前后处理各自的不同事情,依次类推,整个过程在请求向是顺序的,而响应向则是逆序。

当发起一个请求后,任务由分发器Dispatcher将请求包装,然后依次交给重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器,获得响应后解析生成Response,然后逆序回传。

拦截器的内容过多,这里就不分析了,后面会另写一篇专门分析OkHttp的大五拦截器的机制文章。

从OkHttp的使用到源码一步步分析,得出一个大体的流程,如下图:

总结

  • 要熟悉OkHttp的使用流程,和一些自定义配置,如拦截器、代理等。
  • Call调用execute()函数代表了同步请求,而调用enqueue()函数则代表异步请求。两者唯一区别在于一个会直接发起网络请求,而另一个使用OkHttp内置的线程池来进行。
  • Dispatcher分发器,同步请求:把任务直接添加到正在执行的同步任务队列中;异步请求:正在执行的异步队列数量小于最大请求数,同时正在执行的同一域名Host数小于同一域名最大请求数,则添加到运行任务队列,否则添加到等待异步队列中。
  • 理解Dispatcher分发器中的几个队列含义,任务存放那个队列,存放时机有什么。
  • 着重要理解Dispatcher分发器中的线程池,各其各个参数的意义,也可以比较一下普通的线程池参数设置有什么不同或相同之处。
  • 建造者模式,将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
  • 责任链模式,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
  • OkHttp的拦截器,会另写一章

通过学习并以文章形式输出,以便日后查阅,总结得不好,如有错误,请多多指教。

你可能感兴趣的:(OkHttp原理分析<一>)