Android--OkHttp理解系列(一)

本篇文章主要对OkHttp进行分析,主要内容如下:

  • OkHttp初识
  • OkHttp基本执行流程(Dispatcher)
  • 拦截器(Interceptor)
  • 责任链设计模式

1. OkHttp 初识

OkHttp 在开发中经常使用到,常见的用法是:

okhttpclient okhttpclient = new okhttpclient();
request request = new request.builder()
        .url("www.baidu.com")
        .get()//get 请求
        .build();
call call = okhttpclient.newcall(request);
call.enqueue(new callback() {
    @override
    public void onfailure(call call, ioexception e) {
        //nop
    }
    @override
    public void onresponse(call call, response response) throws ioexception {
        //nop
    }
});

上述代码是基于异步的网络请求,同步的网络请求不同的地方是:

try {
    response execute = call.execute();
} catch (ioexception e) {
    //nop
}

不相同的地方点在于call对象调用的方法,那么,同步调用与异步调用主要的差别是什么?
call.execute() 方法将直接进行网络请求,阻塞当前线程直到获得网络请求的响应。异步执行会将call放入一个异步执行队列中,由executorservice在后台进行执行。

2. 基本执行流程

基本执行流程主要谈的是call的执行流程,在call的执行流程中分为同步执行流程与异步执行流程。call其实是一个继承了cloneable的接口,我们调用call call = okhttpclient.newcall(request); 获取的其实是realcall这个对象,因此下文提到的call其实就是realcall。

2.1 Call的同步执行流程

通过realcall.excute()方法执行的流程为:

  1. client.dispatcher().executed(this);向client的dispatcher中注册当前call;
  2. getresponsewithinterceptorchain();执行网络请求,在经过一系列拦截器之后获取响应结果;
  3. client.dispatcher().finished(this);向client的dispatcher中注销当前call。

2.2 Call的异步执行流程

通过realcall.enqueue(callback)方法执行的流程首先是,client.dispatcher().enqueue(new asynccall(responsecallback)); 创建一个asynccall,将enqueue的callback传递过去。asynccall其实是一个runnable,当调用enqueue()方法的时候就是讲asynccall传递给dispatcher。dispatcher里面封装了一个线程池去执行这个call,executorservice().execute(call); ,这个call就是一个runnable,我们跟进这个runnable的run()方法,我们可以看到里面执行了一个execute() ;所以逻辑最终回到了asynccall的execute()方法。realcall.asynccall.execute()与同步执行的流程有些类似:

  1. response response = getresponsewithinterceptorchain(); 执行网络请求获取网络请求的结果;
  2. responsecallback 返回执行结果;
  3. client.dispatcher().finished(this); 向dispatcher中注销当前请求的call。

2.3 Dispatcher干了些啥

通过上面call的执行流程,我们可以看出其实okhttp的核心其实是dispatcher这个分发器,接下来我们详细分析okhttp中dispatcher在网络请求的时候做了些什么事情。

2.3.1 同步Dispatcher

首先看下代码:

/** used by {@code call#execute} to signal it is in-flight. */
synchronized void executed(realcall call) {
  runningsynccalls.add(call);
}

从上面的代码可以看出,大体的执行逻辑就是将传递过来的realcall添加进了一个队列中,那么这个runningsynccalls到底是什么?看下源码:

/** running synchronous calls. includes canceled calls that haven't finished yet. */
private final deque<realcall> runningsynccalls = new arraydeque<>();

在call同步执行的过程中,dispatcher仅仅将call放进了runningsynccalls这个队列,其他的什么都没有做,这个队列包含了正在执行的call,而将call注册该队列主要的作用是方便全局管理运行的call。

2.3.2 异步Dispatcher

首先我们看看代码:

synchronized void enqueue(asynccall call) {
  if (runningasynccalls.size() < maxrequests && runningcallsforhost(call) < maxrequestsperhost) {
    runningasynccalls.add(call);
    executorservice().execute(call);
  } else {
    readyasynccalls.add(call);
  }
}

在异步调度中,传递过来的asynccall是被放在线程池中进行处理的。这个线程池是什么?来看看代码:

public synchronized executorservice executorservice() {
  if (executorservice == null) {
    executorservice = new threadpoolexecutor(0, integer.max_value, 60, timeunit.seconds,
        new synchronousqueue<runnable>(), util.threadfactory("okhttp dispatcher", false));
  }
  return executorservice;
}

默认该线程池是一个不限制大小的线程池。从前面代码我们可以看到,dispatcher会限制每一个host请求的最大限制,private int maxrequestsperhost = 5; 同时也会限制同时执行的最大请求数量,private int maxrequests = 64;。在runningasynccalls队列中,保留全部满足限制条件而正在被executorservice执行的所有asynccall,而不满足限制条件的则由readyasynccalls进行保存。

在异步调度中如果当前的call不能立即入队执行的话,那么会执行readyasynccalls.add(call);这个方法不会立即执行而是需要进行等待 ,进入等待则是说明当前的情况是要么有64条线程正在并发,要么同一个地址有5 个请求,那么等待的call什么时候会被再次执行呢?
他的触发条件为:

1. Dispatcher 的setMaxRequestPerHost() 方法被调用时候 ;
2. Dispatcher 的setMaxRequests() 被调用时候;
3. 当有一条请求结束了 执行了finish()的出队操作, 这个时候会触promoteCalls()方法执行 ,从而进行调整。�

2.3.3 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 <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();
  }
}

同步或者异步dispatcher的finished()方法最后都会执行到finished()方法,在该方法中除了会从runningsyncalls队列中移除当前正在被执行的call,异步方法还会检查由于限制条件(这里的限制条件是指最大请求数与单个host最大的请求数量)而保存在readyasyncalls队列中的asynccall从而进行移除。在异步方法中关键代码:finished(runningasyncalls, call, true);第三个参数就是判断是否需要移除readyasyncalls中的call。

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

    if (runningasynccalls.size() >= maxrequests) return; // reached max capacity.
  }
}

在promotecalls()中,主要的逻辑就是如果当前线程大于maxRequest则不进行操作,如果小于maxRequest则遍历整个readyAsyncCalls,取出一个call,并把这个call放入runningAsyncCalls,然后执行execute。如果在遍历过程中runningAsyncCalls超过maxRequest则不再添加,否则一直添加。总结一下,promoteCalls()负责ready的Call到running的call的转换,在finished()方法中,所有的call,不管是realcall或者asynccall都会在执行完毕之后检测是否存在正在进行的http请求,检测的方法为:

public synchronized int runningcallscount() {
  return runningasynccalls.size() + runningsynccalls.size();
}

当判断runningcallscount为0的时候,且该idlecallback存在的时候,回调idlecallback的run()方法。那么idlecallback什么时候存在?或者说调度器什么时候处于空闲状态?继续分析源码:

/**
 * set a callback to be invoked each time the dispatcher becomes idle (when the number of running
 * calls returns to zero).
 *
 * <p>note: the time at which a {@linkplain call call} is considered idle is different depending
 * on whether it was run {@linkplain call#enqueue(callback) asynchronously} or
 * {@linkplain call#execute() synchronously}. asynchronous calls become idle after the
 * {@link callback#onresponse onresponse} or {@link callback#onfailure onfailure} callback has
 * returned. synchronous calls become idle once {@link call#execute() execute()} returns. this
 * means that if you are doing synchronous calls the network layer will not truly be idle until
 * every returned {@link response} has been closed.
 */
public synchronized void setidlecallback(@nullable runnable idlecallback) {
  this.idlecallback = idlecallback;
}

从上面方法中追溯到,调度器的空闲状态在异步方法调度时,在callback的onResponse()方法或者onfailed()方法被回调的时候,调度器处于空闲状态。同步方法中只有在excute()方法返回的时候才会处于空闲状态。

3. 拦截器

在okhttp中真正核心的除了上面提到的dispatcher还有就是拦截器interceptor。在同步请求方法或者异步请求方法中,都会执行一条很重要的指令getresponsewithinterceptorchain()该方法就是拦截器的入口方法,接下来源码分析该方法:

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, this, eventlistener, client.connecttimeoutmillis(),
      client.readtimeoutmillis(), client.writetimeoutmillis());

  return chain.proceed(originalrequest);
}

从上面源码中可以看到,在一次网络请求中其实是经历了多次拦截器的调用,首先先用一个流程图来大体的分析各个拦截的调用过程:


OkHttp拦截器.png

从上面的流程可以看出,okhttp调用了多个拦截器来对一个请求做拦截操作,那么这样的操作是怎么完成的呢?其实在每一个拦截器后面都会调用RealInterceptorChain.proceed()来进行处理,回到上面的源码,okhttp将所有的拦截器添加进入了一个集合之中,利用自增递增来调用RealInterceptorChain.proceed() 来依次处理添加进入的拦截器。

跟进下RealInterceptorChain的源码:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {

    //...异常判断省略

    // 获取下一个拦截器.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec
    , connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
  }

RealInterceptorChain 构造函数中的index与interceptor进行对应,通过interceptor.intercept(next)方法在各个拦截器里面又能通过next参数继续调用proceed()方法,完成递归调用。至于各个拦截器具体的功能,相信大家都大概清楚,这里就不多阐述了。

4. 责任链设计模式

在okHttp中拦截器部分设计到的核心就是责任链设计模式,首先回顾下什么是责任链模式:使多个对象都有机会处理 同一个请求,从而避免请求的发送者与接收者之间的耦合关系。将这些对象连接成一条链,并沿着这条链传递 请求,直到有一个对象处理它为止。
现在我们来剖析OkHttp中的调用链,回顾下在OkHttp中的调用链结构:

OkHttp拦截器责任链.png

上面的调用模式核心是通过Interceptor来进行完成的,该接口主要的作用为添加、移除、转换请求或者回应头部信息。瞟一眼该接口的源码:

public interface Interceptor {
Response intercept(Chain chain) throws IOException;

interface Chain {
  Request request();

  Response proceed(Request request) throws IOException;
  //省略
  }
}

Interceptor 中intercept(Chain chain)方法负责具体的过滤,而它子类中又调用了Chain,
该Chain其实是指RealInterceptorChain ,在RealInterceptorChain中持有了一个interceptor
的集合 ,通过递归调用该List中的拦截器, 对发起的请求进行层层处理,最后递归结束返回请求结果。

4.1 具体分析责任链模式

上面大体的说明了在OkHttp中责任链的调用,现在详细分析该责任链怎么完成调用。责任链模式其实分为完全责任链模式与不完全责任链模式。完全责任链模式是指: 当请求到达某个处理类的时候,要么处理,要么传递给下个处理类不完全责任链模式: 当请求到达时候, 处理一部分( 可以配置一些参数或者 增加请求头等),然后扔给下一个处理类进行处理。在Okhttp中,调用的责任链就是不完全的责任链模式。责任链模式的特点:

1. 抽象一个处理任务类;
2. 任务处理类持有下一个任务对象;
3. 完全责任链模式处理请求任务时类似于if-else,不完全责任链模式则对一个请求进行分步处理。

在OkHttp中,发起的请求经过拦截器的层层处理最终返回结果,它具体是怎么操作的? 回溯到Okhttp的 getResponseWithInterceptorChain()该方法在okHttp中同步或者异步最终调用的方法,也是责任链调用的起始方法。

Response getResponseWithInterceptorChain() throws IOException {
 // Build a full stack of interceptors.
 List<Interceptor> interceptors = new ArrayList<>();
 // 添加各种拦截器

 Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
     originalRequest, this, eventListener, client.connectTimeoutMillis(),
     client.readTimeoutMillis(), client.writeTimeoutMillis());
 return chain.proceed(originalRequest);
}

接下来介绍核心类RealInterceptorChain ,该类是整个责任链的调度中心,该类继承了Interceptor的内部接口Chain

interface Chain {
Request request();
Response proceed(Request request) throws IOException;
//省略...

request() 获取的是当前的请求,而proceed() 就是负责具体的转发任务, 看看RealInterceptorChain中的proceed() 方法:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
   RealConnection connection) throws IOException {
 // 省略..

 // Call the next interceptor in the chain.
 RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
     connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
     writeTimeout);
 Interceptor interceptor = interceptors.get(index);
 Response response = interceptor.intercept(next);

 //省略..

 return response;
}

该方法核心的就这么几步:

1. 根据传入进来的责任链集合和索引获取下一个责任链(next) ,即每一个拦截器会持有下一个链对象;
2. 从拦截器集合中获取当前的拦截器;
3. 拦截处理下一个责任链(该链中传入当前的请求,只是索引+1 ,那么执行的逻辑就是当前的拦截器如果处理了就直接返回,如果没有执行或者执行了部分,那么就封装一个新的请求对象,由下一个链进行处理,下一个链又对应一个拦截器,同理,如果处理了就直接返回,没有处理了话,下一个拦截器处理一部分再次扔给拦截器集合中的下一个拦截器,这样进行递归调用,直到返回结果)。

具体调用如下:


责任链调用过程.gif

整体流程可以看下图:


OkHttp责任链执行流程.png

上述图片描述的就是责任链的调度过程,关于OkHttp的后续内容,后面文章继续。

如果文章中有什么疏漏或者错误的地方,还望各位指正,你们的监督是我最大的动力,谢谢!

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

推荐阅读更多精彩内容