OKHTTP异步和同步请求简单分析

OKHTTP异步和同步请求简单分析
OKHTTP拦截器缓存策略CacheInterceptor的简单分析
OKHTTP拦截器ConnectInterceptor的简单分析
OKHTTP拦截器CallServerInterceptor的简单分析
OKHTTP拦截器BridgeInterceptor的简单分析
OKHTTP拦截器RetryAndFollowUpInterceptor的简单分析
OKHTTP结合官网示例分析两种自定义拦截器的区别

同步请求就是执行请求的操作是阻塞式,直到 HTTP 响应返回。它对应 OKHTTP 中的 execute 方法。

异步请求就类似于非阻塞式的请求,它的执行结果一般都是通过接口回调的方式告知调用者。它对应 OKHTTP 中的 enqueue 方法。

示例代码

下面的代码演示了如何进行同步和异步请求的操作。

OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
        .url("http://www.qq.com")
        .build();
Call call = okHttpClient.newCall(request);
//1.异步请求,通过接口回调告知用户 http 的异步执行结果
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        System.out.println(e.getMessage());
    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (response.isSuccessful()) {
            System.out.println(response.body().string());
        }
    }
});
//2.同步请求
//Response response = call.execute();
//if (response.isSuccessful()) {
//    System.out.println(response.body().string());
//}

异步请求的基本原理

OKHTTP异步任务执行图解.png

Call

负责准备去执行一个 request 请求,一个 call 只能负责去执行一个请求,不能被执行两次。因为 OkHttpClient 是实现了 Call.Factory 因此它具备创建 Call 对象的功能,内部创建的就是 RealCall 对象。Call 是封装 request 的,它表示一个可以执行的请求。

Call 的实现类 RealCall

因为 Call 是接口,内部定义了同步与异步的请求,以及取消请求等操作,这些操作是由 RealCall 真正去实现的。

在 RealCall 中关键的几个属性:

  • client 就是我们在外界创建的 OkHttpClient 对象,通过这个 client 就是调用 Dispatcher 去分发请求任务。

  • executed 它是 boolean 类型,上面介绍 Call 时已经说明了,一个 Call 只能被执行一次,在内部就是通过这个属性进行判断的。

  • originalRequest Request 对象,它就是通过 okHttpClient.newCall(request) 传入的 Request 对象,这个 Request 在整个网络请求起到非常重要的作用,它会被传入到各个 Interceptor 中去。例如用户创建的 request 对象,只是简单的设置了 url ,method,requestBody 等参数,但是想要发送一个网络请求这样简单地配置还是不够的,系统提供的拦截器 BridgeInterceptor 就是负责做这件事,它会为该请求添加请求头,例如 gzip,cookie,content-length 等,简单说它会将用户创建的 request 添加一些参数从而使其更加符合向网络请求的 request 。其他拦截器的功能也是对 request 进行操作,具体看源码。

Dispatcher 相关知识点

异步任务分发器,它会内部指定线程池去执行异步任务,并在执行完毕之后提供 finish 方法结束异步请求之后从等待队列中获取下一个满足条件的异步任务去执行。

1、在 Dispatcher 有几个比较重要的属性,这几个属性会影响异步请求的执行。

  • int maxRequests = 64 会去指定并发 call 的最大个数。

  • maxRequestsPerHost = 5: 每个主机最大请求数为5 ,也就是最多 5 个call公用一个 host。这个host 就是在 RealCall 中通过 originalRequest.url().host() 去获取的,例如 www.baidu.com

  • executorService 就是执行异步任务的线程池,在内部中已经指定好了线程池,当然也可以在 Dispacther 中通过构造方法去指定一个线程池。

  • Deque<AsyncCall> readyAsyncCalls 表示在队列中已经准备好的请求。

  • Deque<AsyncCall> runningAsyncCalls 正在执行的异步请求,包括已经取消的请求(还没有执行finish操作的请求。)

  • Deque<RealCall> runningSyncCalls 正在运行的同步请求,包括已经取消的请求(还没有执行finish操作的请求。)

2、关于 Dispatcher 的功能在下面的异步和同步请求中我们再一一探索。

Call 的实现者 RealCall

它具备有异步和同步请求,还有取消请求的功能,它内部有一个 AsyncCall 内部类,在 Dispatcher 中分发的异步请求任务就是 AsyncCall 。这里分发的任务指的是异步任务,而不是同步任务。AsyncCall 就是用表示一个异步任务的,在 Dispatcher 内部有维护了两个队列来存储 AsyncCall,分别是 readyAsyncCalls 和
runningAsyncCalls 它们分别表示准备要执行的 AsyncCall 队列和正在执行的 AsycnCall 队列。当然还有一个 runningSyncCalls 这个队列,但是它适用于存放 RealCall ,也就是用于存储同步请求的任务。

  • 在 RealCall 实现异步请求 call.enqueue(new Callback())
@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    //检测该 call 是否被执行过了,如果已经执行了,那么就抛出异常
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
 //关键代码:将 AsycnCall 添加到队列中。将任务交给 Dispatcher 去执行。
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
  • 使用 Dispatcher 去分发一个异步任务

合理性的校验操作,我们在介绍 Dispacther 的相关属性时已经说明,在 OKHTTP 中正在执行的请求不能超过 64 个,并且同一个主机不能超过 5 请求,当满足这两个条件,即可将任务添加到正在执行的队列 runningAsyncCalls 中,并且通知线程池安排线程去执行这个异步任务,否则就会被添加到等待队列中 readyAsyncCalls。

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    //当正在执行的请求小于64个&&该 call 对应的主机少于5个 Call 时
    //将任务添加到 runningAsycnCalls 中,标记为正在执行的任务。
    runningAsyncCalls.add(call);
    //在线程池中执行这个任务。
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

真正的异步执行者 AsyncCall

在前面提到了 AsyncCall 表示的是一个异步任务,在使用 Dispatcher 会将 AsyncCall 交给指定的线程去执行,而 AsyncCall 是 NamedRunnable 的子类,因此它也具备 Runnble 的特性,换句话说,在线程池中执行的任务就是 AsyncCall 了。

当线程池执行这个异步任务时,那么该 Runnable 的 run 方法就会被执行,我们查阅了源码,在 run 方法内部会去调用 AsyncCall 的 execute 方法。

@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);
  }
}

总结该方法中它主要做了这 3 件事:

  • 得到 HTTP 请求的响应 Response :Response response = getResponseWithInterceptorChain();得到一个 response 响应。(这个是一个递归的调用过程,具体在其他博客中再分析具体实现。)

  • 给调用进行接口回调异步任务执行的结果:responseCallback.onResponse 或者 responseCallback.onFailure

  • 结束该请求,并且执行下一个等待的异步任务:client.dispatcher().finished(this);

对于 AsyncCall 中所做的这 3 步中,前面两步都比较好理解,下面主要看看它是如何结束一个请求的并且开启下一个异步请求的?

我们在前面介绍 Dispacther 已经了解了它的作用,这里再次强调一下,它是负责去分发一个异步任务给指定的线程池去执行,并且可以在执行完毕之后去等待队列中获取下一个请求去执行。

在 AsyncCall 中的 execute 中执行一个异步请求,注意在 finally 块内部调用了 client.dispatcher().finished(this);它的作用是通知 Dispatcher 我的任务执行完毕了,你可以将我从集合中移除了,开启下一个异步任务吧。下面就是 finish 的源码:

private <T> void finished(Deque<T> 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();
  }
}

总结该方法中它主要做了这 3 件事:

  • 从 calls 中移除该 AysyncCall 对象,而这个 calls 就是 Dispatcher 中的 runningAsyncCalls。

  • promoteCalls() 调用该方法就可以实现从等待队列中取出下一个异步任务去执行。

  • idleCallback.run(); 没有正在执行的任务时,那么就回调这个接口,该回调接口需要通过 setIdleCallback 方法传递进来。它可以通知当没有任务正在执行时,通知外界。runningCallsCount 这个是同步执行的任务数和异步执行任务数之和。

通过 promoteCalls() 去执行下一个异步任务

该方法是用于在等待队列中获取下一个异步任务去执行。在内部会还是会对 Dispatcher 内部的几个属性进行判断,例如对正在执行的请求数量是否超过了 64 个,还有遍历等待队列里的所有的 AsyncCall ,每遍历出一个 AsycnCall 都校验它的主机是否有超过 5 个正在执行的异步请求在使用了,在满足条件的情况下,就马上会线程池去执行这个任务,以此类推,任务就这样一个一个的被执行。

private void promoteCalls() {
  if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
  if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
  for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
    AsyncCall call = i.next();
    if (runningCallsForHost(call) < maxRequestsPerHost) {
      //满足下一个要执行的任务的要求。
      i.remove();
      //添加到正在请求队列中
      runningAsyncCalls.add(call);
      //由线程池去执行这个任务。
      executorService().execute(call);
    }
    //超过了 64 个请求那么就直接 return
    if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
  }
}

使用 RealCall 实现的同步请求

上面所描述的都是异步请求,现在来看看同步请求。

同步请求调用的是 execute 方法,在内部会调用 client.dispatcher().executed(this); 方法,进去看源码可知道它实际就是将 RealCall 添加到 Dispatcher 的 runningSyncCalls 中,表示当前正在执行的同步队列中。在这里使用 Dispacther 的中 execute 仅仅只是将其添加到集合中而已,没有作别的操作,而真正执行同步任务的核心代码是 getResponseWithInterceptorChain(); ,该方法负责去网络请求,并且得到一个响应,具体内部怎么实现日后再分析。在最后的 finally 代码块执行的功能跟异步任务一样,也是通过 Dispatcher 去 finish 该请求。

在 finish 中虽然同步和异步执行的方法是一样的,但是执行流程并不一样,异步任务需要通过 promoteCalls 去执行下一个异步任务,而同步请求是不需要的,这个的判断标记就 private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {...} 就是第三个参数,当该 promoteCalls 为 false 表示同步请求,true 表示异步请求,其他操作都是和异步请求是一样的。

@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);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容