【学习笔记】OkHttp源码架构解析:从设计模式到核心实现

我们先回顾一下OkHttp请求流程的一个通俗易懂的故事版本:

1. 挂号处(Dispatcher)

  • 你(Request):来到医院说要体检(比如「全身检查」)。
  • 护士姐姐(Call):带你到挂号处分诊。
  • 挂号处(Dispatcher)
    • 如果当前体检科人不多(runningAsyncCalls < 64),直接给你排号(加入 runningAsyncCalls)。
    • 如果人满了(比如流感季),让你去等候区坐着(readyAsyncCalls队列)。

💡 关键:挂号处控制整体人流,防止体检科被挤爆(并发限制)。


2. 体检科线程池(Thread Pool)

  • 护士姐姐 :从挂号处拿到你的号后,不亲自带你体检 ,而是交给体检科的空闲医生(线程池中的线程)。
  • 医生(线程) :按照体检流程单(Interceptor Chain)一步步带你检查。

🌟 为什么需要医生?

护士姐姐如果亲自带你体检,她就没法服务其他人了(阻塞调用线程)。医生是专门干这事的(后台线程)。


3. 体检流程单(责任链 Interceptor Chain)

医生拿着流程单,带你去不同科室:

  1. 预检分诊台(RetryAndFollowUpInterceptor)

    • 先问你是否过敏(检查请求是否可重试)。
    • 如果血常规人太多(超时),带你去其他楼层重试(自动重试)。
  2. 登记处(BridgeInterceptor)

    • 帮你填表、补全个人信息(补全请求头:Content-TypeCookie)。
  3. 档案室(CacheInterceptor)

    • 检查你去年体检过没,如果项目相同且没过期,直接复印一份给你(返回缓存响应)。
  4. 抽血处(ConnectInterceptor)

    • 找护士长(连接池)问有没有一次性针管(Socket 连接),有就直接用(连接复用),没有就拿一个新的。
  5. X光室(CallServerInterceptor)

    • 终于真正做检查了(发送请求到服务器,接收响应)。

4. 体检报告(Response)

  • 医生 :拿到所有科室结果后,整理成报告(Response),交还给护士姐姐
  • 护士姐姐
    • 如果体检成功,打电话通知你(onResponse)。
    • 如果中途医院停电了(IOException),告诉你改天再来(onFailure)。

5. 突发情况(取消与重试)

  • 你不想体检了(call.cancel())
    护士姐姐立刻广播通知所有科室停止检查(中断请求)。
  • 某项检查失败但可重试
    预检分诊台(RetryAndFollowUpInterceptor)会带你重新排队(自动重试),但最多重试 20 次(避免无限循环)。
  • 需要特殊重试
    比如体检要求空腹,但你吃了早饭,护士姐姐会记下需求(自定义拦截器),明天再来(手动重试)。

在了解OkHttp通俗易懂的故事之后,现在我们可以尝试写出源码大致版本,去更好的理解OkHttp整个架构,下面我们回顾它的基本使用,以GET请求为例:

java 复制代码
String url = "https://www.baidu.com/";

OkHttpClient client = new OkHttpClient();
// 配置GET请求
Request request = new Request.Builder()
        .url(url)
        .get()
        .build();

Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, Throwable throwable) {

    }

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

    }
});

下面我们将自顶向下的一步一步写出大概的源码,不难看出我们需要新建OkHttpClient 、Request 、Call 以及Callback这些基本的类,而且Request 还用到了构建者设计模式,它的好处是可以像链式调用一下,在代码中其实就是方法体中执行完相关逻辑之后返回类本身就能够实现;首先我们先实现Request :

java 复制代码
public class Request {
    // 作为一个请求,需要有以下功能
    // 请求方法,是 get 还是 post
    private String method = "GET";

    // 请求到哪里去,url地址;
    // HttpUrl通过url解析服务器主机名以及端口号之类的功能
    private HttpUrl httpUrl = null;

	// ...省略部分代码
    
    public static class Builder {
        public Builder url(String url) {
            return this;
        }

        public Builder get() {
            return this;
        }

        public Request build() {
            return new Request();
        }
    }
}

接着是实现Call,前面我们知道,Call将全程参与请求完整流程,则Call肯定是持有Request对象,然后enqueue方法会有一个callback作为参数:

java 复制代码
public class Call {
    Request request;
    public Call(Request request) {
        this.request = request;
    }

    public Call enqueue(Callback callback) {
        return this;
    }
}

接着是OkHttpClient,从使用方式来看,我们一定知道里面有一个newCall方法,执行完成之后会返回Call对象:

java 复制代码
public class OkHttpClient {
    public Call newCall(Request request) {
        return new Call(this, request);
    }
}

以及Callback接口,可以发现不论是成功还是失败,call对象都是全程参与,作用是支持开发者能够对网络请求的完全控制能力,比如可以在响应之后随时取消请求:

java 复制代码
public interface Callback {
    void onFailure(Call call, Throwable throwable);
    void onResponse(Call call, Response response);
}

至此我们看到的方法大致已经实现完了,现在我们根据已知的信息把okhttp黑盒部分的源码实现。我们都知道:call带着request前往分发器dispatcher,分发器需要看情况这个请求要到哪个队列,比如当正在请求的队列满了就需要去等待队列,不难得出我们需要写出一个Dispatcher,并且至少维护着两个队列,且这两个队列是非阻塞队列;由于请求是耗时任务,不能直接执行(卡主线程),需要利用线程池特性去帮我们完成请求流程,所以会用到ExecutorService ,另外也维护着两个大家都知道的成员变量maxRequests和maxRequestPreHost:

java 复制代码
public class Dispatcher {
    // 请求来了不一定能够执行,所以需要分发器;
    // Dispatcher 不能是 Call持有,不然护士姐姐的权利就是最大了,分分钟排到最前面

    // 异步请求下,分发器的分发方法,会涉及到两个队列,一个是正在请求的队列,另一个是等待队列
    // 现在我们知道队列里放的是请求,但能不能直接放 request进去呢?
    // 因为 request只记录的请求信息,至于要怎么做,并不是在 request里面定义。
    // 另外请求动作是一个耗时任务,所以这个请求动作是实现了 runnable的

    // 可以思考一下为什么不用阻塞队列
    private final Deque<Call.AsyncCall> runningAsyncCalls = new ArrayDeque<>();
    private final Deque<Call.AsyncCall> readyAsyncCalls = new ArrayDeque<>();

	// 分发器一共维护着3个队列,前面2个是异步的队列,剩下这个是同步队列,也需要被记录着
	// 当请求是同步的时候会被分发器加入到这里
  	// private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

    // 最多同时请求的数量
    private int maxRequests;

    // 同一个 host主机最多允许请求的数量
    private int maxRequestPreHost;

    private ExecutorService mExecutorService;

    public Dispatcher() {
        maxRequests = 64;
        maxRequestPreHost = 5;
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread httpClientThread = new Thread(r, "http client thread");
                return httpClientThread;
            }
        };
        // okhttp是没有常驻线程的
        mExecutorService = new ThreadPoolExecutor(0,Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(1), threadFactory);
    }

    // 分发器需要拿到 request,分发到具体的队列,然后 Call.AsyncCall真正的执行
    public void enqueue(Call.AsyncCall asyncCall) {
        // 判断放在哪个队列
        if (runningAsyncCalls.size() < maxRequests) {
            runningAsyncCalls.add(asyncCall);
            // 添加到队列之后,并不能执行。还需要线程池
            mExecutorService.submit(asyncCall);
        } else {
            // 所以等待队列理论上大小是无限的
            readyAsyncCalls.add(asyncCall);
        }
    }
}

然而现在我们知道队列里放的是请求,但能不能直接放request进去呢?很显然不能,因为 request只记录的请求信息,至于要怎么做,并不是在 request里面定义。另外请求动作是一个耗时任务,所以这个请求动作是实现了 runnable的,故我们还要在Call方法里面定义一个内部类并实现runnable;

前面有提到分发器不能是call持有,因为call作为护士姐姐小角色,不能越级去干超出她能力范围的事情,而是应该交给更大能力的,这里很显然是OkHttpClient 去持有:

java 复制代码
public class Call {
	// ...省略部分代码

	private OkHttpClient mOkHttpClient;

    public Call enqueue(Callback callback) {
        synchronized (this) {
            if (executed) {
                throw new IllegalStateException("Already Executed!");
            }
            executed = true;
        }

        // Call 给到分发器
        mOkHttpClient.getDispatcher().enqueue(new AsyncCall(callback));

        return this;
    }
	
    final class AsyncCall implements Runnable {

        private Callback mCallback;

        public AsyncCall(Callback callback) {
            mCallback = callback;
        }

        @Override
        public void run() {
            // 到了这里还不能直接执行请求,开始涉及到拦截器
            // 设计责任链模式,首先要创建一个接口
            // 为了能在一环又一环的链条上传递数据,还需要一个指挥者

            // 怎么组装责任链
            try {
                Response response = getResponse();
                // 因为是异步,所以通过接口返回
                mCallback.onResponse(Call.this, response);
            } catch (IOException e) {
                mCallback.onFailure(Call.this, e);
            }

        }
    }
}

回顾整个请求流程步骤,线程池分配线程去执行Call里面的请求任务AsyncCall ,接下来就是去组装责任链,并启动责任链程序。

关于责任链这里,我们都知道okhttp设计各种拦截器的主要原因,一个是为了把重试、请求头、获取长链接以及真正请求的逻辑去做一个解耦,另一个原因也是为了让开发者可以自定义增加或者删除拦截器。

这里我们简单实现一下okhttp的责任链,首先责任链会涉及到一个指挥者,我们需要定义一个接口,并定义一个拦截的方法intercept:

java 复制代码
public interface Interceptor {
    Response intercept(InterceptorChain interceptorChain) throws IOException;
}

接着是指挥者的实现,为了能够让指挥者能够每次都执行下一个责任链,这里记录了list和index下标,当每次执行完一个环节都会自增,这里直接呼应了okhttp源代码:

java 复制代码
public class InterceptorChain {

    // 指挥者需要持有这些环节
    private List<Interceptor> mInterceptors;

    // 记录执行到哪一个环
    private int index;

    private Call mCall;

    // 怎么组装责任链

    public InterceptorChain(List<Interceptor> list, int index, Call call) {
        mInterceptors = list;
        this.index = index;
        mCall = call;
    }

    public Response proceed() throws IOException {
        if (index > mInterceptors.size()) {
            // 判断是否超出
            throw new IOException("error");
        }

        // 获取当前执行的责任链
        // 最关键的地方,这个方法需要实现,当对应的责任链执行完之后要执行下一个责任链
        // 也就是每个责任链通过参数里面的指挥者,再次调用它自身的 proceed()方法
        InterceptorChain next = new InterceptorChain(mInterceptors, index + 1, mCall);

        Interceptor interceptor = mInterceptors.get(index);
        Response intercept = interceptor.intercept(next);

        return intercept;
    }
}

最后是各个责任链的实现,可以看到,当每个责任链都做完自己的事情后,通过chain参数再次调用proceed()方法就能执行下一个责任链的逻辑:

java 复制代码
public class RetryInterceptor implements Interceptor {
    // 重试责任链
    @Override
    public Response intercept(InterceptorChain interceptorChain) throws IOException {
        // ...省略部分代码
        Response proceed = interceptorChain.proceed();
        return proceed;
    }
}

public class HeadersInterceptor implements Interceptor {
	// 请求头责任链
    public Response intercept(InterceptorChain interceptorChain) throws IOException {
		// ...省略部分代码
        Response proceed = interceptorChain.proceed();
        return proceed;
    }
}

public class ConnectionInterceptor implements Interceptor {
    // 获取长链接责任链
    @Override
    public Response intercept(InterceptorChain interceptorChain) throws IOException {
    	// ...省略部分代码
        Response proceed = interceptorChain.proceed();
        return proceed;
    }
}

public class CallServiceInterceptor implements Interceptor {
    // 请求处理责任链条
    @Override
    public Response intercept(InterceptorChain interceptorChain) throws IOException {
    	// ...省略部分代码
        Response proceed = interceptorChain.proceed();
        return proceed;
    }
}
相关推荐
HuashuiMu花水木11 分钟前
PyTorch笔记3----------统计学相关函数
人工智能·pytorch·笔记
慕y2741 小时前
Java学习第二十四部分——JavaServer Faces (JSF)
java·开发语言·学习
WZF-Sang1 小时前
计算机网络基础——1
网络·c++·git·学习·计算机网络·智能路由器
茫忙然1 小时前
【WEB】Polar靶场 11-15题 详细笔记
笔记
楼田莉子3 小时前
数据学习之队列
c语言·开发语言·数据结构·学习·算法
hcvinh3 小时前
CANDENCE 17.4 进行元器件缓存更新
学习·缓存
小白杨树树3 小时前
【Redis】黑马点评笔记:使用redis解决各种分布式/并发问题
笔记
每次的天空4 小时前
Android-重学kotlin(协程源码第二阶段)新学习总结
android·学习·kotlin
优乐美香芋味好喝4 小时前
2025年7月8日学习笔记——模式识别与机器学习绪论
笔记·学习·机器学习
dragoooon346 小时前
C++——string的了解和使用
c语言·开发语言·c++·学习·学习方法