当前位置: 首页 > news >正文

【学习笔记】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请求为例:

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() {@Overridepublic void onFailure(Call call, Throwable throwable) {}@Overridepublic void onResponse(Call call, Response response) {}
});

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

public class Request {// 作为一个请求,需要有以下功能// 请求方法,是 get 还是 postprivate 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作为参数:

public class Call {Request request;public Call(Request request) {this.request = request;}public Call enqueue(Callback callback) {return this;}
}

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

public class OkHttpClient {public Call newCall(Request request) {return new Call(this, request);}
}

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

public interface Callback {void onFailure(Call call, Throwable throwable);void onResponse(Call call, Response response);
}

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

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() {@Overridepublic 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 去持有:

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;}@Overridepublic void run() {// 到了这里还不能直接执行请求,开始涉及到拦截器// 设计责任链模式,首先要创建一个接口// 为了能在一环又一环的链条上传递数据,还需要一个指挥者// 怎么组装责任链try {Response response = getResponse();// 因为是异步,所以通过接口返回mCallback.onResponse(Call.this, response);} catch (IOException e) {mCallback.onFailure(Call.this, e);}}}
}

回顾整个请求流程步骤,线程池分配线程去执行Call里面的请求任务AsyncCall ,接下来就是去组装责任链,并启动责任链程序。
关于责任链这里,我们都知道okhttp设计各种拦截器的主要原因,一个是为了把重试、请求头、获取长链接以及真正请求的逻辑去做一个解耦,另一个原因也是为了让开发者可以自定义增加或者删除拦截器。
这里我们简单实现一下okhttp的责任链,首先责任链会涉及到一个指挥者,我们需要定义一个接口,并定义一个拦截的方法intercept:

public interface Interceptor {Response intercept(InterceptorChain interceptorChain) throws IOException;
}

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

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()方法就能执行下一个责任链的逻辑:

public class RetryInterceptor implements Interceptor {// 重试责任链@Overridepublic 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 {// 获取长链接责任链@Overridepublic Response intercept(InterceptorChain interceptorChain) throws IOException {// ...省略部分代码Response proceed = interceptorChain.proceed();return proceed;}
}public class CallServiceInterceptor implements Interceptor {// 请求处理责任链条@Overridepublic Response intercept(InterceptorChain interceptorChain) throws IOException {// ...省略部分代码Response proceed = interceptorChain.proceed();return proceed;}
}
http://www.dtcms.com/a/270508.html

相关文章:

  • 保姆级安装 Ruby 环境下载及安装教程, RubyInstaller下载及安装教程
  • Javaweb - 10.7 乱码和路径问题
  • 影石(insta360)X4运动相机视频删除的恢复方法
  • SHA-256算法详解——Github工程结合示例和动画演示
  • 中望CAD2026亮点速递(5):【相似查找】高效自动化识别定位
  • Python(30)基于itertools生成器的量子计算模拟技术深度解析
  • 【SQL】使用UPDATE修改表字段的时候,遇到1054 或者1064的问题怎么办?
  • (八)PS识别:使用 Python 自动化生成图像PS数据集
  • Linux驱动05 --- TCP 服务器
  • 分库分表之实战-sharding-JDBC绑定表配置实战
  • uniapp+vue3+ts项目:实现小程序文件下载、预览、进度监听(含项目、案例、插件)
  • PostgreSQL如何进行跨服务器迁移数据
  • ARIA UWB安全雷达主要产品型号与核心功能全解析
  • 【数字后端】- Standard Cell Status
  • 亚马逊广告进阶指南:CPC与竞价的底层逻辑
  • 游戏开发学习记录
  • 基于Flask 3.1和Python 3.13的简易CMS
  • LLM中 最后一个词语的表征(隐藏状态)通常会融合前面所有词语的信息吗?
  • Java项目集成Log4j2全攻略
  • 速卖通跨境运营破局:亚矩阵云手机如何用“本地化黑科技”撬动俄罗斯市场25%客单价增长
  • 今日行情明日机会——20250709
  • 伪装计算器软件,隐藏手机隐私文件
  • 3.常⽤控件
  • jmeter做跨线程组
  • 第二章:创建登录页面
  • 函数-3-日期函数
  • Java垃圾收集机制Test1
  • css 设置 input 插入光标样式
  • OpenCV图片操作100例:从入门到精通指南(2)
  • java17 gc笔记