Okhttp源码学习一(基本请求流程)

最近学习了一下okhttp的源码,发现okhttp是真滴复杂。因为okhttp是一个网络请求库,它涉及了网络请求的方方面面,比如:http协议,socket通信,计算机网络,线程的调度等等。所以我们不扣具体的实现细节,只学习其中的原理和大概实现,okhttp的内容很多,所以分篇学习记录。

okhttp的基本使用

 OkHttpClient.Builder builder=new OkHttpClient.Builder();
 OkHttpClient okHttpClient = builder.build();    //构造 OkHttpClient

     //构造get请求
    Request request = new Request.Builder()
            .get()  //Method GET
            .url("https://www.baidu.com")
            .build();    //构造请求信息

    //构造post请求
    RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
            "json");
    Request request = new Request.Builder()
            .post(body) //Method POST
            .url("https://www.baidu.com")
            .build();    //构造请求信息
  
  //发起同步请求
    try {
           Response response=okHttpClient.newCall(request).execute();    //发起同步请求
    } catch (IOException e) {
           e.printStackTrace();
    }

    //发起异步请求
    okHttpClient.newCall(request)
            .enqueue(new Callback() {    //发起异步请求
                @Override
                public void onResponse(final Call call, final Response response) throws IOException {
                    //成功拿到响应
                    int code = response.code();
                    ResponseBody body = response.body();
                    String string = body.string();
                    Log.d("hx","code:"+code+"body:"+string);
                }

                @Override
                public void onFailure(final Call call, final IOException e) {
                    e.printStackTrace();
                }
            });

通过基本的get,post,同步或异步请求示例,可以看出okhttp的请求流程:

  • 1.构造OkhttpClient
  • 2.构造请求信息Request(get或post)
  • 3.执行请求(同步或异步)

请求流程非常简单,只有3步,这是okhttp帮我们封装好的。但具体的请求原理,内部怎么实现的呢?下面进入源码看看

Okhttpclient

通过流程我们知道,要请求首先要构造一个okhttpClient对象。看一下他的构造方法:

public OkHttpClient() {
  this(new Builder());
}

OkHttpClient(Builder builder) {
  this.dispatcher = builder.dispatcher;
  this.proxy = builder.proxy;
  this.protocols = builder.protocols;
  this.connectionSpecs = builder.connectionSpecs;
  this.interceptors = Util.immutableList(builder.interceptors);
  this.networkInterceptors = Util.immutableList(builder.networkInterceptors);

  .....省略
}

okhttpClient对象的创建需要传一个Builder对象,它的无参构造函数其实还是调用带有Builder参数的构造函数。Builder是OkhttpClient的静态内部类,通过OkhttpClient的构造函数可以看到,okhttpClient对象在创建的时候需要初始化很多的网络请求配置,所以就采用了建造者模式,方便调用者使用链式调用。

看一下静态内部类Builder

public static final class Builder {
Dispatcher dispatcher;
@Nullable Proxy proxy;
List<Protocol> protocols;
List<ConnectionSpec> connectionSpecs;
final List<Interceptor> interceptors = new ArrayList<>();
final List<Interceptor> networkInterceptors = new ArrayList<>();
EventListener.Factory eventListenerFactory;
ProxySelector proxySelector;
CookieJar cookieJar;
......

public Builder() {
  dispatcher = new Dispatcher();
  protocols = DEFAULT_PROTOCOLS;
  connectionSpecs = DEFAULT_CONNECTION_SPECS;
  eventListenerFactory = EventListener.factory(EventListener.NONE);
  proxySelector = ProxySelector.getDefault();
  cookieJar = CookieJar.NO_COOKIES;
  socketFactory = SocketFactory.getDefault();
  hostnameVerifier = OkHostnameVerifier.INSTANCE;
  ....
}

可以看到,使用 OkhttpClient okhttpClient=new OkhttpCient()这种方式创建的okhttpClient对象,会使用okhttp的默认配置。包括在前面的基本使用中使用的方式:

OkHttpClient.Builder builder=new OkHttpClient.Builder();
OkHttpClient okHttpClient = builder.build();    //构造 OkHttpClient

看一下Builder类的build();

 public OkHttpClient build() {
  return new OkHttpClient(this);
}

使用默认配置的话两者都一样可以创建okHttpClient对象。但是如果想自定义okhttp请求过程中的配置的话,就要通过builder.build()的方式创建。okhttp有很多的优点,其中之一就是请求配置可以高度定制:

builder配置.png

图中列出的方法都是可以配置的属性,其实还有很多没列出来,有兴趣的自己去文档或源码吧

Request

Request代表着一个请求信息

public final class Request {
final HttpUrl url;
final String method;
final Headers headers;
final @Nullable RequestBody body;
final Object tag;

private volatile CacheControl cacheControl; // Lazily initialized.

Request(Builder builder) {
  this.url = builder.url;
  this.method = builder.method;
  this.headers = builder.headers.build();
  this.body = builder.body;
  this.tag = builder.tag != null ? builder.tag : this;
}

通过它的成员可以看到,request包含的请求信息有:请求的url,请求方法method,请求头header,请求体requestBody等。request的构造函数里面需要传一个内部类对象builder,request可以通过其内部类对象builder修改其携带的请求信息

执行请求

看一下执行请求 okHttpClient.newCall(request)的源码:

  @Override 
  public Call newCall(Request request) {
      return RealCall.newRealCall(this, request, false /* for web socket */);
  }

newCall(Request) 方法调用了 RealCall.newRealCall() 方法:

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
  RealCall call = new RealCall(client, originalRequest, forWebSocket);
  call.eventListener = client.eventListenerFactory().create(call);
  return call;
}

okHttpClient.newCall(request)最终返回了一个RealCall对象。RealCall实现了Call接口,Call接口代表着一个准备被执行的请求任务:

 public interface Call extends Cloneable {
    //返回这个call初始化时传入的原始request
    Request request();
    //同步执行请求任务(立即执行),阻塞拿到响应
    Response execute() throws IOException;
    //异步执行请求任务(通过线程池提交执行)
    void enqueue(Callback responseCallback);
    //是否已执行完毕
    boolean isExecuted();
    //是否取消
    boolean isCanceled();

    okhttp3.Call clone();

    interface Factory {
        okhttp3.Call newCall(Request request);
    }
}

可以看到前面okhttp用法示例中发起同步请求的方法:

  //发起同步请求
Response response=okHttpClient.newCall(request).execute();    //发起同步请求

以及异步请求的方法:

 okHttpClient.newCall(request) .enqueue(new Callback() {.... }  //发起异步请求

都是在Call中定义的. 拿到了Call的实例,RealCall 对象后,就可以发起请求了。首先看一下同步请求:

同步请求

 //RealCall.execute()
 @Override
 public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
  try {
    //实际调用的是dispatch.execute()
    client.dispatcher().executed(this);
    //执行请求,拿到响应结果
    Response result = getResponseWithInterceptorChain();
    if (result == null) throw new IOException("Canceled");
    return result;
  } catch (IOException e) {
    eventListener.callFailed(this, e);
    throw e;
  } finally {
    //结束请求
    client.dispatcher().finished(this);
 }
}

同步请求做了三步操作:

  • 1.调用dispatcher的execute()
  • 2.执行请求任务
  • 3.结束请求

再看一下异步请求:

异步请求

// //RealCall.enqueue()
@Override 
public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  eventListener.callStart(this);
  //实际是调用dispatcher的enqueue()方法
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

很简单,只是调用dispatcher的enqueue().所以不管是同步请求还是异步请求,都会去调用Dispatcher的相关方法。Dispatcher是负责请求任务的调度,是比较重要的一个类,大概的看一下:

public final class Dispatcher {
  //最大并发数,最多发起64个请求
  private int maxRequests = 64;
  //同一个host最多发起5个请求
  private int maxRequestsPerHost = 5;
  //空闲的任务
  private @Nullable Runnable idleCallback;
  //执行异步请求任务的线程池
  private @Nullable ExecutorService executorService;
  //等待的异步请求队列
  final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  //正在执行的异步请求队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //正在执行的同步请求队列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
      this.executorService = executorService;
  }
     public Dispatcher() {
  }

  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内部实现了懒加载的无边界限制的线程池。参数解析

  1. 0:核心线程数量,保持在线程池中的线程数量(即使已经空闲),为0代表线程空闲后不会保留,等待一段时间后停止。
  2. Integer.MAX_VALUE:表示线程池可以容纳最大线程数量
  3. TimeUnit.SECOND:当线程池中的线程数量大于核心线程时,空闲的线程就会等待60s才会被终止,如果小于,则会立刻停止。
  4. new SynchronousQueue<Runnable>():线程等待队列。同步队列,按序排队,先来先服务
  5. Util.threadFactory("OkHttp Dispatcher", false):线程工厂,直接创建一个名为OkHttp Dispatcher的非守护线程。

回到之前RealCall中的同步请求调用:client.dispatcher().executed(this):

  //Dispatcher.executed
 synchronized void executed(RealCall call) {
  //只是把请求添加到正在执行的请求队列中
  runningSyncCalls.add(call);
}

RealCall中的异步请求调用:client.dispatcher().enqueue(new AsyncCall(responseCallback)),首先是创建了一个AsyncCall对象,然后传给了enqueue()。先看一下AsyncCall:

final class AsyncCall extends NamedRunnable {
  //响应回调
  private final Callback responseCallback;

AsyncCall(Callback responseCallback) {
  super("OkHttp %s", redactedUrl());
  this.responseCallback = responseCallback;
}

String host() {
  return originalRequest.url().host();
}

Request request() {
  return originalRequest;
}

RealCall get() {
  return RealCall.this;
}

@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 {
      eventListener.callFailed(RealCall.this, e);
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    //结束请求
    client.dispatcher().finished(this);
  }
}

}
那么NamedRunnable 又是什么呢:

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override 
  public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

   protected abstract void execute();
}

可以看到NamedRunnable 只是一个实现了runnable接口的抽象类而已,run方法执行的时候会执行execute()方法,AsyncCall继承了NamedRunnable,实现了excute()方法,在excute()中会去请求网络拿到响应,然后结束请求。

继续回到前面的RealCall中的异步请求调用:client.dispatcher().enqueue(new AsyncCall(responseCallback)):

//Dispatcher.enqueue()
synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) <   maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

可以看到dispatcher().enqueue()里面的操作:

1.先判断当前正在并发执行的请求数是否小于最大数64,并且同一个host的请求数是否小于最大值5
2.如果2个条件都满足直接把当前请求添加到正在执行的请求队列,然后通过线程池提交执行请求任务
3.如果条件不满足,则添加到等待的请求队列中

在第二步,线程池执行请求了,就会去执行AsyncCall的execute()里面的代码,去执行请求,拿到响应结果,然后结束。

对比同步请求和异步请求的调用结果,可以发现他们的请求流程基本是一样的:

  1. runningAsyncCalls.add(call) 都是先把当前请求Call添加到正在运行的请求队列中
  2. Response response = getResponseWithInterceptorChain() 执行请求,拿到响应结果
  3. client.dispatcher().finished(this) 结束请求

不同的是:

  1. 同步请求是直接添加到请求队列,去请求。而异步请求不会立即请求,会先判断当前正在运行的请求是否超过最大并发数以及同一host的请求数是否超过最大值5个,如果都不超过最大值,直接添加到请求队列,去请求。超过了,则添加到等待队列等待执行。
  2. 同步请求并没有创建工作线程去执行,而异步请求是在线程池创建的异步线程中执行的,包括Callback 回调也是。所以用异步请求拿到响应后,不能直接做UI的更新操作,因为不是在主线程。

结束请求

前面总结了同步请求或异步请求的基本请求流程,在请求的三步都是结束请求,那么结束请求具体做了什么操作呢?看一下RealCall里面无论是同步请求还是异步请求,结束请求调用client.dispatcher().finished(this)的源码:

//dispatcher.finished(RealCall call)
void finished(RealCall call) {
  finished(runningSyncCalls, call, false);
}

同步或异步结束请求时都是调用这个代码,只是第三个参数不同,同步请求时false,异步请求时true.再看一下finished()

 //dispatcher. finished(Deque<T> calls, T call, boolean promoteCalls)
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!");
    //如果需要将等待请求调整为正在执行的请求,就将等待的请求添加到正在执行的请求队列,并执行
    if (promoteCalls) promoteCalls();
    //统计正在执行请求的数量
    runningCallsCount = runningCallsCount();
    idleCallback = this.idleCallback;
  }

  if (runningCallsCount == 0 && idleCallback != null) {
    idleCallback.run();
  }
}

可以看到如果是同步请求,第三个参数promoteCalls为false,不会去调用promoteCalls()方法,如果是异步请求,promoteCalls为true,会去调用promoteCalls()将等待的请求调整为执行请求:

  //dispatcher.promoteCalls
 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();
    //同一host上的请求数小于5个,就将当前的请求从等待调整为执行
    if (runningCallsForHost(call) < maxRequestsPerHost) {
      i.remove();
      runningAsyncCalls.add(call);
      executorService().execute(call);
    }

    if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
  }
}

结束请求的基本操作就是从请求队列移除当前请求,然后根据是否是异步请求来调整请求的状态。如果是异步请求,就会去查看等待队列中的请求是否能够调整为执行请求,如果可以调整,就添加到执行请求队列并提交给线程池执行。

至此,一次网络请求的流程就结束了,再最后总结一下okhttp的基本请求过程:

1.通过OkhttpClient的内部类builder创建一个OkhttpClient对象
2.创建一个Request对象,通过Request对象封来装请求信息
3.通过OkhttpClient拿到Call接口的实现类对象RealCall,再根据Requset的请求信息去调用RealCall的同步或异步请求方法来完成网络请求

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

推荐阅读更多精彩内容