Android OkHttp源码解析入门教程(一)

概述

从OkHttp问世以来,度娘,google上关于OkHttp的讲解说明数不胜数,各种解读思想不尽相同,一千个读者就有一千个哈默雷特。本篇文章从源码出发向你介绍Okhttp的基本使用以及底层实现原理,让你从会写转向会用,学习Android顶尖源码的设计理念和开源扩展性,如果解读有误,还望提出探讨纠正。

工欲善其事,必先利其器

Android API23(6.0)版本以后,Google正式移除Apache-HttpClientOkHttp 作为一个现代,快速,高效的HttpClient,其功能之强大也是显而易见的

1.支持SPDY 可以合并多个请求到同一个主机的请求、连接池、GZIP和HTTP缓存

2.支持HTTP/2协议,通过HTTP/2 可以让客户端中到服务器的所有请求共用同一个Socket连接

3.非HTTP/2 请求时, OkHttp内部会维护一个线程池,通过线程池可以对HTTP/1.x的连接进行复用,减少延迟

4.支持post,get请求,基于http的文件上传和下载

5.默认情况下,OkHttp会自动处理常见的网络问题,像二次连接、SSL的握手问题

当然OkHttp的功能远不止这些,这里只是说明平时经常用到的。既然OkHttp已经作为官方库使用,相比我们在做项目的时候也会用,但对于其底层的实现原理还是一知半解,那我们就从这篇文章开始解释其底层实现原理。开车前先来一波介绍:

Ecplise引用:下载最新的Jar

Android studio引用:implementation 'com.squareup.okhttp3:okhttp:3.11.0' //最新版本号请关注okhttp官网

Maven引用:

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.11.0</version> //最新版本号请关注okhttp官网
</dependency>

各位老司机们,马上开车,嘀嘀嘀!

一、基本使用方法

流程如下


// 启动客户端类,主要有两种方法进行创建,new对象和Builder内部类实现实例化
OkHttpClient  client = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build();

// get请求
// 通过Builder模式创建一个Request对象(即请求报文)
// 这里可以设置请求基本参数:url地址,get请求,POST请求,请求头,cookie参数等
Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .header("User-Agent", "xxx.java")
                .addHeader("token", "xxx")
                .get()
                .build();

// POST请求
// 表单形式上传
RequestBody body = new FormBody.Builder().add("xxx","xxx").build();
// JSON参数形式,File对象上传
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); 
RequestBody body = RequestBody.create(MediaType.parse("File/*"), file);

Request request = new Request.Builder()
                .post(body)
                .url(url)
                .header("User-Agent", "xxx.java")
                .addHeader("token", "xxx")
                .build();


// 创建Call对象(Http请求) ,即连接Request和Response的桥梁
// newCall方法将request封装成Call对象
Call call = client.newCall(request);
try{
// Response即响应报文信息,包含返回状态码,响应头,响应体等
    Response response = call.execute();
// 这里深入一点,Call其实是一个接口,调用Call的execute()发送同步请求其实是调用了Realcall实现类的方法,Realcall从源码可以看出示一个Runable
    System.out.println(response.body().string());
}catch(IOException e){
    e.printStackTrace();
}

看完代码你可能觉得OkHttp基本流程很繁琐,但是去掉一些扩展参数,你会发现OkHttp的使用其实很简单,无非就是
1.创建一个OkHttpClient并实例化,可设置相关参数连接时长connectTimeout等
2.创建一个Request对象并实例化,可设置网络地址url,请求方式get,post,携带参数等;
3.创建一个Call对象,通过okhttpClient的newCall()方法将Request封装成Call对象
4.创建一个Response响应,用于接收服务器返回的相关信息;
即OkHttpClient客户端通过newCall()方法接受你的Request请求并生成Response响应

二、同步和异步请求

看到这里你可能会问,为什么不继续讲些关于文件上传,文件下载,Interceptors拦截器这些内容?其实同步和异步请求的实现可以说是源码中非常重要的一环,涉及到线程池Dispatch调度反向代理等,掌握核心科技,剩下的都是开胃小菜。

1)基本使用方法

同步请求方法

OkhttpClient  client = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build();
Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .get()
                .build();
Call call = client.newCall(request);
try{
    Response response = call.execute();//调用同步请求
    System.out.println(response.body().string());
}catch(IOException e){
    e.printStackTrace();  
}

通过上面代码可以看出同步请求的基本流程:
1.创建OkHttpClient和Request对象
2.将Request封装成Call对象
3.调用Call的excute()发起同步请求

*特别注意*:当前线程发送同步请求后,就会进入阻塞状态,直到数据有响应才会停止(和异步最大的不同点)

异步请求方法

OkhttpClient  client = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build();
Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .get()
                .build();
Call call = client.newCall(request);
call.enqueue(new Callback() { //调用异步请求,CallBack用于请求结束以后来进行接口回调
        @Override
        public void onFailure(Call call, IOException e) { System.out.println("Failure");}
        @Override
        public void onResponse(Call call, Response response) throw s IOException {
           System.out.println(response.body().string());
        }
    });

通过上面代码可以看出异步请求的基本流程:
1.创建OkHttpClient和Request对象
2.将Request封装成Call对象
3.调用Call的enqueue发起异步请求

*特别注意*: onFailure和onResponse都是执行在子线程

不难看出,其实异步和同步请求的不同点在于
1、发起请求方法调用 2、是否阻塞线程

到此,我们已经熟悉了OkHttp的同步,异步请求方法的基本使用;不管同步还是异步的调用都需要先初始化OkHttpClient,创建Request ,调用OkHttpClient.newcall()封装Call对象,但其内部又是如何实现的呢?


同步请求执行流程

第一步:初始化OkHttpClient(Builder builder)

    public Builder() {
      dispatcher = new Dispatcher(); // 调度分发器(核心之一)
      protocols = DEFAULT_PROTOCOLS; // 协议
      connectionSpecs = DEFAULT_CONNECTION_SPECS; // 传输层版本和连接协议
      eventListenerFactory = EventListener.factory(EventListener.NONE); // 监听器
      proxySelector = ProxySelector.getDefault(); // 代理选择器
      cookieJar = CookieJar.NO_COOKIES; // cookie
      socketFactory = SocketFactory.getDefault(); // socket 工厂
      hostnameVerifier = OkHostnameVerifier.INSTANCE; // 主机名字
      certificatePinner = CertificatePinner.DEFAULT; // 证书链
      proxyAuthenticator = Authenticator.NONE; // 代理身份验证
      authenticator = Authenticator.NONE; // 本地省份验证
      connectionPool = new ConnectionPool(); // 连接池(核心之一)
      dns = Dns.SYSTEM;// 基础域名
      followSslRedirects = true;// 安全套接层重定向
      followRedirects = true;// 本地重定向
      retryOnConnectionFailure = true; // 连接失败重试
      connectTimeout = 10_000; // 连接超时时间
      readTimeout = 10_000;  // 读取超时时间
      writeTimeout = 10_000; // 写入超时时间
      pingInterval = 0; // 命令间隔
    }

从这里看到,其实OkHttpClient的初始化已经帮我们配置了基本参数,我们也可以根据自身业务需求进行相应的参数设置(失败重连,添加拦截器,cookie等等),一般遇到创建对象需要大量参数时,推荐使用Builider模式链式调用完成参数初始化,具体使用可以去Android源码中的AlertDialog、Notification中详细了解;
这里我们重点注意两个核心,DispatcherConnectionPool,这两点会在后面做详细讲解
Dispatcher:OkHttp请求的调度分发器,由它决定异步请求在线程池中是直接处理还是缓存等待,当然对于同步请求,只是将相应的同步请求放到请求队列当中执行
ConnectionPool: 统一管理客户端和服务器之间连接的每一个Connection,作用在于
1)当你的Connection请求的URL相同时,可以选择是否复用;2)控制Connection保持打开状态还是复用

第二步:创建 (Builder builder)

  public static class Builder {
    HttpUrl url; 
    String method;
    Headers.Builder headers;
    RequestBody body; //请求体
    Object tag; //标签

    public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder(); //Headers内部类
    }

这个构造方法很简单,在Request.Builder模式下默认指定请求方式为GET请求,创建了Headers内部类来保存头部信息,我们再来看build方法

 public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
    }

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,请求头部信息,这样就完成同步请求的前两步

第三步:调用OkHttpClient.newcall()封装Call对象

 /**
   * Prepares the {@code request} to be executed at some point in the future.
   * 准备在将来某个时候执行{@code请求}
   */
  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

上面我们也提到过,Call是一个接口,所以它的实际操作是在RealCall类中实现的

final class RealCall implements Call {
  final OkHttpClient client;
  final RetryAndFollowUpInterceptor retryAndFollowUpInterceptor; //重定向拦截器

  /**
   * There is a cycle between the {@link Call} and {@link EventListener} that makes this awkward.
   * This will be set after we create the call instance then create the event listener instance.
   */
  private EventListener eventListener;

  /** The application's original request unadulterated by redirects or auth headers. */
  final Request originalRequest;
  final boolean forWebSocket;

  // Guarded by this.
  private boolean executed;

//实际构造方法
  private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }

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

从这里就可以看到,RealCall其实是持有之前初始化好的OkHttpClient和Request对象,同时赋值了RetryAndFollowUpInterceptor重定向拦截器,关于拦截器的内容,我们会后面具体讲解OKhttp内部的5大拦截器;

第四步,调用call.exucte方法实现同步请求

 @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace(); // 捕捉异常堆栈信息
    eventListener.callStart(this); // 调用监听方法
    try {
      client.dispatcher().executed(this); // 调度器将call请求 加入到了同步执行队列中
      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);
    }
  }

首先, 加入了synchronized 同步锁,判断executed标识位是否为true,确保每个call只能被执行一次不能重复执行,然后开启了eventListener监听事件,接收相应的事件回调,通过dispatcher将Call请求添加到同步队列中

 public Dispatcher dispatcher() {
    return dispatcher;
  }

  /** Used by {@code Call#execute} to signal it is in-flight. */
  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  // 同步请求队列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

每当调用executed同步方法时,dispather就会帮我们把同步请求添加到同步请求队列中去,由此可以看出Dispather调度器的作用就是维持Call请求发送状态维护线程池并把Call请求添加到相应的执行队列当中,由它决定当前Call请求是缓存等待还是直接执行,流程如下

同步请求流程

getResponseWithInterceptorChain()是一个拦截器链,依次调用拦截器对返回的response进行相应的操作,我们在讲解到责任链模式时会详细介绍,如图


另外要特别注意下client.dispatcher().finished(this);

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

/***********************************************************************************************************************/
  public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

当同步请求完成后会调用finished()方法将队列中的请求清除掉,runningCallsCount()计算返回正在执行同步请求和正在执行异步请求的数量总和,最后判断如果runningCallsCount 为0的时候,表示整个Dispatcher分发器中没有可运行的请求,同时在满足idleCallback不为空的情况下,就调用Run方法开启闲置接口;这里可以看出,在同步请求的方法中,dispatcher的作用只是调用 executed将Call请求添加到同步队列中,执行完毕后调用 finished清除队列中的请求,可见dispatcher更多的是为异步服务

异步请求执行流程
关于OkHttpClient和Request初始化流程上文已经讲解,不清楚的可以返回去看看,所以直奔主题

第四步,调用call.enqueue方法实现异步请求

//RealCall实现类
  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true; // executed用于表示Call请求是否执行过
    }
    captureCallStackTrace();// 捕捉异常堆栈信息
    eventListener.callStart(this);// 开启监听事件
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

有没有发现和同步的excute方法很类似,都是先使用synchronized 防止请求重复执行,然后开启监听事件,最后在执行相应的方法,但奇怪的是同步在执行完excute方法后是直接通过getResponseWithInterceptorChain()返回数据,异步又是如何返回数据的呢?AsyncCall又是干什么的?

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

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

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

这里的 AsyncCall 是 RealCall 的一个内部类,它继承于NamedRunnable抽象类,NamedRunnable抽象类又实现了 Runnable,所以可以被提交到ExecutorService上执行,在execute方法里,我们看到了熟悉的流程,上文也说到getResponseWithInterceptorChain是一个拦截器链,会依次执行相应的拦截器后返回数据,所以当返回数据后,通过retryAndFollowUpInterceptor重定向拦截器判断请求是否正常执行,并且通过Callback接口返回相应数据信息,最后调用finished方法清除队列
这里有个疑问,Dispatcher是通过什么把异步就绪队列的请求调度分发到异步执行队列中的?


还记得我们讲client.dispatcher().finished(this)的时候,说到过promoteCalls方法,只是同步传参的是false没有调用,但异步传参是true,所以promoteCalls方法才真正在异步中调用

  //Disaptcher

  /** Ready async calls in the order they'll be run. */
  // 异步就绪队列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  // 异步执行队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  private void promoteCalls() {
    // maxRequests最大请求数量64
    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.
    }
  }

源码是不是很清楚明了,原来异步队列就是在这里进行调度的,在for循环中,Disaptcher首先对异步就绪队列进行遍历,如果满足runningCallsForHost(当前调用请求主机数)小于maxRequestsPerHost( 最大请求主机数5个)并且异步并发数量没有超过最大请求数量64的前提下,就把异步就绪队列中最后一个元素移除加入到异步执行队列中



我们接着看enqueue方法具体做了哪些操作

//Disaptcher
synchronized void enqueue(AsyncCall call) {
    // 异步并发请求数量不能超过最大请求数量64
    // 当前网络请求的host是否小于5个请求的host
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      // 加入执行队列 并交给线程池执行
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      // 加入就绪队列等待
      readyAsyncCalls.add(call);
    }
  }

***************************************************************************************************************
  public synchronized ExecutorService executorService() {
   // 核心线程 最大线程 非核心线程闲置60秒回收 任务队列
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

Disaptcher的enqueue方法只是做了一个异步请求的逻辑判断,即判断当前异步并发执行队列的数量是否超过最大承载运行数量64和相同host主机最多允许5条线程同时执行请求,满足以上条件,则将传进来的AsyncCall添加到异步执行队列,同时启动线程池执行,反之则添加到异步就绪队列中等待,executorService调用的就是AsyncCall的execute方法

同步和异步请求的源码就讲到这里,对过程还有不理解的可以在下方评论中提出问题,下一篇我们接着讲OkHttp的责任链模式,未完待续...

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

推荐阅读更多精彩内容

  • 最近学习了一下okhttp的源码,发现okhttp是真滴复杂。因为okhttp是一个网络请求库,它涉及了网络请求的...
    有兴不虚昧阅读 604评论 0 1
  • 前几天在在一本书上看到这样一句话 如果仅从微观的视角关注每一个单独的点,可能会因为看不到整体而迷失方向。 所以不会...
    Utte阅读 672评论 0 0
  • 关于okhttp是一款优秀的网络请求框架,关于它的源码分析文章有很多,这里分享我在学习过程中读到的感觉比较好的文章...
    蕉下孤客阅读 3,596评论 2 38
  • OkHttp源码分析 在现在的Android开发中,请求网络获取数据基本上成了我们的标配。在早期的Android开...
    BlackFlag阅读 323评论 0 5
  • OkHttp解析系列 OkHttp解析(一)从用法看清原理OkHttp解析(二)网络连接OkHttp解析(三)关于...
    Hohohong阅读 20,966评论 4 58