Android OkHttp源码分析--分发器

OkHttp是当下Android使用最频繁的网络请求框架,由Square公司开源。Google在Android4.4以后开始将源码中 的HttpURLConnection底层实现替换为OKHttp,同时现在流行的Retrofit框架底层同样是使用OKHttp的。

OKHttp优点:

1、支持Http1、Http2、Quic以及WebSocket;

2、连接池复用底层TCP(Socket),减少请求延时;

3、无缝的支持GZIP减少数据流量;

4、缓存响应数据减少重复的网络请求;

5、请求失败自动重试主机的其他ip,自动重定向;

OKHttp调用流程:

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

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

分发器:内部维护队列与线程池,完成请求调配;

拦截器:五大默认拦截器完成整个请求过程。

用户是不需要直接操作任务分发器的,获得的 RealCall 中就分别提供了 execute 与 enqueue 来开始同步请求或异步请求。无论是同步还是异步请求实际上真正执行请求的工作都在 getResponseWithInterceptorChain() 中。这个 方法就是整个OkHttp的核心:拦截器责任链。

java 复制代码
@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);
        //执行请求
        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);
    }
}

分发器:异步请求工作流程

Dispatcher ,分发器就是来调配请求任务的,内部会包含一个线程池。可以在创建 OkHttpClient 时,传递我们 自己定义的线程池来创建分发器。

Dispatcher中的成员有:

java 复制代码
//异步请求同时存在的最大请求
private int maxRequests = 64;
//异步请求同一域名同时存在的最大请求
private int maxRequestsPerHost = 5;
//闲置任务(没有请求时可执行一些任务,由使用者设置)
private @Nullable Runnable idleCallback;
//异步请求使用的线程池
private @Nullable ExecutorService executorService;
//异步请求等待执行队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//异步请求正在执行队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步请求正在执行队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

同步请求:

java 复制代码
synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
}

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。

异步请求:

java 复制代码
synchronized void enqueue(AsyncCall call) {
	if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) <
		maxRequestsPerHost) {
		runningAsyncCalls.add(call);
		executorService().execute(call);
	} else {
		readyAsyncCalls.add(call);
	}
}

当正在执行的任务未超过最大限制64,同时 runningCallsForHost(call) < maxRequestsPerHost 同一Host的请求 不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

加入线程池直接执行,如果加入等待队列后,就需要等待有空闲名额才开始执行。因此每次执行完 一个请求后,都会调用分发器的 finished 方法

java 复制代码
    //异步请求调用
    void finished(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) {
            //不管异步还是同步,执行完后都要从队列移除(runningSyncCalls/runningAsyncCalls)
            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() 。很显然这个方法肯定会重新调配请求。

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();
            //等待任务想要执行,还需要满足:这个等待任务请求的Host不能已经存在5个了
            if (runningCallsForHost(call) < maxRequestsPerHost) {
                i.remove();
                runningAsyncCalls.add(call);
                executorService().execute(call);
            }
            if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
        }
    }

在满足条件下,会把等待队列中的任务移动到 runningAsyncCalls 并交给线程池执行。所以分发器到这里就完了。 逻辑上还是非常简单的。

分发器线程池:

分发器就是来调配请求任务的,内部会包含一个线程池。当异步请求时,会将请求任务交给线程池 来执行。那分发器中默认的线程池是如何定义的呢?为什么要这么定义?

java 复制代码
    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;
    }

在OkHttp的分发器中的线程池定义如上,其实就和 Executors.newCachedThreadPool() 创建的线程一样。首先核 心线程为0,表示线程池不会一直为我们缓存线程,线程池中所有线程都是在60s内没有工作就会被回收。

而最大线 程 Integer.MAX_VALUE 与等待队列 SynchronousQueue 的组合能够得到最大的吞吐量。即当需要线程池执行任务 时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。

一般来说,等待队列 BlockingQueue 有: ArrayBlockingQueue 、 LinkedBlockingQueue 与 SynchronousQueue 。

假设向线程池提交任务时,核心线程都被占用的情况下:

**ArrayBlockingQueue :**基于数组的阻塞队列,初始化需要指定固定大小。 当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入 队列就会失败,这时就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。所以 最终可能出现后提交的任务先执行,而先提交的任务一直在等待。

**LinkedBlockingQueue :**基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。 当指定大小后,行为就和 ArrayBlockingQueu 一致。而如果未指定大小,则会使用默认的 Integer.MAX_VALUE 作 为队列大小。这时候就会出现线程池的最大线程数参数无用,因为无论如何,向线程池提交任务加入等待队列都会 成功。最终意味着所有任务都是在核心线程执行。如果核心线程一直被占,那就一直等待。

**SynchronousQueue :**无容量的队列。 使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后 如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任 务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合 Integer.MAX_VALUE 就实现了真正的 无等待。

但是需要注意的时,我们都知道,进程的内存是存在限制的,而每一个线程都需要分配一定的内存。所以线程并不 能无限个数。那么当设置最大线程数为 Integer.MAX_VALUE 时,OkHttp同时还有最大请求任务执行个数: 64的限制。这样即解决了这个问题同时也能获得最大吞吐。

相关推荐
大耳猫4 小时前
主动测量View的宽高
android·ui
帅次6 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛7 小时前
Android中Crash Debug技巧
android
kim565912 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼13 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ13 小时前
Android Studio使用c++编写
android·c++
csucoderlee13 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim565913 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式13 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。13 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio