OkHttp原理解析

OkHttp原理解析

OkHttp 3.10.0版本,最新OkHttp为:4.0.1逻辑与3版本并没有太大变化,但是改为kotlin实现。

OkHttp介绍

OkHttp是当下Android使用最频繁的网络请求框架,由Square公司开源。Google在Android4.4以后开始将源码中的HttpURLConnection底层实现替换为OKHttp,同时现在流行的Retrofit框架底层同样是使用OKHttp的。

优点:

  • 支持Spdy、Http1.X、Http2、Quic以及WebSocket
  • 连接池复用底层TCP(Socket),减少请求延时
  • 无缝的支持GZIP减少数据流量
  • 缓存响应数据减少重复的网络请求
  • 请求失败自动重试主机的其他ip,自动重定向
  • …….

使用流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsGUk0us-1591774910843)(img/使用流程.png)]

在使用OkHttp发起一次请求时,对于使用者最少存在OkHttpClientRequestCall三个角色。其中OkHttpClientRequest的创建可以使用它为我们提供的Builder(建造者模式)。而Call则是把Request交给OkHttpClient之后返回的一个已准备好执行的请求。

建造者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。实例化OKHttpClient和Request的时候,因为有太多的属性需要设置,而且开发者的需求组合千变万化,使用建造者模式可以让用户不需要关心这个类的内部细节,配置好后,建造者会帮助我们按部就班的初始化表示对象

同时OkHttp在设计时采用的门面模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

OkHttpClient中全是一些配置,比如代理的配置、ssl证书的配置等。而Call本身是一个接口,我们获得的实现为:RealCall

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
   
   
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
}

Callexecute代表了同步请求,而enqueue则代表异步请求。两者唯一区别在于一个会直接发起网络请求,而另一个使用OkHttp内置的线程池来进行。这就涉及到OkHttp的任务分发器。

分发器

Dispatcher,分发器就是来调配请求任务的,内部会包含一个线程池。可以在创建OkHttpClient时,传递我们自己定义的线程池来创建分发器。

这个Dispatcher中的成员有:

//异步请求同时存在的最大请求
private int maxRequests = 64;
//异步请求同一域名同时存在的最大请求
private int maxRequestsPerHost = 5;
//闲置任务(没有请求时可执行一些任务,由使用者设置)
private @Nullable Runnable idleCallback;

//异步请求使用的线程池
private @Nullable ExecutorService executorService;

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

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

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

同步请求

synchronized void executed(RealCall call) {
   
   
	runningSyncCalls.add(call);
}

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。

异步请求

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

当正在执行的任务未超过最大限制64,同时runningCallsForHost(call) < maxRequestsPerHost同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

加入线程池直接执行没啥好说的,但是如果加入等待队列后,就需要等待有空闲名额才开始执行。因此每次执行完一个请求后,都会调用分发器的finished方法

//异步请求调用
void finished(AsyncCall call) {
   
   
	finished(runningAsyncCalls, call, true);
}
//同步请求调用
void finished(RealCall call) {
   
   
	finished(runningSyncCalls, call, false);
}

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
   
   
	int runningCallsCount;
	Runnable idleCallback;
	synchronized (this) {
   
   
        //不管异步还是同步,执行完后都要从队列移除(runningSyncCalls/runningAsyncCalls)
		if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
		if (promoteCalls) promoteCalls();
        //异步任务和同步任务正在执行的和
		runningCallsCount = runningCallsCount();
		idleCallback = this.idleCallback;
	}
	// 没有任务执行执行闲置任务
	if (runningCallsCount == 0 && idleCallback != null) {
   
   
		idleCallback.run();
	}
}

需要注意的是 只有异步任务才会存在限制与等待,所以在执行完了移除正在执行队列中的元素后,异步任务结束会执行promoteCalls()。很显然这个方法肯定会重新调配请求。

private void promoteCalls() {
   
   
    //如果任务满了直接返回
	if (runningAsyncCalls.size() >= maxRequests) return; 
    //没有等待执行的任务,返回
	if (readyAsyncCalls.isEmpty()) return; 
    //遍历等待执行队列
	for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
   
   
		AsyncCall call = i.next();
        //等待任务想要执行,还需要满足:这个等待任务请求的Host不能已经存在5个了
		if (runningCallsForHost(call) < maxRequestsPerHost) {
   
   
			i.remove();
			runningAsyncCalls.add(call);
			executorService().execute(call);
		}

		if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
	}
}

在满足条件下,会把等待队列中的任务移动到runningAsyncCalls并交给线程池执行。所以分发器到这里就完了。逻辑上还是非常简单的。

请求流程

用户是不需要直接操作任务分发器的,获得的RealCall 中就分别提供了executeenqueue来开始同步请求或异步请求。

@Override public Response execute() throws IOException {
   
   
    synchronized (this) {
   
   
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
   
   
      //调用分发器
      client.dispatcher().executed(this);
      //执行请求
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
   
   
      eventListener.callFailed(this, e);
      throw e;
    } finally {
   
   
      //请求完成
      client.dispatcher().finished(this);
    }
}

异步请求的后续同时是调用getResponseWithInterceptorChain()来执行请求

@Override
public void enqueue(Callback responseCallback) {
   
   
	synchronized (this) {
   
   
		if (executed) throw new IllegalStateException("Already Executed");
		executed = true;
	}
	captureCallStackTrace();
	eventListener.callStart(this);
    //调用分发器
	client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

如果该RealCall已经执行过了,再次执行是不允许的。异步请求会把一个AsyncCall提交给分发器。

AsyncCall实际上是一个Runnable的子类,使用线程启动一个Runnable时会执行run方法,在AsyncCall中被重定向到execute方法:

final class AsyncCall extends NamedRunnable {
   
   
	private final Callback responseCallback;

	AsyncCall(Callback responseCallback) {
   
   
		super("OkHttp %s", redactedUrl());
		this.responseCallback = responseCallback;
	}

    //线程池执行
	@Override
	protected void execute() {
   
   
	 boolean signalledCallback = false;
      try {
   
   
        Response response = getResponseWithInterceptorChain();
       //.......
      } catch (IOException e) {
   
   
       //......
      } finally {
   
   
        //请求完成
        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);
    }

    @Override
    public final void run() {
   
   
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(name);
        try {
   
   
            execute();
        } finally {
   
   
            Thread.currentThread().setName(oldName);
        }
    }

    protected abstract void execute();
}

同时AsyncCall也是RealCall的普通内部类,这意味着它是持有外部类RealCall的引用,可以获得直接调用外部类的方法。

可以看到无论是同步还是异步请求实际上真正执行请求的工作都在getResponseWithInterceptorChain()中。这个方法就是整个OkHttp的核心:拦截器责任链。但是在介绍责任链之前,我们再来回顾一下线程池的基础知识。

分发器线程池

前面我们提过,分发器就是来调配请求任务的,内部会包含一个线程池。当异步请求时,会将请求任务交给线程池来执行。那分发器中默认的线程池是如何定义的呢?为什么要这么定义?

public synchronized ExecutorService executorService() {
   
   
    if (executorService == null) {
   
   
      executorService = new ThreadPoolExecutor(
          					0,   				//核心线程
                            Integer.MAX_VALUE,  //最大线程
                            60,					//空闲线程闲置时间
                            TimeUnit.SECONDS,	//闲置时间单位
                            new SynchronousQueue<Runnable>(), //线程等待队列
                            Util.threadFactory("OkHttp Dispatcher", false) //线程创建工厂
      );
    }
    return executorService;
}

在OkHttp的分发器中的线程池定义如上,其实就和Executors.newCachedThreadPool()创建的线程一样。首先核心线程为0,表示线程池不会一直为我们缓存线程,线程池中所有线程都是在60s内没有工作就会被回收。而最大线程Integer.MAX_VALUE与等待队列SynchronousQueue的组合能够得到最大的吞吐量。即当需要线程池执行任务时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。一般来说,等待队列BlockingQueue有:ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue

假设向线程池提交任务时,核心线程都被占用的情况下:

ArrayBlockingQueue:基于数组的阻塞队列,初始化需要指定固定大小。

​ 当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入队列就会失败,这时就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。所以最终可能出现后提交的任务先执行,而先提交的任务一直在等待。

LinkedBlockingQueue:基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。

​ 当指定大小后,行为就和ArrayBlockingQueu一致。而如果未指定大小,则会使用默认的Integer.MAX_VALUE作为队列大小。这时候就会出现线程池的最大线程数参数无用,因为无论如何,向线程池提交任务加入等待队列都会成功。最终意味着所有任务都是在核心线程执行。如果核心线程一直被占,那就一直等待。

SynchronousQueue : 无容量的队列。

​ 使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合Integer.MAX_VALUE就实现了真正的无等待。

但是需要注意的时,我们都知道,进程的内存是存在限制的,而每一个线程都需要分配一定的内存。所以线程并不能无限个数。那么当设置最大线程数为Integer.MAX_VALUE时,OkHttp同时还有最大请求任务执行个数: 64的限制。这样即解决了这个问题同时也能获得最大吞吐。

拦截器责任链

OkHttp最核心的工作是在getResponseWithInterceptorChain()中进行,在进入这个方法分析之前,我们先来了解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。

责任链模式

为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。比如:

七夕节刚过去。周周同学(我也不知道为什么第一个想到的就是周周同学)在读书的时候就是单身狗一条,看到自习室每天都很多美女后,每天晚上跑去自习都干同一件事情。

周周每天晚上都坐到自习室最后一排,找张纸条写上:“Hi,可以做我的女朋友吗?我的特长就是特别的长,如果不愿意请向前传”。纸条就一个接一个的传上去了,最后传给了扫地阿姨。最后和扫地阿姨过上了幸福的生活,这真是一个…令人高兴的故事。

那整个过程是什么样子的呢?

//传送者
abstract class Transmit{
   
   
    //责任链中下一个传递者
    protected Transmit nextTransmit;
    
    boolean request(String msg);
    
    public void setNextTransmit(Transmit transmit){
   
   
        nextTransmit = transmit;
    }
}

public class Zero extends Transmit{
   
   
    public boolean request(String msg){
   
   
        System.out.println("Zero接到纸条,会心一笑");
        boolean resp = nextTransmit.request(msg); 
        return resp;
    }
}

public class Alvin extends Transmit{
   
   
    public boolean request(String msg){
   
   
        System.out.println("Alvin接到纸条,伤心欲绝”);
        boolean resp = nextTransmit.request(); 
        return resp;
    }
}

public class Lucy extends Transmit{
   
   
    public boolean
OkHttp 是一个广泛应用于 Android 开发中的网络请求框架,其设计精巧、性能优越,内部实现涉及多个核心组件和机制。以下是对 OkHttp 的源码分及其工作原理的详细解。 ### 核心类与架构 OkHttp 的架构由多个关键类组成,包括 `OkHttpClient`、`Request`、`Call`、`Dispatcher`、`ConnectionPool` 等。这些类协同工作,确保网络请求的高效执行。 - **OkHttpClient**:作为客户端的入口点,负责创建和管理 `Call` 对象。它通过建造者模式构建,允许用户自定义配置,如超时时间、拦截器链等。 - **Request**:表示 HTTP 请求,包含 URL、方法、头部信息等。 - **Call**:代表一次具体的网络请求,`RealCall` 是其实现类,负责实际的请求处理。 - **Dispatcher**:任务调度器,管理异步请求的执行,使用线程池来处理并发请求[^4]。 - **ConnectionPool**:连接池,用于复用 HTTP 连接,减少建立新连接的时间开销。默认情况下,OkHttp 使用一个清理线程池定期检查并移除空闲连接[^1]。 ### 请求流程 OkHttp 的请求流程可以分为以下几个步骤: 1. **构建请求**:通过 `OkHttpClient.Builder` 构建 `OkHttpClient` 实例,并设置相关参数。接着,使用 `Request.Builder` 创建 `Request` 对象,指定 URL、方法、头部等信息。 2. **发起请求**:调用 `OkHttpClient.newCall(request)` 创建 `Call` 对象,然后根据需求选择同步或异步请求方式: - **同步请求**:直接调用 `execute()` 方法,阻塞当前线程直到响应返回。 - **异步请求**:调用 `enqueue(Callback responseCallback)` 方法,将请求加入队列并在后台线程中执行,完成后回调 `onResponse` 或 `onFailure`。 3. **拦截器链**:无论是同步还是异步请求,最终都会调用 `getResponseWithInterceptorChain()` 方法,从拦截器链 `interceptors` 中获取结果。拦截器链包括重试、桥接、缓存、连接、网络五个阶段,每个阶段都可以对请求和响应进行处理[^4]。 ### 缓存机制 OkHttp 提供了内置的缓存支持,可以通过 `Cache` 类设置缓存目录和大小。当启用缓存后,OkHttp 会自动存储和读取响应数据,从而减少不必要的网络请求,提高应用性能[^1]。 ### 协议支持 OkHttp 支持多种协议,包括 HTTP/1.0、HTTP/1.1 和 HTTP/2。`protocols` 字段定义了 OkHttp 支持的协议列表,默认情况下,OkHttp 优先使用 HTTP/2。通过 `java.net.URI.getScheme()` 可以获取 URL 的协议(如 http、https),而 `Protocol.get()` 则提供了更具体的协议版本信息(如 http/1.0、http/1.1、h2)[^5]。 ### 示例代码 以下是一个简单的 OkHttp 同步请求示例: ```java OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://api.example.com/data") .build(); try { Response response = client.newCall(request).execute(); if (response.isSuccessful()) { System.out.println(response.body().string()); } } catch (IOException e) { e.printStackTrace(); } ``` 对于异步请求,可以使用如下代码: ```java OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://api.example.com/data") .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { System.out.println(response.body().string()); } } }); ``` ### 总结 OkHttp 以其高效的连接管理和灵活的拦截器机制,成为 Android 开发中最受欢迎的网络请求框架之一。通过对 `Dispatcher`、`ConnectionPool`、`Cache` 等核心组件的理解,开发者可以更好地优化网络请求,提升应用性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值