OkHttp(一)—— 整体流程与分发器

一、介绍

OkHttp 是由 Square 公司贡献的一个处理网络请求的开源项目,是目前 Android 使用最广泛的网络框架,从 Android4.4 开始 HttpURLConnection 的底层实现开始采用 OkHttp,Retrofit 框架底层同样也使用了 OkHttp。官方主页:

OkHttp 有非常多的优点,比如:

  • 支持 Http1、Http2、Quic 以及 WebSocket
  • 允许对同一主机的所有请求共享一个套接字
  • 连接池复用底层 TCP(Socket),减少请求延时
  • 无缝支持 GZIP,默认使用 GZIP 压缩数据以减少数据流量
  • 响应缓存数据,减少重复的网络请求
  • 请求失败自动重试主机的其他 ip,自动重定向

文章中关于 OkHttp 的使用与源码分析会基于 3.10.0 版本,当然我们能查到 3.x 版本在 2020.5.17 之后就不再更新了,而从 4.x 版本开始,OkHttp 改由 Kotlin 实现,后续版本的新东西,有机会会在后面更新。

二、使用方法

还是要简单的提一下使用方法。

2.1 基本使用方法

首先添加 OkHttp 依赖:

	implementation "com.squareup.okhttp3:okhttp:3.10.0"

OkHttp 支持同步或异步发起一个网络请求,我们先用同步方式执行一个 GET 请求:

	// 同步方式不能在主线程中执行
    private void executeGet() throws IOException {
        // 获取默认配置的 OkHttpClient
        OkHttpClient client = new OkHttpClient();
        // 构造一个 GET 请求
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
        // 获取网络请求的执行任务
        Call call = client.newCall(request);
        // 同步方式执行任务
        Response response = call.execute();
        // 打印响应体
        Log.d(TAG, "response body:" + response.body().string());
    }

用异步方式执行一个 POST 请求:

	// 异步方式可以在主线程中执行
	public void executePost() {
        // 自定义一个 OkHttpClient
        OkHttpClient client = new OkHttpClient.Builder()
                .writeTimeout(2000, TimeUnit.MICROSECONDS)
                .build();

        // 构造一个 POST 请求
        RequestBody requestBody = new FormBody.Builder()
                .add("param1", "111")
                .build();
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .post(requestBody)
                .build();

        // 异步方式执行请求
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "Post request failed.");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "Post request succeed,response body:" + response.body().string());
            }
        });
    }

2.2 调用流程

从上述例子中可以看出,使用 OkHttp 进行网络请求至少需要接触到 OkHttpClient、Request、Call 和 Response,框架内还会进行大量的逻辑处理,流程大致如下:

在这里插入图片描述

  1. 首先创建一个 OkHttpClient 对象,可以通过构造方法获得一个默认配置的 OkHttpClient,也可以通过 OkHttpClient.Builder 根据需求自己配置一个
  2. 根据需求创建一个网络请求对象 Request,并作为参数传递给 OkHttpClient 的 newCall() 生成一个任务 Call(newCall() 其实会调用实现类 RealCall 的 newRealCall() 返回一个 RealCall 对象)
  3. 如需同步执行任务则调用 Call 的 execute(),异步执行任务调用 Call 的 enqueue()
  4. 任务不论是同步还是异步执行,都会交由分发器 Dispatcher 负责存入相应的队列,并调度任务去执行
  5. 执行任务的核心是拦截器 Interceptors,OkHttp 内置的五大拦截器会帮助我们将网络请求发出,并接收服务器的响应封装到 Response 后返回

其实分发器和拦截器为我们做了大量的复杂且细致的工作,只不过由于 OkHttp 采用了门面模式,将这些细节都隐藏起来,仅通过 OkHttpClient 对象统一暴露子系统接口。

三、分发器 Dispatcher

Dispatcher 主要用来调配请求任务,其内部包含一个线程池,在用构建者模式创建 OkHttpClient 对象时,可以通过 dispatcher() 指定一个自定义的分发器(指定线程池、闲时任务等)。分发器内的主要成员有:

public final class Dispatcher {
	// 异步请求同时存在的最大请求
    private int maxRequests = 64;
    // 异步请求同一域名同时存在的最大请求
    private int maxRequestsPerHost = 5;
    // 闲时任务(没有请求时,可以去执行的其它任务)
    private @Nullable Runnable idleCallback;

    // 异步请求使用的线程池,懒创建线程池对象
    private @Nullable ExecutorService executorService;

    // 异步请求等待执行的任务队列
    private final Deque<RealCall.AsyncCall> readyAsyncCalls = new ArrayDeque<>();

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

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

我们注意到异步任务类型是 RealCall.AsyncCall,并且分成了执行队列和等待队列;而同步任务类型是 RealCall,仅有执行队列。

下面我们就来分析同步请求和异步请求的任务是如何被调度的。

3.1 同步请求

同步请求的流程相较于异步请求要简单不少,所以我们先来看同步请求。

调用 Call 的 execute() 会以同步方式执行该任务,要看实现类 RealCall 的具体处理:

	@Override
    public Response execute() throws IOException {
    	// 防止被重复调用
        synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        captureCallStackTrace();
        eventListener.callStart(this);
        try {
        	// 任务入队要交给 Dispatcher
            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 {
        	// 无论是成功得到了结果还是抛出了异常,都要让 Dispatcher 标记当前任务已经结束
            client.dispatcher().finished(this);
        }
    }

Dispatcher 会将任务添加到同步队列中:

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

在任务执行结束后,Dispatcher 会将任务从队列中移除:

	void finished(RealCall call) {
		// 将 call 从 runningSyncCalls 队列中移除,false 表示不需要优化任务调度,
		// 因为只有在进行异步任务调度时,才会用到,后面介绍异步任务时会看到
    	finished(runningSyncCalls, call, false);
    }

3.2 异步请求

先来看一下异步任务的执行过程:

  1. AsyncCall 调用 enqueue() 以异步方式执行,Dispatcher 负责将 AsyncCall 入队,入队前,先看当前队列状态是否可以将 AsyncCall 加入运行队列 runningAsyncCalls,条件满足则放入,并且放入线程池 ThreadPool 中执行,否则放入等待队列 readyAsyncCalls 中;
  2. 在 ThreadPool 中的任务被执行后,会调用 AsyncCall 的 finished() 来结束该任务,finished() 主要做了两件事:
    2.1. 将该任务从 runningAsyncCalls 中移除
    2.2.调用 promoteCalls(),去 readyAsyncCalls 中筛选是否有任务满足条件可以添加到 runningAsyncCalls 中,如有,则将其添加到 runningAsyncCalls 内,且放入线程池中

以上是异步任务的执行流程,至于图中的两个进入 runningAsyncCalls 的条件,其实是 runningAsyncCalls 中的任务数没有达到 maxRequests,且对于同一 Host 的请求数没有到达 maxRequestsPerHost,后续我们结合源码看会更清晰一些。

结合上图来看源码,首先,调用 Call.enqueue() 异步执行任务时,实际上调用的是 RealCall 的 enqueue():

	@Override
    public void enqueue(Callback responseCallback) {
    	// 第二次调用 enqueue() 就会抛异常,保证最多被执行一次
        synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        captureCallStackTrace();
        eventListener.callStart(this);
        // 通过 Dispatcher 的 enqueue() 将任务入队,而且任务类型是 RealCall.AsyncCall
        client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
    }

Dispatcher 在拿到 AsyncCall 这个任务后,会根据当前异步任务队列的状态决定把它放到哪个队列:

	synchronized void enqueue(RealCall.AsyncCall call) {
		// 正在运行的异步任务数小于最大值(默认64),并且同一域名的异步请求小于最大值(默认5)
        if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        	// 放入正在运行的队列,并放入线程池中
            runningAsyncCalls.add(call);
            executorService().execute(call);
        } else {
        	// 放入等待队列中
            readyAsyncCalls.add(call);
        }
    }

假如命中了 if 条件,将 AsyncCall 放入正在执行队列,并放入线程池中执行该任务,那么 AsyncCall 作为 Runnable 的一个实现类,它的 run() 包含什么样的任务呢?追溯源码,我们发现 AsyncCall 并没有重写 run(),这是由于它继承自一个抽象类 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 希望子类将需要执行的任务写在 execute() 中,所以我们回头看 AsyncCall 的 execute():

	@Override
    protected void execute() {
    	// signalledCallback 表示用户是否赋值了 Callback
        boolean signalledCallback = false;
        try {
        	// 通过责任链发送请求并得到响应,责任链后面会详解
            Response response = getResponseWithInterceptorChain();
            // 当 try 代码块中运行出现异常进入 catch 时,如果 signalledCallback 为 true
            // 表示是用户的 Callback 中发生了异常,否则就是上面的由 OkHttp 的责任链
            // 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 {
        	// 无论是成功执行还是发生了异常,都会让 Dispatcher 结束这个任务
            client.dispatcher().finished(this);
        }
    }

不管任务执行成功与否,最后都需要让 Dispatcher 结束这个任务:

	// 异步任务结束
	void finished(RealCall.AsyncCall call) {
        finished(runningAsyncCalls, call, true);
    }
    
    // 同步任务结束
    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) {
        	// 将任务 call 从队列 calls 中移除
            if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
            // 异步任务的 promoteCalls 为 true,同步为 false,promoteCalls() 会将处于等待
            // 状态、并且符合条件的异步任务,从其所在队列中拿到正在运行的异步任务队列当中去
            if (promoteCalls) promoteCalls();
            // 计算正在运行的任务数量,这是运行中的同步和异步任务数量之和
            runningCallsCount = runningCallsCount();
            idleCallback = this.idleCallback;
        }

		// 如果当前没有正在运行的任务,且设置了闲时任务,则开始执行闲时任务
        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }
    }

promoteCalls() 并不是简单的从 readyAsyncCalls 队列中将队首任务取出放入 runningAsyncCalls 队列,而是有条件的筛选:

	private void promoteCalls() {
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

        for (Iterator<RealCall.AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
            RealCall.AsyncCall call = i.next();
			// 保证同一个 Host 的任务数不超过 5 个的情况下,才会移动 call
            if (runningCallsForHost(call) < maxRequestsPerHost) {
                i.remove();
                runningAsyncCalls.add(call);
                executorService().execute(call);
            }

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

以上就是异步请求的大致流程。

3.3 线程池

Dispatcher 调度异步任务离不开线程池。这个线程池对象可以通过 Dispatcher 的构造方法指定,如果没有显式指定的话,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;
    }

ThreadPoolExecutor() 各参数含义如下:

  • 核心线程数:核心线程池的大小。如果核心线程池有空闲位置,新来的任务就会被核心线程池新创建的线程执行。默认情况下,核心线程在执行完任务,处于空闲状态时也不会被回收,而是在缓存队列中等待再次被运行,除非你手动将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设置为 true(默认为 false),那么核心线程将会与非核心线程一样,在空闲时间到达第三、四个参数组成的保活时间之后,被销毁。
  • 最大线程数:线程池能创建最大的线程数量,是核心线程数与非核心线程数的和。如果核心线程池和缓存队列都已经满了,新任务进来就会创建新的线程来执行,但是数量不能超过最大线程数,否则会采取拒绝接受任务策略(拒绝策略可以在 ThreadPoolExecutor 的构造方法中配置,但是当前方法并没有配)。
  • 保活时间:默认情况下指非核心线程处于空闲等待状态的最长时间,一旦超过这个时间就会被销毁。核心线程在设置 allowCoreThreadTimeOut 为 true 之后也会受到这个时间的约束。
  • 保活时间单位:保活时间的单位,有天、小时、分钟、秒等。
  • 线程等待队列:缓存队列,用来存放等待执行中的任务,需是一个阻塞队列,一般可以是 ArrayBlockingQueue、LinkedBlockingQueue 和 SynchronousQueue 三者之一。
  • 线程创建工厂:创建线程的工厂,这里是给创建的线程命名为"OkHttp Dispatcher",且设置不是守护线程。

了解了以上参数后,再来看看线程池是如何调度任务的。

线程池调度机制

当我们通过 ThreadPoolExecutor 的 execute() 提交一个任务进线程池时,线程池是如何处理这个任务的?来看下图:

基本思想还是本着【核心线程->阻塞队列->空闲线程/新建线程/拒绝】的优先顺序来执行任务,只有上一级条件不满足时才会由下一级执行。

具体的处理顺序:

  1. 如果当前线程池中的线程数量小于最大核心线程数,那么创建一个新的核心线程来处理该任务
  2. 如果当前线程数量不小于最大核心线程数,那就要尝试将新任务添加到阻塞队列中
  3. 添加队列成功,如果当前线程池中没有任何线程,那就新建一个非核心线程来执行任务;如果线程池有线程,那就让任务等待空闲线程执行自己
  4. 添加队列失败,如果线程数量小于最大线程数,则创建一个新线程执行任务,否则就只能根据拒绝策略拒绝该任务了

简单看下 execute() 的源码:

	public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        int c = ctl.get();
        // 1.如果运行中的线程数量小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
        	// 创建一个新的核心线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2.如果核心线程满载了,并且任务被成功添加到队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // double-check 一下是否应该新建一个线程执行这个任务,因为可能会有一个
            // 自上次检查之线程池的状态发生了改变,比如变为 SHUTDOWN 状态,这样的话
            // 还需要回滚入队操作,并拒绝这个任务
            if (!isRunning(recheck) && remove(command))
                reject(command);
            // 如果线程池中一个线程都没有,那就新起一个线程执行任务
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 3.任务不能入队,先尝试启动一个非核心新线程,如果失败,则只能拒绝该任务
        else if (!addWorker(command, false))
            reject(command);
    }

实际上,线程池调度的细节内容还是很多的,篇幅受限我们仅了解整体的调度思路,帮助我们更好的理解 OkHttp 对线程池的应用。

OkHttp 配置线程池

我们再回头看一下 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;
    }

这个线程池的配置,其实和 Executors.newCachedThreadPool() 创建的缓存线程池一样(但是之前看阿里的编程规范中规定,强制不允许使用 Executors 创建线程池,原因是使用 new ThreadPoolExecutor() 这种方式能让使用者更加明确线程池的运行规则,规避资源耗尽的风险),核心线程数为 0,表示线程池不会一直为我们缓存线程,所有线程在空闲时间到达 60s 后就会被回收,并且有无等待,高并发,最大吞吐量的特点。

前面我们也提到,异步队列有三种:SynchronousQueue、ArrayBlockingQueue 和 LinkedBlockingQueue,那 OkHttp 为什么选择了 SynchronousQueue?这其实与线程池的任务调度机制有关。

记得,OkHttp 配置的默认线程池的核心线程数为 0,这是前提。

先来看 ArrayBlockingQueue,该队列基于数组,初始化时必须显式指定一个队列容量,在队列满了之后,可能会出现后提交的任务先被执行,而先提交的任务一直处于等待状态的问题。示例代码:

	public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), threadFactory(false));

        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1:" + Thread.currentThread());
                while (true) {
                }
            }
        });

        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2:" + Thread.currentThread());
            }
        });

        /*executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务3:" + Thread.currentThread());
            }
        });*/
    }

我们设置了 ArrayBlockingQueue 的容量为 1,先执行任务 1,内部的 while 死循环模拟执行时间较长、队列已满的情况,然后执行任务 2,猜一下此时的运行结果是什么呢?

任务1Thread[Thread-0,5,main]

任务 2 并没有得到执行!而在打开任务 3 的注释,去执行任务 3,神奇的事情发生了:

任务1Thread[Thread-0,5,main]
任务3Thread[Thread-1,5,main]
任务2Thread[Thread-1,5,main]

任务 2 和任务 3 都被同一个线程执行了!并且任务 3 先于任务 2 执行!发生这种现象的原因,要结合线程池调度机制来看。我们叙述一下示例代码的执行步骤:

  1. 初始条件:线程池没有任何线程,ArrayBlockingQueue 容量为 1
  2. 执行任务 1,由于现在没有线程,那么任务 1 要进入 ArrayBlockingQueue。入队成功后,会做个检查,如果当前一个线程都没有,那就新起一个线程执行任务 1,执行它时会将其从 ArrayBlockingQueue 中移除
  3. 再执行任务 2,由于此时没有核心线程和空闲线程,但是 ArrayBlockingQueue 中有位置,所以任务 2 可以入队,入队成功的它会等待一个空闲线程来执行自己。但是当前线程池中唯一的线程在执行任务 1,而任务 1 执行时间很长,导致一直没有空闲线程,所以任务 2 就一直等待,得不到执行
  4. 最后执行任务 3,由于此时还是没有核心线程和空闲线程,而且 ArrayBlockingQueue 也满了(容量为 1,被任务 2 占据),所以任务 3 入队失败。由于当前线程池中的线程数小于最大线程数,所以可以新创建一个线程执行任务 3
  5. 任务 3 被执行完毕后,执行它的线程空闲了,等待了许久的任务 2 终于等来了一个空闲线程来执行自己,所以任务 2 最后被执行

LinkedBlockingQueue 与 ArrayBlockingQueue 存在同样的问题,即便不显式指定容量,其内部也会使用默认的 Integer.MAX 作为队列容量。

SynchronousQueue 是没有容量的队列,添加任务会一直失败,在没有空闲线程且线程数量没达到最大线程数的情况下,会直接起一个新线程执行任务,这是使用 SynchronousQueue 的主要原因。这种线程池的配置也满足高并发、最大吞吐量的特点,线程池的作用也集中体现在线程复用上。当然,即便使用了 SynchronousQueue 线程数也不会达到线程池定义时设置的 Integer.MAX 那么多,因为 OkHttp 不是定义了最大请求数 maxRequests = 64 限制着呢么~

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值