OkHttp 知识梳理(2) - OkHttp 源码解析之异步请求 & 线程调度

OkHttp 知识梳理系列文章

OkHttp 知识梳理(1) - OkHttp 源码解析之入门
OkHttp 知识梳理(2) - OkHttp 源码解析之异步请求 & 线程调度
OkHttp 知识梳理(3) - OkHttp 之缓存基础


一、前言

OkHttp 知识梳理(1) - OkHttp 源码解析之入门 中,介绍了OkHttp的简单使用及同步请求的实现流程,今天这篇文章,我们来一起学习一下异步请求的内部实现原理及线程调度。

首先,让我们回顾一下异步请求的实现方式:

    private void startAsyncRequest() {
        //以下三步和同步请求的步骤相同。
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(URL).build();
        Call call = client.newCall(request);
        //区别在于拿到 RealCall 对象之后的处理方式。
        call.enqueue(new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                //返回结果给主线程。
                Message message = mMainHandler.obtainMessage(MSG_UPDATE_UI, result);
                mMainHandler.sendMessage(message);
            }
        });
    }

可以看到,对于异步请求而言,前面三步和同步请求是相同的,区别在于发起请求时,同步请求使用的是call.execute(),只有当整个请求完成时才会从.execute()函数返回。

而对于异步请求来说,.enqueue(Callback)方法只要调用完就立即返回了,当网络请求返回之后会回调CallbackonResponse/onFailure方法,并且这两个回调方法是在子线程执行的,这也是异步请求和同步请求之间最主要的差别。

下面我们就来分析一下异步请求的内部实现逻辑。

二、异步请求源码解析

对于前面三步的内部实现不再重复说明了,大家可以查看 OkHttp 知识梳理(1) - OkHttp 源码解析之入门 中的分析。最终我们会得到一个RealCall实例,它代表了一个执行的任务。接下来看enqueue内部做了什么。

    public void enqueue(Callback responseCallback) {
        //首先判断该对象是否曾经被执行过。
        synchronized(this) {
            if(this.executed) {
                throw new IllegalStateException("Already Executed");
            }

            this.executed = true;
        }
        //捕获堆栈信息。
        this.captureCallStackTrace();
        //通知监听者请求开始了。
        this.eventListener.callStart(this);
        //调用调度器的 enqueue 方法。
        this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
    }

这里,我们又见到了熟悉的dispatcher()类,它enqueue的实现为:

    private int maxRequests = 64;
    private int maxRequestsPerHost = 5;
    
    //执行任务的线程池。
    private ExecutorService executorService;

    //等待被执行的异步请求任务队列。
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();
    //正在被执行的异步请求任务队列。
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();
    
    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;
    }

    synchronized void enqueue(AsyncCall call) {
        //如果当前正在请求的数量小于 64,并且对于同一 host 的请求小于 5,才发起请求。
        if(this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
            //将该任务加入到正在请求的队列当中。
            this.runningAsyncCalls.add(call); 
            //通过线程池执行任务。
            this.executorService().execute(call);
        //否则加入到等待队列当中。
        } else {
            this.readyAsyncCalls.add(call);
        }
    }

Dispatcherenqueue首先会判断:如果当前正在请求的数量小于64,并且对于同一host的请求小于5,才发起请求。发起请求之前会将RealCall加入到runningAsyncCalls队列当中,并通过ThreadPoolExecutor来执行该请求,

2.1 通过线程池执行任务

ThreadPoolExecutorJava提供的线程池,在 多线程知识梳理(6) - 线程池四部曲之 ThreadPoolExecutor 中我们已经介绍过它,这里根据它的参数配置可以看出,它对应于CachedThreadPool,该线程池的特点是 线程池大小无界,适用于执行很多的短期异步任务的程序或者是负载较轻的服务器


它的具体实现方式为:

  • 等待队列使用的是SynchonousQueue,它的 每个插入操作都必须等待另一个线程的移除操作,对于线程池而言,也就是说:在添加任务到等待队列时,必须要有一个空闲线程正在尝试从等待队列获取任务,才有可能添加成功。
  • 因此,当一个任务被添加进入线程池时,会有以下两种情况:
    • 如果当前有空闲线程正在尝试从等待队列中获取任务,那么这个 任务将会被交给这个空闲线程 进行处理
    • 如果当前没有空闲线程尝试从等待队列中获取任务,那么将会 创建一个新线程来执行任务
  • 由于设置了等待超时时间,某个线程在60s内都无法获取到新的任务将会被销毁。

线程池的execute函数接收Runnable的接口实现类作为参数,在该任务被执行时将会调用它的run()方法,这上面的AsyncCall也是一样的道理,它继承了NamedRunnable抽象类,而NamedRunnable又实现了Runnable接口,当NamedRunnablerun()方法被回调时,会调用AsyncCallexecute()方法。

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    //responseCallback 就是调用 call.enqueue 方法时传入的回调。
    AsyncCall(Callback responseCallback) {
        super("OkHttp %s", new Object[]{RealCall.this.redactedUrl()});
        this.responseCallback = responseCallback;
    }
        
    //该函数是在子线程当中执行的。
    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 {
                        //调用 Dispatcher 的 finished 方法
            RealCall.this.client.dispatcher().finished(this);
        }

    }
}

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 {
            //调用子类的 execute() 方法。
            this.execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }

    }
    protected abstract void execute();
}

execute()方法最终是在 子线程当中执行的,这里我们看到了熟悉的一句话:

Response response = RealCall.this.getResponseWithInterceptorChain();

这里面就是进行请求的核心逻辑,我们在 OkHttp 知识梳理(1) - OkHttp 源码解析之入门 中的3.4节中已经介绍过了,这里会通过一系列的拦截器进行处理,重试请求、缓存处理和网络请求都是在里面完成的,最终得到返回的Response,并根据情况回调最开始传入的CallbackonResponse/onFailure方法。

2.2 任务执行完后的处理

当回调完之后,最终会调用Dispatcherfinished方法:

    void finished(AsyncCall call) {
        //如果是异步请求,那么最后一个参数为 true。
        this.finished(this.runningAsyncCalls, call, true);
    }

    void finished(RealCall call) {
        //如果是同步请求,那么最后一个参数为 false。
        this.finished(this.runningSyncCalls, call, 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) {
                this.promoteCalls();
            }
            runningCallsCount = this.runningCallsCount();
            idleCallback = this.idleCallback;
        }
        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }
    }

    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);

            }
        }
    }

这里和同步请求相同,都会走到finished方法当中,区别在于最后一次参数是true,而同步请求是false,也就是说会调用到promoteCalls方法中,promoteCalls的作用为:在最开始时,如果不满足执行条件,那么任务将会被加入到等待队列readyAsyncCalls中,那么当一个任务执行完之后,就需要去等待队列中寻找符合执行条件的任务,并将它加入到任务队列中执行,之后的逻辑和前面的相同。

promoteCalls函数除了在一个异步请求执行完毕后会调用,当我们改变最大请求数量和对于同一个host的最大请求数量时,也会触发该查找过程。

    //改变了最大请求数量。
    public synchronized void setMaxRequests(int maxRequests) {
        if(maxRequests < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequests);
        } else {
            this.maxRequests = maxRequests;
            this.promoteCalls();
        }
    }
    //改变了同一个 Host 的最大请求数量。
    public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
        if(maxRequestsPerHost < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
        } else {
            this.maxRequestsPerHost = maxRequestsPerHost;
            this.promoteCalls();
        }
    }

三、小结

以上就是对于异步请求方式的源码分析,由此我们可以总结出OkHttp对于异步请求的调度方式:

  • 通过两个列表对异步请求任务进行管理,runningAsyncCalls中存放的是正在执行任务的列表,readyAsyncCalls中则是等待被执行的任务。
  • 当一个任务执行完毕后,会去readyAsyncCalls查找下一个可以被执行的任务。
  • 任务的执行是通过线程池ThreadPoolExecutor在子线程中来完成的,由它来负责正在执行任务的调度,内部的实现原理如 多线程知识梳理(6) - 线程池四部曲之 ThreadPoolExecutor 所分析。
  • 真正进行网络请求的核心代码在AsyncCallexecute()函数中,这里会通过一系列的拦截器进行处理,重试请求、缓存处理和网络请求都是在里面完成的,最终得到返回的Response,并根据情况回调最开始传入的CallbackonResponse/onFailure方法。
  • 对于异步请求而言,CallbackonResponse/onFailed是在子线程当中执行的,因此如果要在其中执行更新UI的操作,那么需要通知主线程来更新。

更多文章,欢迎访问我的 Android 知识梳理系列:

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