OkHttp流程之Dispatcher

OkHttp的基本流程

android 开发大多用过Okhttp, 在使用过程中,大多也是同步异步两种方式。一般使用方式如下(基于3.14.2版本):

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
       RequestBody body = RequestBody.create(json, JSON);
       Request request = new Request.Builder()
               .url(url)
               .post(body)
               .build();
       //同步
       try (Response response = client.newCall(request).execute()) {
           return response.body().string();
       }

       //异步
      client.newCall(request).enqueue(new Callback(){
        @Override
              public void onFailure(Call call, IOException e) {
              }

              @Override
              public void onResponse( Call call,  Response response) throws IOException {

              }
        });
   }
}

由此可见,无论同步异步其实整个请求大致可以分为三个模块, Request,Call,Response。它们就简单概括了一个网络请求的流程:封装请求Request,进行网络请求Call,返回结果Response. 所以我们在使用过程中只需要对这三个模块进行封装。

OkHttp的线程控制

  • client.newCall

虽然我们将一个网络请求过程简单的归为三个部分,但是其中的细节才是我们需要清楚的。接下来看client.newCall的内容。

/**
 * Prepares the {@code request} to be executed at some point in the future.
 */
@Override public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}

newCall 方法返回了一个RealCall这个类实现了Call的接口, 可以认为这个RealCall是Ok中执行网络请求的一个单位。在RealCall中实现了一个请求的同步异步的两个方法execute()和enqueue().

  • RealCall中的真正执行函数

我们看execute和enqueue的伪代码(暂时不用的地方已经省略)

public Response execute() throws IOException {

    try {
      //调用okhttpclient中的分发器去执行
      client.dispatcher().executed(this);
      //将结果直接返回,无论同步异步最终都会调用到getResponseWithInterceptorChain
      //这个函数可以看做是整个okhttp的核心
      return getResponseWithInterceptorChain();
    } finally {
      //执行完需要调用分发器中的结束函数
      client.dispatcher().finished(this);
    }
  }

  public void enqueue(Callback responseCallback) {

    //同样是用dispatcher进行分发,这里是将AsyncCall进行执行
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

AysncCall是RealCall的内部类,这样是为了使用RealCall中的资源,比如okhttpclient,封装的request等等。AsyncCall继承了NameRunnable接口,NameRunable为Runnable的实现,代码如下:

public final void run() {
    //更换执行时候的线程名,主要是为了调试的时候能根据name能明白线程的作用
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      //执行此类的抽象方法,NameRunable的子类需要实现这个方法
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

OkHttpClient中的Dispatcher

在上面的code中我们发现,无论是同步异步都会通过 client.dispatcher()这个对象去执行,看这个类名我们也可以猜测出这个类应该是负责线程的分发,可以说这个类算是okhttp的调度中心。既然是线程的分发,那么肯定是涉及到了线程池。我们接下来看看Dispather的庐山真面目。

 //最大并发请求数,
 private int maxRequests = 64;
 // 同一个域名最大连接数是5
 private int maxRequestsPerHost = 5;

 //空闲线程,当dispatcher空闲的时候执行,类似Handler机制中的idleHandler,开发者可视情况选择使用,本文不详述
 private @Nullable Runnable idleCallback;

 //OkHttp 线程池
 private @Nullable ExecutorService executorService;

 //准备执行的保存异步Asyncall的有一个等待队列
 private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

 //保存正在执行AsyncCall的队列
 private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

 //正在执行的同步RealCall队列
 private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

Dispatcher就是通过这些内容控制整个过程中线程的并发问题。首先来看线程池的配置

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      //核心线程为0, 最大线程数为MAX_VALUE理论上可以无限制的开辟线程,
      //空闲线程存活60s
      //SynchronousQueue为有界队列,大小为0,只是进行生产消费的传递作用
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

其实看完发现和android提供的newCachedThreadPool很类似,只是改了线程名。我们可以简单分析一下这个线程池的运行机制:

  • 线程池收到runnable因为没有核心线程,所以不需要创建核心线程。所以就直接加入任务队列。
  • 因为SynchronousQueue不保存Runnable,所有接收到线程,就会去检查如果有空闲线程,直接传递给空闲线程,没有的话就开辟一个新的线程去执行。

如果只是这样就会存在一个很大的问题,如果一直这么创建新的线程, 理论上就可以创建无数线程。 这显然不合理。其实这只是okhttp调度线程的一部分,更重要的部分由readyAsyncCalls runningAsyncCalls runningSyncCalls这部分实现。无论是同步还是异步,httpclient封装一个call传递给调度器。在上面提到的execute或者是enqueue函数我们追查下去都会最终到Dispatcher中的promoteAndExecute()方法中。我们以enqueue为例。

Dispatcher.java

void enqueue(AsyncCall call) {
    synchronized (this) {
      //将传递进来的call保存进入readyAsyncCalls队列
      readyAsyncCalls.add(call);
      //找出同域名的请求,如果存在,那么已经存在的call中的callsPerHost数将会被共享,
      //callsPerHost数目表示同一个域名下链接的call的数目
      //callsPerHost为AtomicInteger保证多线程原子操作性,并且保证线程可见为volatile类型
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }

  private boolean promoteAndExecute() {
    //创建一个新的arraylist保存可执行的call
    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        //遍历准备执行的队列,如果有数据, 需要将数据视情况去执行并将可以执行的放到正在执行队列
        AsyncCall asyncCall = i.next();
        //如果目前正在执行的call大于maxRequest默认是64,那么直接停止退出循环
        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        //如果这次请求的域名下已经满足最大连接个数(默认是5),那这个call暂时也不能执行
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

        i.remove();
        //将call中的表示域名连接数的变量自加1.
        asyncCall.callsPerHost().incrementAndGet();
        //将这个可以执行的call添加到executableCalls链表
        executableCalls.add(asyncCall);
        //同样添加到正在执行的队列
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    //遍历executableCalls列表,将每个ayncall添加到线程池去执行
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());
    }

    return isRunning;
  }

由上面我们可以看出实际最大执行并发数是64, 并不会真的无限制的增加。最大并发线程数与每个域名同时可以连接数都是可以用户自定义的。

Dispatcher.java
//设置最大并发线程数
public void setMaxRequests(int maxRequests)

//设置相同域名最大并发数
public void setMaxRequestsPerHost(int maxRequestsPerHost)

看到这里我们已经清楚了,call是怎样执行的,同时我们会有疑问,插入和移出必须是成对出现的,线程执行的时候添加到running队列了。那么什么时候移出队列呢?这就要我们继续看AsyncCall的实际执行函数了。
AysncCall的父类NamedRunnable可以当做一个Runnable,从上文我们得知,在它的run方法中都需要执行 execute()方法,而这个方法是抽象方法,需要每个子类去实现。我们看看Ayncall中发生了什么。

RealCall.AsyncCall

 protected void execute() {
      try {
        //获取执行结果
        Response response = getResponseWithInterceptorChain();
        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 {
        //执行完之后,调用dispatcher中的finish函数
        client.dispatcher().finished(this);
      }
    }
  }

  //我们进一步看看Dispatcher中的finish函数
  Dispatcher.java

  //这个类重载了多个finished方法,最终调用到这个私有函数里面
  private <T> void finished(Deque<T> calls, T call) {
  Runnable idleCallback;
  synchronized (this) {
    //将队列中的元素删除,如果没有这个元素会抛出异常
    if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
    idleCallback = this.idleCallback;
  }

  boolean isRunning = promoteAndExecute();
  //如果队列没有在执行的线程且idle线程不为空则执行。 可以看出每个线程结束完都会检查一遍是不是
  //要执行空闲线程
  if (!isRunning && idleCallback != null) {
    idleCallback.run();
  }
}

总结

以上就是okhttp大致的调度逻辑,本篇文章不涉及各种拦截器的分析。所以我们可以将请求的逻辑分为三个模块

  • 封装Request。
  • 封装RealCall提交到调度器Dispatcher去执行
  • 将结果返回
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章