Okhttp之同步和异步请求简单分析

在读这篇博客之前,如果想了解okhttp更多原理,可移步博主的okhttp分类博客。用过okhttp的应该都了解,Okhttp是支持同步和异步请求的,本篇就就对其原理做一个简单的梳理。算是加深okhttp的理解。
同步请求使用方式如下:

Response response = okhttpClient.newCall(request).execute();

异步请求使用方式如下:

Response response = okhttpClient.newCall(request).enqueue(callback);

可以发现在请求之前会通过OkhttpClient对象的newCall方法将Request对象转换成一个RealCall对象:

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

同步execute方法简单说明

先看看同步请求执行了什么,RealCall的execute方法如下:

public Response execute() throws IOException {
      //省略了部分代码
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      client.dispatcher().finished(this);
    }

1、把RealCall交给Okhttp的Dispathcer,放到一个队列里:

final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

2、调用getResponseWithInterceptorChain发起完整的网络请求流程。 对于此方法再此不会做说明,详细可参考博主的此博客
3、执行完毕后,从队列里删除该对象(这个在异步请求中再做说明)

异步请求的简单说明

其实异步请求当然也很简单,无非就是将getResponseWithInterceptorChain 放到线程中去执行而已,这其实是很有道理的废话。因为线程创建是比较好资源的,所以有了线程池,okhttp也不例外。线程池执行的单元为runanble,所以Okhttp也将异步请求封装了一个Ruannble,这个Ruannalbe就是AsyncCall.需要注意的是这个AsyncCall可不是RealCall的子类,也不是Call接口的实现类。啰嗦了这么多所以让我们看看okhttp是怎么实现异步请求的吧:

public void enqueue(Callback responseCallback) {
    //省略部分代码
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

所以在看下Dispatcher的enqueue方法:

//正在运行的异步请求
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //等待异步执行的线程
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

上面的逻辑很清晰,当请求过多的时候放入放入等待队列中,否则放在正在执行的runningAsyncCalls队列中并用executorService来执行这个AsyncCall. 执行AsyncCall主要是让线程池的某个线程执行其run方法,而AsyncCall是一个NamedRunnable,所以分析AsyncCall的execute即可:

protected void execute() {
      try {
        Response response = getResponseWithInterceptorChain();
        //当客户端主动取消请求的时候执行
        if (retryAndFollowUpInterceptor.isCanceled()) {
          //请求失败回调
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          //请求成功回调
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
         //省略部分代码
      } finally {
       //从runningAsyncCalls移除,并且从readyAsyncCalls取一个AsyncCall执行
        client.dispatcher().finished(this);
      }
    }
  }

首先如同步请求那样调用getResponseWithInterceptorChain()进行网络请求,然后返回Response对象。如果当前请求取消的话,那么就回调onFailure方法,否则就执行onResponse方法。最后执行了Dispathcer来标志当前请求的结束!先看看这个finish方法执行了神马,这个跟同步的请求还是有区别的:

void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }
///////////////////////////////
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;

    Runnable idleCallback;
    synchronized (this) {
      //删除执行完毕的Call
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      //从等待队列中获取一个AsncCall对象并执行值
      if (promoteCalls) promoteCalls();
      //获取正在运行的请求个数
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

   //当所有请求都执行完毕的时候执行这个runnable
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

finished方法总的来说做了如下几个工作:
1、从runningAsyncCalls删除已经结束的AsycCall对象。
2、执行promoteCalls()方法:

private void promoteCalls() {
    //省略部分代码
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        //从等待队删除call对象
        i.remove();
        //将AsycnCall对象放入runningAsyncCalls中
        runningAsyncCalls.add(call);
        //执行AsycnCall
        executorService().execute(call);
      }
      //省略部分代码
    }
  }

promoteCalls方法主要就是从等待队列里获取AsycCall,然后放入runningAsyncCalls,并且交给线程池里的线程执行。
3、调用runningCallsCount()获取当前正在执行的网络而请求个数,如果为0话说明此时OkhttpClient处于空闲状态,并且客户端在初始化Dispatcher对象的时候调用了setIdleCallback(@Nullable Runnable idleCallback) 方法,则直接调用者run方法。这个idleCallback可以用来告知我们OkhttpClient什么时候全部请求完毕,然后处理我们自己的业务逻辑。

//返回同步请求的个数和异步请求的个数之和
 public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

通过上面的分析,可以发现不论是同步请求还是异步请求,都会交给Dispatcher统一管理,该Dispathcer有三个队列:

  //等待执行的请求队列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  //异步执行的请求队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

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

Dispather也提供了相应的的方法来获取这些队列对象,比如我们获取正在执行的请求,可以通过Dispatcher的runningCalls方法:

//将同步请求队列和异步请求队列的数据组装起来,返回一个集合
  public synchronized List<Call> runningCalls() {
    List<Call> result = new ArrayList<>();
    result.addAll(runningSyncCalls);
    for (AsyncCall asyncCall : runningAsyncCalls) {
      //get()方法返回的是一个realCall
      result.add(asyncCall.get());
    }
    return Collections.unmodifiableList(result);
  }

通过上文我们知道AsyncCall并不是Call接口的实现类,而是RealCall的内部类,它提供了get()方法,来返回一个RealCall对象:

RealCall get() {
      return RealCall.this;
    }

其实Call接口还有一个cancel()方法用来取消当前的Request请求,那么我们就可以通过runningCalls返回的List来取消所有的请求:

List<Call> calls = runningCalls();
for(Call call : calls) {
  call.cancel();
}

其实Dispatcher本身就提供了取消所有请求的cancelAll方法:

 public synchronized void cancelAll() {
    //取消所有等待执行的异步请求
    for (AsyncCall call : readyAsyncCalls) {
      call.get().cancel();
    }

    //取消所有的同步请求
    for (AsyncCall call : runningAsyncCalls) {
      call.get().cancel();
    }

   //取消正在执行的异步请求
    for (RealCall call : runningSyncCalls) {
      call.cancel();
    }
  }

那么这个cancel对象的cancel方法都做了什么呢?进入RealCall对象查看之:

public void cancel() {
    retryAndFollowUpInterceptor.cancel();
  }

很简单就是将调用retryAndFollowUpInterceptor的cancel方法,取消拦截器链的执行。(想了解retryAndFollowUpInterceptor干什么的,点击此处

注意的是cancel方法并不能取消通过CallServerInterceptor拦截器发送的数据(除非你能顺着网线把发送的数据在拉回来),如果该拦截器已经执行的话,服务器会正常返回Resonse对象,此时对于异步请求来说就会走callback的onFailure方法而已:

 //当客户端主动取消请求的时候执行
        if (retryAndFollowUpInterceptor.isCanceled()) {
          //请求失败回调
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          //请求成功回调
          responseCallback.onResponse(RealCall.this, response);
        }

那么既然Dispatcher提供了cancelAll方法了,我们拿到runningCalls()还有什么用呢?在Okhttp的api上官方建议我们使用OkhttpClient的时候最好就实例化一个OkhttpClient对象:
OkHttp performs best when you create a single instance and reuse it for all of your HTTP calls. This is because each client holds its own connection pool and thread pools.
那么如果我们在Android应用中使用了OkhttpClient单利对象,在某Activity的onDestory方法调用了cancelAll()方法,很可能会导致如下问题:当一个页面的A Activity onDestory的时候,另一个Activity B也已经创建且Activity B会通过OkhttpClient来访问网络.有时候onDestroy的执行会比较之后,也就是说另B activity都已经启动了,A 的onDestroy才执行,那么B页面因为A页面的调用了callAll()方法就不会加载B页面所需的数据了,尴了个尬!

那么有没有解决的方法呢?,大写的有!
在我们构建Reqeust对象的时候,Reqeust.Buider对象提供了如下方法:

 public Builder tag(Object tag) {
      this.tag = tag;
      return this;
    }

可以通过tag方法为每一个Requset设置一个tag标识,此标识可以作为Request的唯一标识来用,且通过newCall(request)返回的RealCall也提供了获取Request引用的方法:

//该方法是接口call提供的方法,由RealCall来实现
public Request request() {
    return originalRequest;
  }

那么我们就可以通过为不同页面的访问请求来设置tag,在onDestroy取消掉自己的请求就可以了,代码如下:

     public void cancelByTag(Object tag) {
        Dispatcher dispatcher = client.dispatcher();
        //取消等待执行的异步请求
        for (Call call : dispatcher .queuedCalls()) {
            if (tag.equals(call.request().tag())) {
                call.cancel();
            }
        }
        //取消所有执行的异步和同步请求
        for (Call call : dispatcher .runningCalls()) {
            if (tag.equals(call.request().tag())) {
                call.cancel();
            }
        }
    }

其实这个cancelAll可以用在所有页面或者应用退出的时候调用。
到此为止,其简单的清流流程可以用如下来表示:
这里写图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭梧悠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值