Android OkHttp★

本文详细介绍了Android中广泛使用的网络库OkHttp,包括其特点、使用方法、同步与异步请求、设置请求头、下载文件,以及源码分析。OkHttp支持HTTP/2、连接池、GZip压缩,提供了高效且灵活的网络请求解决方案。

1.OkHttp

OkHttp是Square公司开发的一个处理网络请求的开源项目,是目前Android使用最广泛的网络框架。

OkHttp的特点:

①支持HTTP/2并允许对同一主机的所有请求共享一个socket连接;

②如果非HTTP/2,则通过连接池减少了请求延迟;

③默认请求GZip压缩数据;

④响应缓存,避免一些完全重复的请求。

 

2.OkHttp的使用

使用时需添加依赖:
compile ‘com.squareup.okhttp3:okhttp:3.4.1’

OkHttp请求过程中最少只需要接触OkHttpClient、Request、Call、Response,但是框架内部进行了大量的逻辑处理。

①OkHttpClient客户端对象

使用OkHttp请求网络时,首先需要获取客户端对象OkHttpClient,可以直接通过new创建,也可以通过OkHttpClient静态内部类Builder创建,开发中最常用的是通过build的方式。静态内部Builder提供了很多方法,比如readTimeout读时间、writeTimeout写时间、connectTimeout连接超时时间以及retryOnConnectionFailure是否重连等。

1)通过new创建OkHttpClient

OKHttpClient client = new OkHttpClient();

2)通过builder创建OkHttpClient

private OkHttpClient mHttpClient = null;

private void initHttpClient() {
    if (null == mHttpClient) {
        mHttpClient = new OkHttpClient.Builder()
            .readTimeout(5, TimeUnit.SECONDS)
            .writeTimeout(5,TimeUnit.SECONDS)
            .connectTimeout(15,TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();
    }
}

之后就可以进行网络请求了。

②get请求

get请求分为同步请求和异步请求。

同步请求会阻塞当前线程,因此不能在UI线程中请求,需要开启子线程,在子线程中发送请求,请求成功后再跳转到主线程更新UI。

new Thread(new Runnable() {

    @Override

    public void run() {

        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()

                .url("http://www.baidu.com")

                .build();//创建Request 对象

        Response response = null;

        response = client.newCall(request ).execute();

        if (response.isSuccessful()) {

            Log.d("tag","response.code() == " + response.code());

            Log.d("tag","response.message()==" + response.message());

            Log.d("tag","res==" + response.body().string());

        //TODO 此时在子线程,更新UI的操作需使用handler跳转到主线程

            }

    }

}).start();

打印结果:

response.code()==200;

response.message()==OK;

res=={“code”:200,”message”:success};

异步请求不用再次开启子线程,但回调方法是执行在子线程中,所以在更新UI时也需要跳转到主线程中。异步请求中两个回调方法onResponse和onFailure都是在子线程中执行的,执行结果可以通过Handler来发送。

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()

        .url("http://www.baidu.com")

        .build();

client.newCall(request).enqueue(new Callback(){

    @Override

    public void onFailure(Call call, IOException e) {

    }

    @Override

    public void onResponse(Call call, Response response) throws IOException {

        if(response.isSuccessful()){//回调的方法执行在子线程

        }

    }

});

不管是同步请求还是异步请求,网络请求的步骤都是一样的,首先通过build方式创建一个请求对象Request,在Request的静态内部类Builder中设置请求地址、请求方式、请求头等。然后创建Call对象,Call对象是Request和Response之间的一个桥梁,通过Call对象的execute/enqueue方法完成同步/异步请求,获取Response的读取。

注意:
(1)Response.code是http响应行中的code,如果访问成功则返回200。这个不是服务器设置的,而是http协议中自带的。res中的code才是服务器设置的。注意二者的区别。
(2)response.body().string()本质是输入流的读操作,所以它还是网络请求的一部分,所以这行代码必须放在子线程。
(3)response.body().string()只能调用一次,在第一次时有返回值,第二次再调用时将会返回null。原因是response.body().string()的本质是输入流的读操作,必须有服务器的输出流的写操作时客户端的读操作才能得到数据。而服务器的写操作只执行一次,所以客户端的读操作也只能执行一次,第二次将返回null。

(4)异步请求与同步请求最大的不同点就是异步请求不需要开启子线程,enqueue方法会自动将网络请求部分放入子线程中执行。

总结get请求的三个步骤如下:

  1. 创建OkHttpClient和Request对象。

  2. 使用client.newCall(reaquest)方法将Request对象封装成Call对象。

  3. 调用Call的execute/enqueue方法进行请求。

③post请求

Post请求也分同步和异步两种方式。

Post请求可以传递各种格式的参数,看一下Request.Builder类的post方法的声明:

public Builder post(RequestBody body)

post方法接收的参数是RequestBody对象,所以只要是RequestBody及子类对象都可以当作参数进行传递。表单FormBody就是RequestBody的一个子类对象。

(1)使用FormBody传递键值对参数

OkHttpClient client = new OkHttpClient();

FormBody.Builder formBody = new FormBody.Builder();//创建表单请求体

formBody.add("username","zhangsan");//传递键值对参数

Request request = new Request.Builder()

        .url("http://www.baidu.com")

        .post(formBody.build())//传递请求体

        .build();

client.newCall(request).enqueue(new Callback(){

    ……

});

(2)使用RequestBody传递Json或File对象

RequestBody是抽象类,所以不能直接使用,但是它有静态方法create,使用这个方法可以得到RequestBody对象。这种方式可以上传Json对象或File对象。

上传json对象:

OkHttpClient = new OkHttpClient();

MediaType JSON = MediaType.parse( "application/json; charset=utf-8");//设置数据类型为json格式

String jsonStr = "{\"username\":\"lisi\", \"nickname\":\"李四\"}"; //json数据.

RequestBody body = RequestBody.create(JSON, josnStr);

Request request = new Request.Builder()

    .url("http://www.baidu.com")

    .post(body)

    .build();

client.newCall(request).enqueue(new Callback(){

    ……

});

上传File对象:

OkHttpClient client = new OkHttpClient();

MediaType fileType = MediaType.parse("File/*"); //数据类型为file格式,

File file = new File("path");//file对象

RequestBody body = RequestBody.create( fileType, file);

Request request = new Request.Builder()

    .url("http://www.baidu.com")

    .post(body)

    .build();

client.newCall(request).enqueue(new Callback(){

});

(3)使用MultipartBody同时传递键值对参数和File对象

FormBody传递的是字符串型的键值对,RequestBody传递的是多媒体,如果想二者都传递就需要使用MultipartBody类。

OkHttpClient client = new OkHttpClient();

MultipartBody multipartBody =new MultipartBody.Builder()

    .setType(MultipartBody.FORM)

    .addFormDataPart("groupId",""+groupId)

    .addFormDataPart("title","title")

    .addFormDataPart("file",file.getName(), RequestBody.create(MediaType.parse("file/*"), file)) //添加文件

    .build();

final Request request = new Request.Builder()

    .url("http://www.baidu.com")

    .post(multipartBody)

    .build();

client.newCall(request).enqueue(new Callback(){

});

(4)自定义RequestBody实现流的上传

只要是RequestBody类及子类都可以作为post方法的参数,因此可以自定义一个类继承RequestBody,从而实现流的上传。

首先创建一个RequestBody类的子类对象:

RequestBody body = new RequestBody() {

    @Override

    public MediaType contentType() {

        return null;

    }

    @Override

    public void writeTo(BufferedSink sink) throws IOException {

        FileInputStream fio= new FileInputStream(new File("fileName"));

        byte[] buffer = new byte[1024*8];

        if(fio.read(buffer) != -1){

            sink.write(buffer);

        }

    }

};

然后使用body对象:

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()

    .url("http://www.baidu.com")

    .post(body)

    .build();

client.newCall(request).enqueue(new Callback(){

});

body对象重写了write方法,里面有个sink对象。这个是OKio包中的输出流,有write方法。使用这个方法可以实现上传流的功能。

使用RequestBody上传文件时,并没有实现断点续传的功能。可以使用这种方法结合RandomAccessFile类实现断点续传的功能。

④设置请求头

OKHttp中设置请求头很简单,在创建request对象时调用header方法即可。

Request request = new Request.Builder()

        .url("http://www.baidu.com")

        .header("User-Agent", "OkHttp Headers.java")

        .addHeader("token", "myToken")

        .build();

⑤下载文件

在OKHttp中并没有提供下载文件的功能,但是在Response中可以获取流对象,有了流对象就可以自己实现文件的下载。

InputStream is = response.body().byteStream(); //从服务器得到输入流对象

 

3.源码分析

①同步请求

(1)首先看OkHttpClient的静态内部类Builder:

OkHttpClient.java:

public static final class Builder {

    Dispatcher dispatcher; //分发器

    ConnectionPool connectionPool; //连接池

}

OkHttpClient使用了构建者模式,它的内部类Builder中有2个比较重要的参数:

1)Dispatcher分发器:分发器内部维护了队列与线程池,完成请求调配。在同步请求中Dispatcher将请求放入队列中;在异步请求中Dispatcher会判断是直接进行处理,还是进行缓存等待。

2)ConnectionPool连接池:客户端与服务器的连接可以理解为一个connection,每个connection都会放在ConnectionPool这个连接池中,当请求的URL相同时,可以复用连接池中的connection,并且ConnectionPool实现了哪些网络连接可以保持打开状态以及哪些网络连接可以复用的相应策略的设置。

(2)创建Request请求对象时,Request也有一个内部类Builder。

Request.java:

public static class Builder {

    String method;

    Headers.Builder headers;

    public Builder() {

        this.method = "GET";

        this.headers = new Headers.Builder();

    }

    public Request build() {

       if (url == null) throw new IllegalStateException( "url == null");

        return new Request(this);

    }

}

Request也使用了构建者模式,它的内部类Builder构造方法中初始化了请求方式,默认是get请求,同时初始化了头部信息。

在build方法中先校验传入的url是否为空,再将当前配置的请求参数传给Request。

Request的构造方法:

Request(Builder builder) {

    this.url = builder.url;

    this.method = builder.method;

    this.headers = builder.headers.build();

    this.body = builder.body;

    this.tags = Util.immutableMap(builder.tags);

  }

将配置好的参数,比如请求地址url、请求方式、请求头部信息、请求体及请求标志传给Request。

(3)通过OkHttpClient的newCall方法将Request包装成Call对象。

Call call = mHttpClient.newCall(request);

进入OkHttpClient的newCall方法:

OkHttpClient.java:

@Override

public Call newCall(Request request) {

    return new RealCall(this, request);

  }

Call只是一个接口,它的所有操作都在RealCall中实现。看一下RealCall的构造方法:

RealCall.java:

private RealCall(OkHttpClient client, Request originalRequest) {

    this.client = client;

    this.originalRequest = originalRequest;

    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client);

}

在RealCall的构造方法中,设置了前两步创建的对象OkHttpClient和Request。

(4)最后通过Call的execute方法完成同步请求,execute方法的具体实现在RealCall中。

RealCall.java:

@Override

public Response execute() throws IOException {

    //第一步:判断同一Http是否请求过(同一个Http请求只能执行一次,否则抛出异常)

    synchronized (this) {

        if (executed) throw new IllegalStateException("Already Executed");

        executed = true;

    }

    try {

        //第二步:调用Dispatcher分发器的executed方法,将同步请求添加到同步队列中

        client.dispatcher().executed(this);

        //第三步:通过一系列拦截器进行操作,最终获取请求结果response

        Response result = getResponseWithInterceptorChain();

        if (result == null) throw new IOException("Canceled");

        return result;

    } finally {

        //第四步:主动回收同步请求

        client.dispatcher().finished(this);

    }

}

同步请求中第二步调用了Dispatcher的executed方法:

Dispatcher.java:

private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
}

Dispatcher的executed方法只是将RealCall对象(也就是请求)添加到runningSyncCalls同步队列中。

第三步得到请求结果response后,在第四步finally块中,主动回收同步请求,进入Dispatcher的finished方法:

void finished(RealCall call) {

    finished(runningSyncCalls, call, false); //注意:同步请求中第三个参数为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();//根据第三个参数判断是否要执行promoteCalls方法

        runningCallsCount = runningCallsCount(); //计算此时正在运行的同步请求+异步请求的总数

        idleCallback = this.idleCallback;

    }

    if (runningCallsCount == 0 && idleCallback != null) {

        idleCallback.run();

    }

}

将正在执行的同步请求RealCall对象传进finished方法,接着从当前的同步队列中移除本次同步请求,promoteCalls传入的是false,也就是该方法不会执行到(如果是异步请求,这里会传入ture,执行promoteCalls方法)。从同步队列中清除当前请求RealCall后,重新计算当前正在运行的请求总数。runningCallsCount方法的具体实现:

public synchronized int runningCallsCount() {

    return runningAsyncCalls.size() + runningSyncCalls.size();

  }

方法非常简单,就是计算正在执行的同步请求和异步请求的总和。

在finished方法最后判断runningCallsCount,如果正在执行的请求数为0并且idleaCallback不为null,就执行idleaCallback的回调方法run。

同步请求结束了,在同步请求中Dispatcher分发器做的工作很简单,就是在executed方法时将请求添加到runningSyncCalls队列(保存同步请求),并且执行完请求后将该请求从runningSyncCalls队列中移除(移除同步请求)。

②异步请求

异步请求和同步请求最大的区别就是最后一步调用的是RealCall的enqueue方法,enqueue之前都没有发起真正的网络请求,所以着重看enqueue方法。

在enqueue方法中会传入Callback对象,这个Callback对象就是用于请求结束后对结果进行回调的。

RealCall.java:

@Override

public void enqueue(Callback responseCallback){

    //判断同一Http是否请求过

    synchronized (this) {

        if (executed) throw new IllegalStateException("Already Executed");

        executed = true;

    }

    client.dispatcher().enqueue(new AsyncCall( responseCallback));

}

重点是最后一行代码,分为两步:

1)首先把传入的Callback对象封装成了AsyncCall对象,AsyncCall是RealCall的内部类。

RealCall.java:

final class AsyncCall extends NamedRunnable {

    private final Callback responseCallback;

    AsyncCall(Callback responseCallback) {

        super("OkHttp %s", redactedUrl());

        this.responseCallback = responseCallback;

    }

    ...

}

AsyncCall继承了NameRunnable:

public abstract class NamedRunnable implements Runnable {

}

NameRunnable就是一个Runnable对象,也就是说传入的Callback对象被封装成AsyncCall对象,而AsyncCall对象的本质就是一个Runnable。

2)获取Dispatcher分发器,调用Dispatcher的enqueue方法将AsyncCall对象作为参数传递进来,最终完成异步请求。

看下Dispatcher的enqueue方法:

Dispatcher.java:

private int maxRequests = 64;

private int maxRequestsPerHost = 5;

synchronized void enqueue(RealCall.AsyncCall call) {

    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {

        runningAsyncCalls.add(call);

        executorService().execute(call);

    } else {

        readyAsyncCalls.add(call);

    }

}

enqueue是一个synchronized方法。首先判断正在运行的异步请求runningAsyncCalls总数是否小于设定的最大请求数(默认是64),以及正在运行的每个主机请求数(即同一个域名的请求数)是否小于设定的主机最大请求数(默认是5),如果同时满足这两个条件就会把传递进来的AsyncCall对象添加到正在运行的异步请求队列中,并通过线程池执行这个请求。如果满足不了上面的两个条件就会将AsyncCall对象存入readyAsyncCalls队列中,readyAsyncCalls是用来存放等待请求的队列。

满足条件会将AsyncCall对象通过线程池执行,看一下线程池方法executorService():

Dispatcher.java:

private ExecutorService executorService;

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;

}

executorService()方创建了一个线程池对象,然后调用线程池的execute方法,execute方法需要传入一个Runnable对象,AsyncCall对象继承NamedRunnable,而NamedRunnable又继承了Runnable,因此AsyncCall就是一个Runnable对象,这里就会将AsyncCall对象传入。

executorService().execute(call);就是将AsyncCall对象传入到线程池中,当线程池执行该Runnable任务时会执行它的run()方法。在源码中发现AsyncCall并没有实现run方法,那这个run方法一定就是在它的父类NamedRunnable中,点击进去看下:

public abstract class NamedRunnable implements Runnable {

  @Override

    public final void run() {

          execute();

    }

  protected abstract void execute();

}

发现NamedRunnable是一个抽象类,在run方法中并没有做实际操作,只是调用了抽象方法execute,这是一个典型的模板方法模式。既然AsyncCall继承了NamedRunnable这个抽象类,那么抽象方法execute的具体实现就交由AsyncCall来实现了。

进入AsyncCall中的execute方法:

RealCall.java:

final class AsyncCall extends NamedRunnable {

    ……

    @Override

    protected void execute() {

        try {

            //通过一系列的拦截器获取Response对象

            Response response = getResponseWithInterceptorChain();

            //判断重定向和重试拦截器是否取消,如果取消,调用responseCallback的onFailure回调,responseCallback就是通过enqueue方法传入的Callback对象。如果没取消,调用responseCallback的onResponse回调

            if(retryAndFollowUpInterceptor.isCanc eled() ) {

                responseCallback.onFailure( RealCall.this, new IOException("Canceled"));

            } else {

                responseCallback.onResponse( RealCall.this, response);

            }

        } catch (IOException e) {

        } finally {

            //结束当前的异步请求

            client.dispatcher().finished(this);

        }

    }

}

由于execute方法是在run方法中执行的,所以onFailure和onResponse回调都是在子线程当中。

最后在finally块中,调用Dispatcher的finished方法:

void finished(AsyncCall call) {

    finished(runningAsyncCalls, call, true);//注意:此时异步请求,第三个参数传入的是true

}

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!");   //将当前请求从runningAsyncCalls队列中移除

        if (promoteCalls) promoteCalls(); //此时promoteCalls为true,需要执行promoteCalls方法

        runningCallsCount = runningCallsCount();

        idleCallback = this.idleCallback;

    }

    if (runningCallsCount == 0 && idleCallback != null) {

        idleCallback.run();

    }

}

finished方法与同步请求中唯一的区别就是promoteCalls参数,同步时传入的是false,但在异步请求时传入的是true,所以异步时会执行promoteCalls方法。

Dispatcher.java:

private void promoteCalls() {

    if (runningAsyncCalls.size() >= maxRequests) return; 

    if (readyAsyncCalls.isEmpty()) return; 

    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; 

    }

}

promoteCalls方法就是在当前请求执行完成后,将等待就绪的异步队列readyAsyncCalls中的一个请求添加到正在请求的异步请求队列runningAsyncCalls中,并通过线程池来执行该异步请求。

 

4.Dispatcher任务调度器(分发器)

OkHttp所有的逻辑大部分集中在拦截器中,但是在进入拦截器之前需要依靠分发器来调配请求任务。

OkHttp发送的同步或异步请求队列的状态会在Dispatcher中进行管理,Dispatcher的作用就是完成请求调配,它内部维护了三个请求队列和一个线程池。

①三个队列

private final Deque<RealCall.AsyncCall> runningAsyncCalls = new ArrayDeque<>(); //正在执行的异步请求队列

private final Deque<RealCall.AsyncCall> readyAsyncCalls = new ArrayDeque<>(); //就绪状态的异步请求队列,如果当前的请求不满足某种条件时,当前的异步请求会进入就绪等待的异步请求队列中,当满足条件时就会从就绪等待的异步请求队列中取出异步请求,放入正在执行的异步请求队列中

private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); //正在执行的同步请求队列

OkHttp的异步请求在dispatcher中的一系列操作可以理解为生产者消费者模式,其中Dispatcher是生产者,它是运行在主线程中的,ExecutorService代表消费者池。

同步请求和异步请求开始时,会调用Dispatcher的execute或enqueue方法,将当前请求加入到相应的队列中;当请求结束后,会调用dispatcher的finished方法,将当前的请求从队列中移除。

注意:client.dispatcher().finished(this);这段代码是在finally块中,也就是说,无论请求是否成功,或者出现异常,这段代码都会执行,保证了请求的整个生命周期从开始到销毁。

②一个线程池

ExecutorService executorService; //线程池对象,OkHttp的异步请求操作会放入这个线程池中

线程池的创建如下:

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;

}

创建线程池时,核心线程数为0,当线程池空闲一段时间后,就会将线程池中的所有线程进行销毁;最大线程数为整型的最大值,具体多少线程数还是受限OkHttp中的maxRequests这个参数;第三个参数表示当线程数大于核心线程数时,多余的空闲线程存活的最大时间为60秒,结合第一个核心线程数为0,也就是说OkHttp中的线程在工作完毕后,会在60秒之内进行关闭。

注意:该线程池里的队列是SynchronousQueue,这是一个无容量的队列,又因为线程池的最大容量是整型的最大值,也就是每送来一个请求就会开辟一个线程来处理。至于OkHttp的线程池为什么要用无容量的队列,可以结合runningAsyncCalls与readyAsyncCalls来理解,在进入线程池之前请求已经在runningAsyncCalls或readyAsyncCalls这两个队列里排队等待过了,所以进入线程池之后就无需再次等待了。

 

5.拦截器

拦截器是OkHttp提供的一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能。

OkHttp中不管是同步还是异步,都是通过拦截器完成网络的获取。拦截器采用的是责任链设计模式。OkHttp内部以拦截器链的形式执行http请求,共提供了5种拦截器:

①RetryAndFollowUpInterceptor重试和失败重定向拦截器:在交给下一个拦截器之前,负责判断用户是否取消了请求;在获得了结果以后,会根据响应码判断是否需要重定向,如果满足条件就会重启执行所有拦截器。

②BridgeInterceptor桥接和适配拦截器:在交给下一个拦截器之前,负责将HTTP协议必备的请求头加入其中(如Host),并添加一些默认的行为(如GZip压缩);在获得了结果后,调用保存cookie接口并解析GZip数据。

③CacheInterceptor缓存拦截器:在交给下一个拦截器之前读取并判断是否使用缓存;获取结果后判断是否缓存。

④ConnectInterceptor连接拦截器:交给下一个拦截器之前负责找到或者新建一个可用的连接,并获得对应的socket流;获得结果后不进行额外的处理。

⑤CallServerInterceptor请求服务拦截器:进行真正与服务器的通信,向服务器发送数据,解析读取的响应数据。

在OkHttp中,无论是同步请求还是异步请求,最终都是通过getResponseWithInterceptorChain()方法执行网络请求并获取网络请求的响应结果。

看一下getResponseWithInterceptorChain()方法:

RealCall.java:

private Response getResponseWithInterceptorChain() throws IOException {

    List<Interceptor> interceptors = new ArrayList<>();

    interceptors.addAll(client.interceptors()); //用户自定义的拦截器   

   //添加OkHttp提供的五个拦截器以及networkInterceptors

    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( retryAndFollowUpInterceptor.isForWebSocket()));

    //将拦截器添加到RealInterceptorChain对象

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest);

    //执行chain.proceed方法

    Response response = chain.proceed( originalRequest);

    return response;

}

首先创建一个拦截器集合,其中包括自定义的拦截器以及OkHttp提供的5种拦截器。可以发现,处理拦截器的时候会先执行自定义的拦截器再执行内部的拦截器。

然后使用拦截器集合创建了一个RealInterceptorChain对象,这个就是责任链,将拦截器集合都放到这个链条上组成了一个拦截器责任链。注意第五个参数index为0,表示定位到第一个拦截器。然后执行RealInterceptorChain对象的proceed方法。

RealInterceptorChain实现了Interceptor接口中的Chain接口:

RealInterceptorChain.java:

public final class RealInterceptorChain implements Interceptor.Chain {

    public RealInterceptorChain(List<Interceptor interceptors, Transmitter transmitter, Exchange exchange, int index, Request request, Call call, int connectTimeout, int readTimeout, int writeTimeout) {

        this.interceptors = interceptors;

        ……

    }

    @Override

    public Response proceed(Request request) {

        return proceed(request, streamAllocation, httpStream, connection);

      }

    public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream, Connection connection) {

        RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpStream, connection, index + 1, request); //下一个拦截器

        Interceptor interceptor = interceptors.get( index); //取出当前位置index的拦截器

        Response response = interceptor.intercept( next); //执行当前拦截器的intercept方法,并把下一个拦截器传给它

        return response;

    }

}

proceed方法的核心就是继续创建下一个拦截器链。从代码中可以看到,proceed()方法里又调用了RealInterceptorChain类构造方法,而这里不同的是,参数index的值为index+1,并且在该类中index为全局变量,所以index的值增量为1。然后通过index将拦截器集合interceptors中的第index个拦截器取出,并执行它的intercept()方法。

在第index个拦截器的intercept()方法中执行完自己负责的责任后,会调用传进来的拦截器链(index+1)的proceed方法,(index+1)拦截器的proceed方法会执行(index+1)拦截器的intercept方法,同时又创建下一个拦截器链(index+2),(index+1)拦截器的intercept方法执行完自己负责的责任后会调用传进来的拦截器链(index+2)的proceed方法……就这样拦截器链一层一层的调用,所有的拦截器链构成了一个完整的链条。而且在每个拦截器的intercept方法中会通过proceed方法获取下一个拦截器的Response结果并可以对结果进行处理。

这里的递归思想就是责任链模式的核心思想,即不断执行拦截器中的intercept方法,在intercept方法中实现需要逻辑,可以通过Chain获取到Request或者Response,实现对请求体或请求头的处理。

总结一下:Intercept方法的工作可以分为两部分,第一部分是在调用proceed()方法前,这部分的任务主要是向服务器发送request前的准备。第二部分是在调用proceed()方法后,对proceed()方法返回的response(也就是从下面的拦截器传递上来的服务器返回的response)进行处理。

简略流程:

75982e11f411453fbc890e1a78b67792.png

 具体看看这5个拦截器的作用:

①RetryAndFollowUpInterceptor重试重定向拦截器

主要是负责失败重连。OkHttp中的重定向功能是默认开启的。

RetryAndFollowUpInterceptor.java:

@Override

public Response intercept(Chain chain) {

    Request request = chain.request();

    RealInterceptorChain realChain = (RealInterceptorChain)chain;

    Transmitter transmitter= realChain.transmitter;

    while (true) {

        transmitter.prepareToConnect(request); //处理自己负责的任务

        Response response = realChain.proceed( request, transmitter, null); //传给下一个处理者

        Request followUp = followUpRequest( response); //对返回的Response进行检查,对Response返回的状态码判断,并创建重定向需要发出的Request对象

        if(followUp == null) { //不需要重定向

            return response;

        }

        //重定向次数上限为20,这样做是为了防止无限制的重试网络请求,造成资源的浪费

        if (++followUpCount > MAX_FOLLOW_UPS) {

            throw new ProtocolException("Too many follow-up requests: " + followUpCount);

        }

        request = followUp;//重定向后的request

    }

}

RetryAndFollowupInterceptor拦截器先处理自己负责的任务,即判断用户是否取消了请求需要重新连接,然后把请求传递到下一个拦截器,获得了下一个拦截器返回的结果后,根据响应码判断是否需要重定向,如果满足条件就会重启执行所有拦截器,对Response进行处理,返回给上一个拦截器。

②BridgeInterceptor桥接和适配拦截器

用来设置内容长度、编码方式以及压缩等一系列操作,主要是添加头部的作用。

BridgeInterceptor.java:

@Override

public Response intercept(Chain chain) {

    Request userRequest = chain.request();

    Request.Builder requestBuilder = userRequest.newBuilder();

    //处理自己负责的任务,即添加网络请求必需的头部信息

    requestBuilder.header("Content-Type", body.contentType().toString());

    requestBuilder.header("Content-Length", Long.toString(body.contentLength()));

    requestBuilder.header("Host", HostHeader(userRequest.url(),false));

    requestBuilder.header("Connection", "Keep-Alive"); 

    if(userRequest.header("Accept-Encoding" == null) {

        requestBuilder.header("Accept-Encoding", "gzip");

    }

    requestBuilder.header("Cookie", cookieHeader("cookies");

    requestBuilder.header("User-Agent", Version.userAgent);

    Response networkResponse = chain.proceed( requestBuilder.build()); //将请求传给下一个处理者

     // 将服务器返回的Response转化为客户端可以识别的Response

    Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);

    if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) {

        //将返回的结果使用GZip解析

        GzipSource responseBody = new GzipSource(networkResponse.body().source();

        ……

    }

    return responseBuilder.build();

}

BridgeInterceptor首先处理自己负责的任务,即将HTTP协议必备的请求头加入其中(如Host),并添加一些默认的行为(如GZip压缩),然后将请求传递到下一个处理者,在获得了结果后,调用保存cookie接口并解析GZip数据。

主要是给Request添加一些头部信息,防止调用者没有添加网络请求必需的某些头部信息而无法进行网络访问。如果调用者没有在header里设置Connection,则这里会自动将Connection设置为Keep-Alive,Keep-Alive的作用是当开启一个HTTP连接后,在一定时间内保持它的连接状态。

然后通过chain.proceed调用了拦截器链的proceed方法向服务器发起请求。得到response响应后,将服务器返回的Response转化为客户端可以识别的Response,如果HTTP默认支持gzip,那么BridgeInterceptor拦截器将会对这个Response进行解压,最终得到客户端使用的Response。

对transparentGzip标志位进行判断,如果transparentGzip为true就表明Accept-Encoding支持gzip压缩;再判断头部的Content-Encoding是否为gzip,保证服务端返回的响应体内容是经过了gzip压缩的;最后判断HTTP头部是否有Body。当满足这些条件后会将Response的body转换为GzipSource类型,这样client端直接以解压的方式读取数据。

③CacheInterceptor缓存拦截器

在OkHttp中使用缓存的方法:

mHttpClient = new OkHttpClient.Builder()

                .cache(new Cache(new File("cache"), 30*1024*1024)) //使用缓存策略

                .build();

cache方法中传入一个Cache对象,Cache是OkHttp内部提供的:

public Cache(File directory, long maxSize) {

    this(directory, maxSize, FileSystem.SYSTEM);

  }

创建Cache对象时需要传入两个参数,第一个参数directory代表缓存目录,第二个参数maxSize代表缓存大小。

Cache类中有一个很重要的接口InternalCache,通过InternalCache接口实现了缓存的存取等一系列操作,具体看看缓存的get和put操作。

先看InternalCache的put方法,也就是存储缓存。InternalCache的put方法调用Cache的put方法:

private final DiskLruCache cache;

CacheRequest put(Response response) {

    String requestMethod = response.request().method(); //获取请求方法

    if (HttpMethod.invalidatesCache( response.request().method())) {  //判断缓存是否有效

       remove(response.request());

        return null;

    }

    if (!requestMethod.equals("GET")) {

        return null;  //非GET请求不使用缓存

    }

    Entry entry = new Entry(response);//创建缓存体类

    DiskLruCache.Editor editor = cache.edit( urlToKey(response.request().url())); //使用DiskLruCache缓存策略

    if (editor == null) {

        return null;

    }

    entry.writeTo(editor);

    return new CacheRequestImpl(editor);

}

首先获取请求方式,根据请求方式判断缓存是否有效,通过HttpMethod的invalidatesCache方法:

public static boolean invalidatesCache(String method) {

    return method.equals("POST")

        || method.equals("PATCH")

        || method.equals("PUT")

        || method.equals("DELETE")

        || method.equals("MOVE"); // WebDAV

  }

如果请求方式是POST、PATCH、PUT、DELETE以及MOVE中一个,则缓存无效,将当前请求的缓存移除。

然后判断如果当前请求不是GET请求,就不会进行缓存。

接着根据response创建Entry对象,Entry对象在内部会保存请求url、头部、请求方式、协议、响应码等一系列参数。

OkHttp的缓存策略使用的是DiskLruCache,DiskLruCache是用于磁盘缓存的一套解决框架,OkHttp对DiskLruCache稍微做了点修改,并且OkHttp内部维护着清理内存的线程池,通过这个线程池完成缓存的自动清理和管理工作。

拿到DiskLruCache的Editor对象后,通过它的edit方法创建缓存文件,edit方法传入的是缓存的文件名,通过urlToKey方法将请求url进行MD5加密并获取它的十六进制表示形式。

接着执行Entry对象的writeTo方法并传入Editor对象,writeTo方法的目的是将缓存信息存储在本地。该方法内部对Response的相关信息进行缓存,并判断是否是https请求并缓存Https相关信息(包括url、method和header,但是返回的响应主体body并没有在这里进行缓存)。

最后返回一个CacheRequestImpl对象。在CacheRequestImpl类中有一个body对象,这个就是响应主体。CacheRequestImpl实现了CacheRequest接口,用于暴露给缓存拦截器,这样缓存拦截器就可以直接通过这个类来更新或写入缓存数据。

接下来看get方法,InternalCache的get方法调用了Cache的get方法:

Response get(Request request) {

    String key = urlToKey(request.url());//获取缓存的key

    DiskLruCache.Snapshot snapshot;//创建快照

    Cache.Entry entry;

    snapshot = cache.get(key); //根据key从缓存中获取快照

    entry = new Cache.Entry(snapshot.getSource( ENTRY_METADATA)); //从快照中获取缓存

    Response response = entry.response( snapshot);

    return response;

}

get方法首先根据请求的url获取缓存key,创建snapshot目标缓存中的快照,根据key获取快照,当目标缓存中没有这个key对应的快照,说明没有缓存返回null;如果目标缓存中有这个key对应的快照,那么根据快照创建缓存Entry对象,再从Entry中取出Response。

Entry的response方法:

public Response response( DiskLruCache.Snapshot snapshot) {

    String contentType = responseHeaders.get( "Content-Type");

    String contentLength = responseHeaders.get( "Content-Length");

    //根据头部信息创建缓存请求

    Request cacheRequest = new Request.Builder()

                .url(url)

                .method(requestMethod, null)

                .headers(varyHeaders)

                .build();

    //创建Response

    return new Response.Builder()

                .request(cacheRequest)

                .protocol(protocol)

                .code(code)

                .message(message)

                .headers(responseHeaders)

                .body(new Cache.CacheResponseBody( snapshot, contentType, contentLength))

                .handshake(handshake)

                .sentRequestAtMillis(sentRequestMillis)

                .receivedResponseAtMillis( receivedResponseMillis)

                .build();

}

Entry的response方法中会根据头部信息创建缓存请求,然后创建Response对象并返回。

get方法最后会判断响应和请求是否成对出现,如果不是成对出现,关闭流并返回null,否则返回Response。

缓存的get和put方法的整体流程清楚了,接下来看看缓存拦截器。进入CacheInterceptor的intercept方法:

CacheInterceptor.java:

@Override

public Response intercept(Interceptor.Chain chain) throws IOException {

    //第一步,先尝试从缓存中获取Response,这里分两种情况,要么获取缓存Response,要么cacheCandidate为null

    Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;

    //第二步,获取缓存策略CacheStrategy对象,CacheStrategy内部维护了一个Request和一个Response,也就是说CacheStrategy能指定到底是通过网络还是缓存,或两者同时使用获取Response

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

    Request networkRequest = strategy.networkRequest;

    Response cacheResponse = strategy.cacheResponse;

     //第三步,判断当前如果不能使用网络同时又没有找到缓存,这时会创建一个Response对象,code为504的错误

    if (networkRequest == null && cacheResponse == null) {

      return new Response.Builder()

          .request(chain.request())

          .code(504)

          .message("Unsatisfiable Request (only-if-cached)")

          .body(Util.EMPTY_RESPONSE)

          .build();

    }

    //第四步,如果当前不能使用网络,就直接返回缓存结果

    if (networkRequest == null) {

      return cacheResponse.newBuilder()

          .cacheResponse( stripBody(cacheResponse))

          .build();

    }

    //第五步,调用下一个拦截器进行网络请求

    Response networkResponse = null;

    networkResponse = chain.proceed( networkRequest);

    //第六步,当通过下一个拦截器获取Response后,判断当前如果有缓存Response,并且网络返回的Response的响应码为304,代表从缓存中获取

    if (cacheResponse != null) {

        if (networkResponse.code() == HTTP_NOT_MODIFIED) {

            Response response = cacheResponse.newBuilder()

                .headers( combine(cacheResponse.headers(),networkResponse.headers()))

                .cacheResponse( stripBody(cacheResponse))

                .networkResponse( stripBody(networkResponse))

                .build();

            networkResponse.body().close();

            cache.trackConditionalCacheHit();

            cache.update(cacheResponse, response);

            return response;

        } else {

            closeQuietly(cacheResponse.body());

        }

    }

    //第七步,如果有缓存,则判断http头部有没有响应体,并且缓存策略可以被缓存的,满足这两个条件后,网络获取的Response通过cache的put方法写入到缓存中,这样下次取的时候就可以从缓存中获取;接下来判断请求方法是否是无效的请求方法,如果是的话,从缓存池中删除这个Request。最后返回Response给上一个拦截器

    if (cache != null) {

      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {

        CacheRequest cacheRequest = cache.put(response);

        return cacheWritingResponse( cacheRequest, response);

      }

      if (HttpMethod.invalidatesCache( networkRequest.method())) {

          cache.remove(networkRequest);

      }

    }

    return response; 

}

④ConnectInterceptor网络连接拦截器

用于打开与服务器之间的连接,正式开启OkHttp的网络请求。

Connect interceptor.java:

@Override

public Response intercept(Interceptor.Chain chain) {

    RealInterceptorChain realChain = (RealInterceptorChain) chain;

    Request request = realChain.request();

    Transmitter transmitter = realChain.transmitter();

    boolean doExtensiveHealthChecks = !request.method.equals("GET");

    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    //继续调用拦截器链的下一个拦截器

    return realChain.proceed(request, transmitter, exchange);

}

ConnectInterceptor拦截器中有一个很重要的概念-连接池。不管HTTP协议是1.1还是2.0,它们的Keep-Alive机制,或者2.0的多路复用机制在实现上都需要引入一个连接池的概念,来维护整个网络连接。OkHttp中将客户端与服务端之间的链接抽象成一个Connection类,而RealConnection是它的实现类,为了管理所有的Connection,OkHttp提供了一个ConnectionPool类,它的主要作用就是在时间范围内复用Connection。

⑤CallServerInterceptor拦截器

CallServerInterceptor拦截器主要作用是负责向服务器发起真正的网络请求,并获取返回结果。

CallServerInterceptor的intercept方法:

CallServerInterceptor.java:

@Override

public Response intercept(Chain chain)  {

    RealInterceptorChain realChain =  (RealInterceptorChain)chain;

    Exchange exchange = realChain.exchange();

    Request request = realChain.request();

    exchange.writeRequestHeaders(request);

    if(HttpMethod.permitsRequestBody( request.method()) && request.body() != null) {

        //向socket当中写入请求的body信息

        request.body().writeTo( bufferedRequestBody);

    } else {

        exchange.noRequestBody();

    }

    if(request.body() != null || !request.body().isDuplex()) {

        //调用exchange的finishRequest方法,表示网络请求的写入工作已经完成

        exchange.finishRequest();

    }

    Response response = exchange.readResponseHeaders(false)

        .request(request)

        .handshake( exchange.connection().handshake())

        .sentRequestAtMillis(sentRequestMillis)

        .receivedResponseAtMillis( System.currentTimeMillis())

        .build();

    ...

    return response;

}

通过httpStream的openResponseBody方法获取body,并通过build创建Response对象,最终返回Response对象。

 

我的理解:

OkHttp最重要的就是分发器和拦截器。

①分发器:分发器中最重要的就是三个队列和一个线程池。不管是同步请求还是异步请求首先都要经过分发器。

如果是同步请求,client将request封装成Call对象后调用RealCall的execute方法,在该方法中,调用分发器的executed方法将该次请求放入runningSyncCalls队列中,然后进入拦截器去获取请求结果。最后不管请求成功还是失败,都会将该请求从runningSyncCalls队列中移除。

如果是异步请求,client将request封装成Call对象后调用RealCall的enqueue方法并传入Callback回调。在enqueue方法中,首先将传入的回调参数封装成AsyncCall对象(实际是一个Runnable),然后调用分发器的enqueue方法,分发器会根据当前runningAsyncCalls个数(最大64)以及每个主机正在运行的异步请求个数(最大5)来判断将该次请求放入runningAsyncCalls队列还是readyAsyncCalls队列中。如果是放入runningAsyncCalls队列会立即调用线程池来执行该请求,因为该请求已经被封装成了一个Runnable,因此会调用它的run方法,在run方法里调用的是AsyncCall的execute方法,在execute里会进入拦截器去获取请求结果。最后不管请求成功还是失败都会将该请求从runningASyncCalls队列中移除。移除后,会从readyAsyncCalls队列中取出一个请求放入runningASyncCalls队列中,并立即调用线程池来执行该请求,依次循环。

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值