okHttp源码分析(1)-分发器

一. okHttp简介

okhttp已经成为Android开发中必不可少的网络请求工具,无论在平时开发还是面试的过程中,都会有所涉及,弄清okhttp的使用流程,已经是每一个android开发者成为高级工程师的必经之路。其实其原理并不复杂,本文将分两篇来着重介绍okhttp中分发器和拦截器原理。

二. 大纲

  1. okHttp的优点以及请求过程
  2. Dispatcher的分发流程

三. okHttp的优点以及请求过程

  1. 首先我们来看看使用okhttp都有哪些优点:
  • 支持HTTP/2并允许对同一主机的所有请求共享一个套接字
  • 通过连接池,复用socket,减少请求延迟
  • 默认通过gzip压缩数据
  • 响应缓存,避免重复请求网络
  • 请求失败自动重试主机其它ip,自动重定向
  1. okHttp的请求流程

使用okhttp的大致流程为:

  1. 创建一个OkHttpClient

  2. 创建一个Request对象

  3. 创建一个call对象,接受request

  4. 进行请求执行任务

  5. 内部通过Dispatcher分发任务

  6. 五大默认拦截器完成整个请求过程

  7. 返回结果

  8. 什么是分发器Dispatcher

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

    这个Dispatcher中的成员有:

    //异步请求同时存在的最大请求
    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<>();
    
  9. get,post请求流程

get请求流程:

//1.创建OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
//2.创建Request对象,设置请求方式。
Request request = new Request.Builder()
        .url("https://www.jianshu.com/u/f260c485f077")
        .get()
        .build();
//3.创建一个call对象
Call call = okHttpClient.newCall(request);
//4.同步请求
new Thread(new Runnable() {
    @Override
    public void run() {
        
        Response response = call.execute();
       
     }}).start();
//5.异步请求
call.enqueue(new Callback() {
    //请求失败执行的方法
    @Override
    public void onFailure(Call call, IOException e) {
        String err = e.getMessage().toString();
    }

    //请求成功执行的方法
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        inal String rtn = response.body().string();
    }
});

post请求流程:

//1.创建OkHttpClient对象,设置参数
OkHttpClient okHttpClient = new OkHttpClient();

FormBody.Builder mBuild = new FormBody.Builder();
mBuild.add("key1", "vaule1")
        .add("key2", "vaule2");
RequestBody requestBodyPost = mBuild.build();

//2.创建Request对象,设置请求方式。
Request request = new Request.Builder()
        .url("http://www.baidu.com")
        .post(requestBodyPost)
        .build();
//3.创建一个call对象
Call call = okHttpClient.newCall(request);
//4.请求回调方法
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    //请求成功执行的方法
    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});

四. Dispatcher的分发流程

okHttp中Dispatcher的分发流程大致分为以下几步:

  1. 调用execute或者enqueue加入任务
  2. Dispatcher根据条件判断将当前任务加入队列 runningAsyncCalls(执行队列)还是readyAsyncCalls(等待队列)
  3. runningAsyncCalls队列调用线程池执行任务,
  4. runningAsyncCalls执行任务完成,根据条件获取readyAsyncCalls中任务执行

根据okhttp中Dispatcher的分发流程,我们有几个问题需要弄清楚:

Q: Dispatcher将请求分发到队列过程中,如何决定放入ready还是running?

Q: 从ready移动running的条件是什么?(如何移动,从哪里移动)

Q: Dispatcher分发器线程池的工作行为是怎样的。

带着以上三个问题,我们可以看看源码是如何实现的。

第一个问题 Dispatcher将请求分发到队列过程中,如何决定放入ready还是running

当我们要执行一个异步请求的时候,会调用okhttp的enqueue方法,其内部就会调用分发器的enqueue方法,我们直接来看分发器的enqueue方法做了什么:

okhttp的enqueue方法:

@Override 
public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    // client就是OkHttpClient 通过client获取Dispatcher 调用Dispatcher的enqueue
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

Dispatcher的enqueue方法:

private int maxRequests = 64;
private int maxRequestsPerHost = 5;

synchronized void enqueue(AsyncCall call) {
    //runningAsyncCalls.size() < maxRequests(最大正在请求的数量)
    //runningCallsForHost(call) < maxRequestsPerHost(同一域名最大正在请求的个数限制)
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) <                      maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
    } else {
        readyAsyncCalls.add(call);
    }
  }

看到这里相信大家都能明白第一个问题,由注解可以看到,Dispatcher将任务加入队列的两个条件,当前最大请求数不超过64,同一域名最大正在请求的个数限制不超过5,同时满足这两个的情况下,想请求加入执行队列runningAsyncCalls中,并立即执行。不满足条件加入等待队列readyAsyncCalls中。

第二个问题 从ready移动running的条件是什么?(如何移动,从哪里移动)

然后我们再回过头来看看AsyncCall是个什么,我们的请求加入这个AsyncCall以后都做了什么,打开AsyncCal源码

RealCall implements Call 

首先可以看到RealCall implements Call ,当我们调用call.enqueue时,也就是再调用RealCall.enqueue,那么我们只要看RealCall.enqueue中做了什么即可。

@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 {
            responseCallback.onFailure(RealCall.this, e);
        }
    } finally {
        //请求执行完毕,调用finish();
        client.dispatcher().finished(this);
    }
}

如何把ready移动running,那么肯定是running中任务已经执行完成,所以我们继续看finnally中 client.dispatcher().finished(this);做了什么。

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();
    }
}
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();
        //判断当前call的host执行个数是否小于maxRequestsPerHost
        if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            //将等待队列中的call加入到runningAsyncCalls
            runningAsyncCalls.add(call);
            //开始执行任务
            executorService().execute(call);
        }
        //判断正在执行的队列个数,满足就returen,不在继续增加
        if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
}

以上代码逻辑,注释的很清晰。会通过相同的条件,将等待队列中的请求加入到执行队列

最后一个问题,分发器线程池的工作行为是怎样的。

看到这里,相信大家已经对okhtt的分发流程了解个大概,此时我们已经将请求加入到了队列中,而在分发执行请求的过程中,okhttp是怎么来执行这些任务的呢,还是继续来看源码,通过以上源码的分析我们可以看到Dispatcher是通过executorService().execute(call)方法执行任务,所以我们直接看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;
}

在OkHttp的分发器中的线程池定义如上,其实就和Executors.newCachedThreadPool()创建的线程一样。首先核心线程为0,表示线程池不会一直为我们缓存线程,线程池中所有线程都是在60s内没有工作就会被回收。而最大线程Integer.MAX_VALUE与等待队列SynchronousQueue的组合能够得到最大的吞吐量。即当需要线程池执行任务时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。一般来说,等待队列BlockingQueue有:ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue

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

ArrayBlockingQueue:基于数组的阻塞队列,初始化需要指定固定大小。

​ 当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入队列就会失败,这时就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。所以最终可能出现后提交的任务先执行,而先提交的任务一直在等待。

LinkedBlockingQueue:基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。

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

SynchronousQueue : 无容量的队列。

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

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

分发器的流程以及原理就到这里,okhttp每个版本代码可能会略有不同,本文基于okhttp3.6.0源码进行分析。下一篇我们将介绍okhttp中的拦截器是如何实现的。

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

推荐阅读更多精彩内容