「Android 路线」| OkHttp 分发器

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文「Android 路线」| 导读 —— 从零到无穷大 已收录。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 网络请求是 App 中非常重要的一个组件,而 OkHttp 作为官方和业界双重认可的解决方案,其学习价值不必多言;
  • 在这篇文章里,我将分析 OkHttp 分发器 & 拦截器 的实现原理。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


1. 前置知识

Editting...


2. OkHttp 简介

OkHttp 是 Square 开源的网络请求框架,自从 Android 4.4 移除 HttpURLConnection 后,逐渐演变成 Android 端最主流的网络请求框架。

2.1 优点

  • 1、支持 Http1、Http2、Quic 以及 WebSocket 等多种应用层协议;
  • 2、连接池复用底层 TCP 连接(Socket),降低了请求时延;
  • 3、无缝支持 GZIP 减少数据流量;
  • 4、缓存响应数据,减少了重复网络请求;
  • 5、自动失败重试、自动重定向。

2.2 使用流程

一次 OkHttp 请求过程最少需要用到 OkHttpClient、Request、Call、Response,例如:

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
    .url(url)
    .build();

同步请求
Call call = client.newCall(request);
获得响应
Response response = call.execute();

异步请求
Call call = client.newCall(request);
call.enqueue(new Callback(){

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

    @Override
    public void onResponse(Call call, Response response) {
        获得响应
    }

});

需要注意的是,Call 是一个接口,OkHttpClient#newCall(...)返回的其实是它的实现类 RealCall:

OkHttpClient.java

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

RealCall.java

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

3. Dispatcher 分发器

Call#execute() & Call#enqueue(...) 分别表示调用同步请求和异步请求,客户端对请求任务的调度过程是不感知的。这是因为 OkHttp 内部的 「Dispatcher 分发器」 封装了整个请求任务的调度过程,这一节我们来具体分析下。

3.1 自定义 Dispatcher 分发器

Dispatcher 主要成员变量如下:

Dispatcher.java

异步请求最大并发数
private int maxRequests = 64;

同一域名请求最大并发数
private int maxRequestsPerHost = 5;

闲置任务(没有请求时执行)
private Runnable idleCallback;

线程池
private ExecutorService executorService;

异步请求等待队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();

异步请求执行队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();

同步请求执行队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque();

其中,以下几个变量是允许自定义的:

变量 描述 默认值
maxRequests 异步请求最大并发数 64
maxRequestsPerHost 同一域名请求最大并发数 5
idleCallback 闲置任务 null
executorService 线程池 无等待,最大并发

提示: 限制请求数是为了避免客户端和服务器的负载过大(Linux 一切皆文件,Socket 占用文件文件句柄,打开文件数是有限制的),至于为什么默认值为 64 和 5,据说 OkHttp 是参考了主流浏览器得出的经验值,未查及论据。

自定义的 Dispatcher 对象通过 OkHttpClient.Builder 构建者设置:

OkHttpClient.java

public OkHttpClient.Builder dispatcher(Dispatcher dispatcher) {
    if (dispatcher == null) {
        throw new IllegalArgumentException("dispatcher == null");
    } else {
        this.dispatcher = dispatcher;
        return this;
    }
}

3.2 同步请求

这一节我们来分析 OkHttp 同步请求的执行过程:

RealCall.java

已简化
public Response execute() throws IOException {
    1、禁止重复执行
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }
        this.executed = true;
    }

    2、记录
    this.client.dispatcher().executed(this);
    3、执行请求,阻塞等待响应返回
    Response result = this.getResponseWithInterceptorChain();
    4、移除记录
    this.client.dispatcher().finished(this);

    return result;
}

Dispatcher.java

记录到 runningSyncCalls 中
synchronized void executed(RealCall call) {
    this.runningSyncCalls.add(call);
}

从 runningSyncCalls 中移除
void finished(RealCall call) {
    内部逻辑见 第 3.3 节
    finished(runningSyncCalls, call);
}

同步请求是在当前线程阻塞执行,所以不需要 Dispatcher 调度任务,Dispatcher 内部也仅仅是通过 runningSyncCalls 记录同步请求。

同时可以看到,RealCall#getResponseWithInterceptorChain() 是真正执行请求的地方,我一并在 「Android 路线」| OkHttp 拦截器 中分析。

3.3 异步请求

这一节我们来分析 OkHttp 异步请求的执行过程:

RealCall.java

已简化
public void enqueue(Callback responseCallback) {
    1、禁止重复执行
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }
        this.executed = true;
    }
    2、调用分发器
    this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}

OkHttp 异步请求就需要 Dispatcher 登场了,我们来看看:

Dispatcher.java

void enqueue(AsyncCall call) {
    synchronized (this) {
        2.1 加入 readyAsyncCalls
        readyAsyncCalls.add(call);

        if (!call.get().forWebSocket) {
            2.2 复用同一个计数器(记录同一域名异步请求数)
            AsyncCall existingCall = findExistingCallWithHost(call.host());
            if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
        }
    }
    2.3 触发或执行请求
    promoteAndExecute();
}

主要的逻辑应该在 Dispatcher#promoteAndExecute() 中:

2.3 触发或执行请求
private boolean promoteAndExecute() {
    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
        1、遍历 readyAsyncCalls 列表
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
            AsyncCall asyncCall = i.next();

            2、判断全部异步请求并发数是否超过限制
            if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
            3、判断同一域名异步请求并发数是否超过限制
            if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

            4、符合执行条件
            i.remove();
            4.1、同一域名异步请求数加一
            asyncCall.callsPerHost().incrementAndGet();
            4.2 可执行任务列表
            executableCalls.add(asyncCall);
            4.3 执行中任务列表
            runningAsyncCalls.add(asyncCall);
        }
        isRunning = runningCallsCount() > 0;
    }

    5 处理可执行任务列表
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
        AsyncCall asyncCall = executableCalls.get(i);
        执行请求
        asyncCall.executeOn(executorService());
    }

    6 返回是否正在执行请求(若无请求,则执行闲置任务 idleCallback)
    return isRunning;
}

可以看到,异步请求是存在限制的,只有 「全部异步请求并发数不超过限制」 & 「同一域名异步请求并发数不超过限制」,才允许执行,否则会停留在 readyAsyncCalls 中等待。

那么,谁来拯救在 readyAsyncCalls 中等待的请求呢?其实就是看什么时候会触发promoteAndExecute()方法了,除了 setMaxRequests(int)、setMaxRequestsPerHost(int)外,在异步请求完成后,让出 “空闲名额” 也会触发。

AsyncCall.java

5 处理可执行任务列表
void executeOn(ExecutorService executorService) {
    boolean success = false;
    try {
        5.1 线程池执行
        executorService.execute(this);
        success = true;
    } catch (RejectedExecutionException e) {
        5.2 线程池拒绝执行
        InterruptedIOException ioException = new InterruptedIOException("executor rejected");
        ioException.initCause(e);
        eventListener.callFailed(RealCall.this, ioException);
        responseCallback.onFailure(RealCall.this, ioException);
    } finally {
        5.3 finish
        if (!success) {
            client.dispatcher().finished(this); // This call is no longer running!
        }
    }
}

5.1 线程池执行(已简化)
protected void execute() {
    try {
        5.1.1 执行请求,阻塞等待响应返回
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
            5.1.2 请求取消
            responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
            5.1.3 请求成功
            responseCallback.onResponse(RealCall.this, response);
        }
    } catch (IOException e) {
        5.1.4 请求失败
        responseCallback.onFailure(RealCall.this, e);
    } finally {
        5.1.5 finish
        client.dispatcher().finished(this);
    }
}

这段代码不算复杂,AsyncCall 是 Runnable 的子类,Runnable#run()最终会走到AsyncCall#execute(),主要分为三步:

Dispatcher#finished() 我们在 第 3.2 节 同步请求里见过,现在我们来分析下:

Dispatcher.java

5.1.5 finish 或 5.3 finish
private <T> void finished(Deque<T> calls, T call) {
    Runnable idleCallback;
    synchronized (this) {
        1、从移除请求执行列表中移除
        if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
        idleCallback = this.idleCallback;
    }

    2、触发或执行请求
    boolean isRunning = promoteAndExecute();

    3、若无请求,则执行闲置任务 idleCallback
    if (!isRunning && idleCallback != null) {
        idleCallback.run();
    }
}

4. 分发器线程池

为什么不能使用ArrayBlockingQueue()
为什么不能使用LinkedBlockingQueue
为什么使用SynchronousQueue


5. 总结

看到这里,我们先来总结这篇文章的内容以及遇到的疑问:

  • 1、同步请求不需要 Dispatcher 任务调度,Dispatcher 只做记录;
  • 2 、异步请求有限制,被限制的请求会在 ready 队列中等待,直到有请求完成让出 “空闲名额” 才会触发执行;

在前面关于同步请求和异步请求的分析中,我们都提到了 RealCall#getResponseWithInterceptorChain() 是真正执行请求的地方,我在 「Android 路线」| OkHttp 拦截器 里讨论。


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

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

推荐阅读更多精彩内容