几分钟带你搞懂OkHttp3(一)

没想到我竟然更新得这么频繁,之前有写个一篇OkHttp相关的文章,只不过那篇是带着问题去看源码,只是为了解决问题。这次是带着好奇心来看,想知道它内部到底做了什么。
主要还是以分析源码来分析整个流程,大致的流程我能保证是能讲对的,不会坑爹,但是一些细节上的思想我不敢保证是不是那么一回事。
这份源码是基于okhttp 3.11.0 不保证以后的流程不会改动

一. 调用过程

写个最简单调用Okhttp的方法

        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url("https://www.baidu.com/")
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

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

            }
        });

1. 设计分析

(1)OkHttpClient是一个客户端,一个抽象的客户端,所以它内部有引用很多对象,就像我们一个请求的客户端有很多东西。
(2)Request是一个请求数据对象,可以当成一个快递,这个快递有快递内容、寄件人信息、收件人信息等等。
(3)Response 是一个响应对象,和上面差不多,就只包含了响应的内容。
(4)Call 是请求,是一个行为。
那么客户端发送一个请求,这个抽象用代码表示就是okHttpClient.newCall(request)。
请求数据能有很多个,所以Request每次都是new出来,就像寄很多快递每个都不同,请求也有很多个,因为每个请求都是独立的,就像每个快递员帮你寄一个快递,所以Call也是每次newCall都生成不同的对象。但是客户端从抽象的层面讲只有一个,你就是你,是独一无二的,所以从理论上来讲OkHttpClient应该是单例,并且okhttp在使用的时候OkHttpClient确实是全局共用一个,但是我不知道为什么设计者没有把它设计成单例,而是让使用者用单例的形式去使用。

2.简单的引用关系图

二. 从源码分析内部流程

OkHttpClient okHttpClient = new OkHttpClient(); 就不用说了,获取客户端对象。
Request request = ...... ; 也不用说了,打包请求数据

1. newCall方法

Call call = okHttpClient.newCall(request);

往下看

    public Call newCall(Request request) {
        return RealCall.newRealCall(this, request, false);
    }

看出RealCall是Call的具体实现类,可以参考上面的流程图。

    static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
        RealCall call = new RealCall(client, originalRequest, forWebSocket);
        call.eventListener = client.eventListenerFactory().create(call);
        return call;
    }

嗯,每次newCall都是new 一个新的对象,没毛病,符合上面的分析。
构造方法就是一些赋值操作,就不贴代码了
eventListener 是一个监听器,通过钩子告诉调用者当前的流程走到哪步了,是用一个工厂来创建

从这里就能看出newCall方法就只是创建一个RealCall并且做了一些初始化的操作。

2. enqueue

这是具体的请求操作

call.enqueue(callback);

往下看

    public void enqueue(Callback responseCallback) {
        // 防止同一个call对象重复调用enqueue方法
        synchronized(this) {
            if (this.executed) {
                throw new IllegalStateException("Already Executed");
            }
            this.executed = true;
        }

        this.captureCallStackTrace();
        this.eventListener.callStart(this);  // 这里调用钩子通知整个call流程开始
        this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
    }

我在想这里的synchronized换成使用Atomic是不是会好一点,欢迎评论告诉我。
发现调用了OkHttpClient的Dispatcher的enqueue方法,传了一个AsyncCall对象。
这个AsyncCall是一个CallBack,所以我们可以先不用管它,再回调之后再去看它内部的代码

    final class AsyncCall extends NamedRunnable {
public abstract class NamedRunnable implements Runnable {
    protected final String name;

    public NamedRunnable(String format, Object... args) {
        this.name = Util.format(format, args);
    }

    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(this.name);

        try {
            this.execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }

    }

    protected abstract void execute();
}

这个execute是AsyncCall 的方法,我们回调的地方再看,先继续看Dispatcher的enqueue

3.enqueue

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

跳到这个方法里面

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

    }

这代码很简单,从它的设计角度出发,它是设计了一个“生产—消费者模型”,至于这个是什么,这里不讲了,自己去找,很简单的。所以设置了两个队列,一个队列runningAsyncCalls表示正在运行,一个队列readyAsyncCalls表示等待运行。如果运行队列满了,那这个请求就进入等待队列。从maxRequests 看出,他设计的默认的队列的大小是64,当然也有提供方法给调用者去改。
放到队列之后执行 this.executorService().execute(call);

    public synchronized ExecutorService executorService() {
        if (this.executorService == null) {
            this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
        }

        return this.executorService;
    }

这是一个没有核心线程的线程池,也不用多说了吧,这篇主要讲okhttp,线程池这些就不细说了,也不难。但是从这里能看出,okhttp这个请求流程的核心就是这两个队列+线程池进行调度。
然后等线程被cpu调度之后,就会去执行Runnable的run方法,也就是上面说的AsyncCall的execute方法。

4. AsyncCall 的 execute

        protected void execute() {
            boolean signalledCallback = false;

            try {
                Response response = RealCall.this.getResponseWithInterceptorChain();
                if (RealCall.this.retryAndFollowUpInterceptor.isCanceled()) {
                    signalledCallback = true;
                    this.responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
                } else {
                    signalledCallback = true;
                    this.responseCallback.onResponse(RealCall.this, response);
                }
            } catch (IOException var6) {
                if (signalledCallback) {
                    Platform.get().log(4, "Callback failure for " + RealCall.this.toLoggableString(), var6);
                } else {
                    RealCall.this.eventListener.callFailed(RealCall.this, var6);
                    this.responseCallback.onFailure(RealCall.this, var6);
                }
            } finally {
                RealCall.this.client.dispatcher().finished(this);
            }

        }

Response response = RealCall.this.getResponseWithInterceptorChain(); 这是具体的网络请求的过程。所以说,Dispatcher只是负责调度的工作,Call才是具体负责请求操作的。简单来说就是你去食堂打饭,Call才是负责打饭的食堂大妈,Dispatcher只是负责管理排队的保安,你敢插队砍洗你。
然后我们先不管这个请求具体做了什么操作,因为里面的拦截器那些代码逻辑不那么简单,总之先当成做了某部操作得到结果Response ,详解的操作封装得很好,十分的解耦,放心。

得到结果Response 之后调responseCallback的onFailure或者onResponse方法通知调用者网络请求的结果。并且
RealCall.this.client.dispatcher().finished(this); 告诉调度者,这个请求流程到这里已经执行完了。

5. Dispatcher的finished

    void finished(AsyncCall call) {
        this.finished(this.runningAsyncCalls, call, 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!");
            }

            if (promoteCalls) {
                this.promoteCalls();
            }

            // 后面的idleCallback是一个扩展吧,暂时不用管
            runningCallsCount = this.runningCallsCount();
            idleCallback = this.idleCallback;
        }

        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }

    }

可以看出这里从运行队列中移除请求之后,调用promoteCalls方法

    private void promoteCalls() {
        if (this.runningAsyncCalls.size() < this.maxRequests) {
            if (!this.readyAsyncCalls.isEmpty()) {
                Iterator i = this.readyAsyncCalls.iterator();

                do {
                    if (!i.hasNext()) {
                        return;
                    }

                    AsyncCall call = (AsyncCall)i.next();
                    if (this.runningCallsForHost(call) < this.maxRequestsPerHost) {
                        i.remove();
                        this.runningAsyncCalls.add(call);
                        this.executorService().execute(call);
                    }
                } while(this.runningAsyncCalls.size() < this.maxRequests);

            }
        }
    }

这里的操作就是判断移除之后的运行队列是否小于最大长度,如果小于,则从准备队列中将第一个请求放到运行队列中然后运行,就永动机了。
这里整个过程就结束了

6. runningCallsForHost方法

单独把这个方法拿出来讲是因为我不敢保证我当前理解就是对的。请求运行之前的enqueue方法,和请求结束之后的finished方法,都有调用这个方法

this.runningCallsForHost(call) < this.maxRequestsPerHost
    private int runningCallsForHost(AsyncCall call) {
        int result = 0;
        Iterator var3 = this.runningAsyncCalls.iterator();

        while(var3.hasNext()) {
            AsyncCall c = (AsyncCall)var3.next();
            if (!c.get().forWebSocket && c.host().equals(call.host())) {
                ++result;
            }
        }

        return result;
    }

(1)首先forWebSocket 这个参数,上面源码中显示传进来的是false,我没具体理解是什么意思,讲真从这命名真看不出来,我感觉是一个开关,提供给扩展用的。
(2)c.host().equals(call.host()))
这个host是HttpUrl的,我的理解是判断相同主机的请求是否一样,一样的话就+1,并且限制最大是5个,也就是说你的url如果是同一个Host,只能同时请求5个,第6个排队去。我不知道这个判断的设计思想是什么,限制这个是为了保证什么?

总结

本来是想一次性写完整个流程的,但我最近在在找工作,这段时间一直在准备比较忙,所以没能写完。所以这次打算分开写,不然我这篇写到一半就要搁着了。这篇文章先介绍了整个okhttp并发的一个过程,具体的请求操作,拦截器那块等总结好了再发出来,只能先说一声抱歉了。

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

推荐阅读更多精彩内容