okhttp3.2源码分析(一)

okhttp简单使用

  1. 创建Request
  2. 创建Call,将Request添加到Call中
  3. 使用异步enqueue,或者同步的execute方法获得结果

okhttp网络请求过程分析

Call

同步请求

@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(false);
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    client.dispatcher().finished(this);
  }
}

首先加锁置标志位,接着使用分配器的executed方法将call加入到同步队列中,然后调用getResponseWithInterceptorChain方法(稍后分析)执行http请求,最后调用finishied方法将call从同步队列中删除

异步请求

void enqueue(Callback responseCallback, boolean forWebSocket) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
}

同样先置标志位,然后将封装的一个执行体放到异步执行队列中。这里面引入了一个新的类AsyncCall,这个类继承于NamedRunnable,实现了Runnable接口。NamedRunnable可以给当前的线程设置名字,并且用模板方法将线程的执行体放到了execute方法中,所以我们分析AsyncCall只需要看execute方法

@Override protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain(forWebSocket);
    if (canceled) {
      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!
      logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}

通过getResponseWithInterceptorChain方法来执行http请求,这个方法是不是很熟悉,在同步请求中也是用的这个方法来执行http请求。紧接着判断call是否被cancel来执行不同的回调,最后使用finished方法将call从异步执行队列中移除。这里有个需要注意的地方,onResponse回调被执行的条件是本次http请求是完整的,也就是说即使服务器返回的是错误信息,依然会走onResponse回调,我们在应用层使用的时候,可以自己再封装一次。

OK,以上就是okhttp可以同时支持同步和异步请求的分析过程,而在getResponseWithInterceptorChain方法中我们将会分析okhttp的另一个重要模块:拦截器

拦截器

这是我最喜欢okhttp的地方,你可以拦截当前正在发出的请求。我们可以使用拦截器做很多事情,例如:添加log方便调试,在服务器还没有ready的情况下模拟一个网络应答等。在getResponseWithInterceptorChain方法中处理了拦截器的相关逻辑

private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
  Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
  return chain.proceed(originalRequest);
}

这里ApplicationInterceptorChain实现了Interceptor.Chain接口,然后在preceed方法中处理相应的逻辑,preceed代码如下

@Override public Response proceed(Request request) throws IOException {
  // If there's another interceptor in the chain, call that.
  if (index < client.interceptors().size()) {
    Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
    Interceptor interceptor = client.interceptors().get(index);
    Response interceptedResponse = interceptor.intercept(chain);

    if (interceptedResponse == null) {
      throw new NullPointerException("application interceptor " + interceptor
          + " returned null");
    }

    return interceptedResponse;
  }

  // No more interceptors. Do HTTP.
  return getResponse(request, forWebSocket);
}

在index在getResponseWithInterceptorChain方法中被初始化为0,当我们添加了拦截器之后index < client.interceptors().size()就会走到true的代码段,之后会从client.interceptors()中拿出一个拦截器,执行我们的拦截回调。这里也可以看到在拦截回调中是必须要有个Response返回的,否则会出现异常。如果没有自定义拦截器的话,将会调用getResponse方法执行真正的网络请求逻辑(相对于拦截器模块来说是执行了真正的网络请求,其实后面还有缓存模块)

有意思的是我们可以定义多个拦截器,这就对应了ApplicationInterceptorChain类的名称应用拦截链。只要我们在自定义的拦截器回调方法中调用chan.proceed,拦截器就会链式的调用下去。如果我们不希望okhttp执行真正的网络请求,只需要在拦截器中虚拟一个response即可。需要注意的是,如果某个拦截器内部没有调用chan.proceed方法,那么在它之后添加的拦截器都不会再被执行

getResponse方法将会把网络请求交给Engine处理

Response getResponse(Request request, boolean forWebSocket) throws IOException {
    // Copy body metadata to the appropriate request headers.
    RequestBody body = request.body();
    if (body != null) {
      Request.Builder requestBuilder = request.newBuilder();

      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }

      request = requestBuilder.build();
    }

    // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
    engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);

    int followUpCount = 0;
    while (true) {
      if (canceled) {
        engine.releaseStreamAllocation();
        throw new IOException("Canceled");
      }

      boolean releaseConnection = true;
      try {
        engine.sendRequest();
        engine.readResponse();
        releaseConnection = false;
      } catch (RequestException e) {
        // The attempt to interpret the request failed. Give up.
        throw e.getCause();
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }
        // Give up; recovery is not possible.
        throw e.getLastConnectException();
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        HttpEngine retryEngine = engine.recover(e, null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }

        // Give up; recovery is not possible.
        throw e;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          StreamAllocation streamAllocation = engine.close();
          streamAllocation.release();
        }
      }

      Response response = engine.getResponse();
      Request followUp = engine.followUpRequest();

      if (followUp == null) {
        if (!forWebSocket) {
          engine.releaseStreamAllocation();
        }
        return response;
      }

      StreamAllocation streamAllocation = engine.close();

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      if (!engine.sameConnection(followUp.url())) {
        streamAllocation.release();
        streamAllocation = null;
      }

      request = followUp;
      engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
          response);
    }
  }
}

getResponse会先根据request body的contentType来设置相应的header,Content-Length相信都比较熟悉,在http header中Transfer-Encoding: chunked表示的是内容长度不定,这里比较奇怪的是header的属性分别在不同的设置,不清楚为何不放在一起设置。接着会创建一个HttpEngine对象,设置追加发送的请求次数,在HttpEngine中处理网络请求代码如下

engine.sendRequest();
engine.readResponse();
Response response = engine.getResponse();

紧接着是处理各种异常和发送追加请求,获取发送追加请求是在HttpEnginefollowUpRequest方法中处理,在三种情况下okhttp会发送追加请求,通过MAX_FOLLOW_UPS = 20控制最大追加请求

  1. 未授权(401):调用okhttpclient授权方法重新授权
  2. 重定向(3xx)
  3. 请求超时(408):重复发送原请求

未完待续...

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

推荐阅读更多精彩内容