Android之OkHttp框架的分析

news2024/10/7 12:20:13

Okhttp是Android开发常用的一个网络请求框架,下面将按照自己的理解将okhttp分为三条主线进行分析。

文章目录

    • 使用方式
    • OkHttp第一条主线:请求发送到哪里去了?
    • OkHttp第二条主线:请求是如何被消费的?
    • OkHttp第三条主线:请求是如何被维护的?
    • 常见问题

使用方式

   OkHttpClient okHttpClient= new OkHttpClient.Builder().build();
        Request request=new Request.Builder().url("").build();
        Call call=okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                
            }

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

            }
        });

OkHttp第一条主线:请求发送到哪里去了?

首先先看call.enqueue方法,在call接口中

/**
   * Schedules the request to be executed at some point in the future.
   *
   * <p>The {@link OkHttpClient#dispatcher dispatcher} defines when the request will run: usually
   * immediately unless there are several other requests currently being executed.
   *
   * <p>This client will later call back {@code responseCallback} with either an HTTP response or a
   * failure exception.
   *
   * @throws IllegalStateException when the call has already been executed.
   */
  void enqueue(Callback responseCallback);

再看一下call接口的实现类

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

在进去看一下client.dispatcher().enqueue(new AsyncCall(responseCallback))方法

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

可以发现在dispatcher类中有两个队列,一个是runningSyncCalls,一个是readyAyncCalls,也就是运行中的队列和等待中的队列。
并且有判断条件,当运行中队列请求数小于64并且访问同一目标机器的数量小于5,将请求放入运行中的队列,不然放入等待队列。
但是你会发现一个问题,假设我们有84个请求,那么我们会将64条请求放入运行中队列,剩下的86-64条放到等待中的队列,那么等待中的队列要等到什么时候进行处理呢?如果两个队列中都有数据再来20条数据又要怎么办?这个时候我们再看第二条主线。

OkHttp第二条主线:请求是如何被消费的?

点进行看executorService().execute(call)

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

可以发现重点在与我们传入的call,回到


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

再观察AsyncCall

final class AsyncCall extends NamedRunnable{
    private final Callback responseCallback;

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

    String host() {
      return originalRequest.url().host();
    }

    Request request() {
      return originalRequest;
    }

    RealCall get() {
      return RealCall.this;
    }

    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

再点进去看一下继承类NamedRunnable

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

发现run()方法中会执行execute()方法,且这里的execute方法是抽象的
那么也就表示我们在执行 executorService().execute(call);的时候其实会调用AsyncCall的execute()方法。说明我们把请求放在运行中队列的时候会立马放入线程池执行。

 @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

这里的代码也就是代表是请求怎么访问服务器的。点进行看一下getResponseWithInterceptorChain,getResponseWithInterceptorChain是处理请求的方法。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

这一块运用到了责任链模式,假设客户端的请求到达服务端,中间有3个节点要求,需要满足个节点的要求才能到达服务器,如果你第1个节点需要验证的东西你通不过,那么就到达不了第2个节点,在第1个节点就被拦截了。使用这种设计模式的话,可以节省很多无用功,做到优化。
在这里插入图片描述
在这里插入图片描述

还有可以添加自己定义的拦截器,增加框架的拓展性。

OkHttp第三条主线:请求是如何被维护的?

    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

在execute()方法中点进去 client.dispatcher().finished(this);

  /** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }

再进来

  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) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

有个promoteCalls()方法

  private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

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

如果运行中的队列个数已经大于等于maxRequests64,则直接返回,如果等待中的队列为空,那么也直接返回。不然的话就去循环等待中的队列,把等待中的队列移除并将其放入运行中的队列,再和上次请求消费完一样,放入线程池进行处理。
说明了当某个请求被消费掉之后,会回来检查等待队列中是否有数据,如果有数据,则将数据从等待中移除,然后放入运行中,再直接交给线程池进行处理

常见问题

下面是几个常见的问题,如果有错误欢迎指正!

  1. 为什么运行中的队列最大是64
    因为okhttp在写源码的时候,大量参考了浏览器的源码 默认都是64,避免手机和服务器建立过多的线程造成内存溢出 ;而且这个数是可以改的
  2. 访问统一目标机器数量为什么是5
    这可能是出于对服务器压力的考虑。如果同一个手机对同一个服务器的请求个数太多,可能会影响服务器的性能和稳定性。这个数字也是OkHttp的开发者根据经验和实践设定的,没有一个确定的理由或标准。
  3. 为什么设计两个队列,能不能用一个队列
    等待队列(readyAsyncCalls):用于存放还未执行的异步请求,等待调度器分配线程执行
    运行队列(runningAsyncCalls):用于存放正在执行的异步请求,当运行中队列请求数小于64并且访问同一目标机器的数量小于5才能将请求放入运行中队列,限制最大并发数
    Dispatcher类负责调度和分发这两个队列中的请求,以及复用和管理线程池
    okhttp设计两个队列的原因是为了完成调度和复用,以及控制执行和分发,这样可以保证请求的顺序和效率,以及避免资源的浪费和冲突。一个队列可能无法满足这些需求。
  4. 队列为什么用Deque?能不能用arraylist?hashmap?普通数组行不行?
    Deque:双端队列,可以在头部和尾部进行数据的访问和增删。
    ArrayList:基于数组实现的列表,元素有序且可以重复,支持随机访问。
    HashMap:基于数组和键值对实现的映射,元素无序且键不能重复,支持根据键快速查找值。
    数组:一种基本的数据结构,元素有序且可以重复,支持根据下标快速访问。
    队列为什么用Deque?因为Deque可以实现先进先出或后进先出的操作,适合用于存储请求等待执行。
    如果需要存储有序的元素,并且不需要根据键查找值,可以用ArrayList或数组。如果需要存储无序的键值对,并且需要根据键查找值,可以用HashMap。但是这些数据结构可能不如Deque方便和高效地进行头尾的操作。
  5. 为什么线程池中,默认的队列使用SynchronousQueue?
    SynchronousQueue是一个没有容量的阻塞队列,它可以保证每个提交的任务都会被执行,而不会被缓存或丢弃。 可以支持公平性策略,让等待时间最长的生产者或消费者优先执行。可以避免线程池中的线程过多或过少,提高资源利用率和响应速度
  6. 使用了什么设计模式?
    OkHttp中最直接的责任链模式的使用就是Interceptor的使用。书写简单漂亮,使用也非常方便,只需要OkHttpClient.Builder调用addInterceptor()方法,将实现了Interceptor接口的类添加进去即可,扩展性和可定制化都非常方便。
OkHttpClient httpClient = new OkHttpClient.Builder()
                .addInterceptor(new HeaderInterceptor())
                .addInterceptor(new LogInterceptor())
                .addInterceptor(new HttpLoggingInterceptor(logger)
                ......
                .readTimeout(30, TimeUnit.SECONDS)
                .cache(cache)
                .build();

OkHttp中最直接的建造者模式的使用就是XXBuilder的使用。在OkHttp中的OkHttpClient、Request、Response、HttpUrl、Headers、MultipartBody等大量使用了类似的建造者模式。

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
......
  public static final class Builder {
    Dispatcher dispatcher;
    @Nullable Proxy proxy;
    int callTimeout;
    int connectTimeout;
......

    public OkHttpClient build() {
      return new OkHttpClient(this);
    }
  }
}

public class Headers {
......
  public static final class Builder {
    final List<String> namesAndValues = new ArrayList<>(20);
......

    public Headers build() {
      return new Headers(this);
    }
  }
}

public class Request {
......

  public static class Builder {
    @Nullable HttpUrl url;
    String method;
    Headers.Builder headers;
    @Nullable RequestBody body;
......

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

将对象的创建与表示相分离,Builder负责组装各项配置参数,并且生成对象,目标对象则对外提供接口,符合类的单一原则
工厂模式相对于建创者模式的生成对象的过程更复杂,侧重于对象的生成过程,比如

public interface Call extends Cloneable {
  
  Request request();
  Response execute() throws IOException; 
  void enqueue(Callback responseCallback);
  void cancel();  
  boolean isExecuted();  
  boolean isCanceled(); 
  Call clone();
  //创建Call实现对象的工厂
  interface Factory {
    //创建新的Call,里面包含了Request对象。
    Call newCall(Request request);
  }
}
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }
}final class RealCall implements Call {
  ......
}

在Call接口中,有一个内部工厂Factory接口。这样只要像下面这样就可以了:
实现Call接口,现实相应的功能,RealCall;
使用某个类(OkHttpClient)实现Call.Factory接口,在newCall中返回RealCall对象,就可以了。

  1. 为什么这么设计?如果是你,还能想到其他更好的设计模式吗?
    这些设计模式的目的是为了提高OkHttp的可扩展性、可维护性和可读性。如果是我,我可能会考虑使用观察者模式,让用户可以注册一些回调函数,来监听请求和响应的状态变化。这样可以让用户更方便地处理异步请求和异常情况。
  2. 每个拦截器设计的意义是什么?
    RetryAndFollowUpInterceptor:负责请求失败的时候实现重试重定向功能。
    BridgeInterceptor:负责将用户构造的请求转换为向服务器发送的请求,添加一些必要的头部信息。
    CacheInterceptor:负责处理缓存逻辑,根据缓存策略和响应头判断是否使用缓存或更新缓存。
    ConnectInterceptor:负责建立连接,选择路由,协商协议等。
    CallServerInterceptor:负责向服务器发送请求和接收响应,处理GZIP压缩等。
  3. 为什么使用Socket连接池?好处是什么
    okhttp使用Socket连接池的原因是提高性能和效率。连接池可以复用已有的连接,避免频繁地创建和关闭连接,减少请求延迟和网络开销。如果使用HTTP/2协议,连接池还可以多路复用同一主机的请求,进一步提升并发能力。
  4. 为什么每次请求运行完之后都需要对队列中的请求进行维护?这样设计有什么好处
    如果是以前的volley框架的话,他会启动一个线程会有一句代码while(true),代表这个线程一直是死循环。假设客户端一直源源不断向服务器发送请求,那么没问题,但是要是不发送请求,线程一直跑的话,这样的话就会造成浪费。
    但是okhttp不一样,假设发送一条请求,会将请求放到线程池,线程池执行去请求服务器,请求完成后会回去检查,如果没有任何请求发送过来,就不可能将请求加入到运行中的队列,也就不可能放到线程池,也不是发送到服务器,使得框架内部是停止状态。不会向while(true)那样源源不断发送请求,使得造成浪费,性能下降。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/723164.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替&#xff0c;则数字序列称为 摆动序列 。第一个差&#xff08;如果存在的话&#xff09;可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如&#xff0c; [1, 7, 4, 9, 2, 5] 是一个 摆动序列 &…

机器学习 day24(多类分类模型)

多类分类 多类分类问题仍然是分类问题&#xff0c;所以预测y的可能结果是少量的&#xff0c;而不是无穷多个&#xff0c;且对于多类分类它&#xff1e;2 如上图&#xff1a;左侧为二分类&#xff0c;右侧为多分类&#xff0c;可以通过决策边界来划分区域

EasyExcel导出横向动态表头

需求&#xff1a;导出如下所示excel文档&#xff0c;红框内为动态表头 mysql表内数据格式如下图所示&#xff0c;由于某些特殊原因不能横向展示&#xff0c;仅能纵向展示&#xff0c;所以一个vin对应的数据可能有很多此时导出的时候上图所示的样式更便于人员观看。 依赖项 <…

VS多处理器编译提高编译速度

VS多处理器编译提高编译速度 开启多处理器编译能够提升编译速度&#xff0c;特别是当工程巨大时候&#xff0c;编译速度往往很慢&#xff0c;打开多处理器编译效果明显&#xff0c;下面给出设置和对比 开启多处理器编译 关闭多处理器编译

RabbitMQ系列(8)--实现RabbitMQ队列持久化及消息持久化

概念&#xff1a;在上一章文章中我们演示了消费者宕机的情况下消息没有被消费成功后会重新入队&#xff0c;然后再被消费&#xff0c;但如何保障RabbitMQ服务停掉的情况下&#xff0c;生产者发过来的消息不会丢失&#xff0c;这时候我们为了消息不会丢失就需要将队列和消息都标…

提高计算能力的五种方法

一旦你投入时间&#xff0c;就会 不舍得 轻易放手。 一开始可能会很浪费时间&#xff0c;但是当你练熟之后&#xff0c;效果显而易见。 二次函数&#xff1a;

php对称加密AES加密解密

AES-128-ECB和AES-256-CBC是两种常见的AES加密模式&#xff0c;它们在加密方式和安全性上有以下区别&#xff1a; 加密方式&#xff1a; AES-128-ECB&#xff1a;ECB&#xff08;Electronic Codebook&#xff09;模式是最简单的AES加密模式&#xff0c;它将数据分成固定大小的…

CSS 自定义提示(重写 title 属性)

前言 CSS 原生 title 属性太丑&#xff0c;需要重写 效果 改造 HTML 代码第2行&#xff0c;tip-label 自定义属性 <div class"tools"><div class"btn tip" v-for"item of list" :key"item.icon" :tip-label"item.l…

允许Traceroute探测漏洞和ICMP timestamp请求响应漏洞解决方法

目录 服务器检测出了漏洞需要修改 1.允许Traceroute探测漏洞解决方法 2、ICMP timestamp请求响应漏洞 服务器检测出了漏洞需要修改 1.允许Traceroute探测漏洞解决方法 详细描述 本插件使用Traceroute探测来获取扫描器与远程主机之间的路由信息。攻击者也可以利用这些信息来…

Sentinel持久化实战

前言 Sentinel有pull&#xff08;拉&#xff09;模式&#xff0c;和push&#xff08;推&#xff09;模式。本文是使用reids实现pull模式。 通过SPI机制引入自己的类 在项目的 resources > META-INF > services下创建新文件&#xff0c;文件名如下&#xff0c;内容是自…

centos7系统一键离线安装docker

离线安装脚本 # 离线安装docker rpm -Uvh --force --nodeps *.rpm # 启动docker systemctl start docker sudo systemctl daemon-reload # 设置开机自动启动docker systemctl enable docker.service下载 程序包下载地址 https://gitcode.net/zenglg/centos7_docker_offline_…

泛微E9鉴权

调用OA鉴权接口的过程 认证流程时序图 第一步、注册许可 请求地址&#xff1a; ​ http://泛微服务地址/api/ec/dev/auth/regist 请求方式&#xff1a;post 请求头部参数&#xff08;request headers&#xff09;&#xff1a; 参数名必选类型说明appid是string许可证号码…

前端学习——jsDay4

函数 函数使用 小练习 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widt…

【算法与数据结构】232、LeetCode用栈实现队列

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;这道题要求我们用栈模拟队列&#xff08;工作上一定没人这么搞&#xff09;。程序当中&#xff0c;pus…

【cuda】cuda-12.0对应tensorflow-gpu安装

我的cuda是12.0的&#xff0c;先根据下面的链接装了cudnn (12条消息) Windows10系统CUDA和CUDNN安装教程_windows安装cudnn_流泪&#xff06;枯萎的博客-CSDN博客 (12条消息) 【精简】2023年最新Windows安装GPU版本的tensorflow&#xff08;含bug记录及解决&#xff09;_tenso…

js引入json文件

引入json文件有多种方法&#xff0c;这里记录一种方法&#xff1a; 1、有json文件 文件内容 2、上传到服务器获得url&#xff08;也可以不上传&#xff09; 获取url:“https://asc-test1.oss-cn-beijing.aliyuncs.com/2023/07/05/b1917481d4eb40628bc6d10113896d82taizhou.jso…

玩机搞机---小米刷机工具平台刷写报错对症解决方法

安卓玩机搞机技巧综合资源-----全安卓机型通用线刷 卡刷教程。新老机型可参考【十八】 小米刷机平台工具版本较多。有时候更换工具版本刷机会有不同的提示。虽然目前很多第三方包都采用脚本方式刷写&#xff0c;但我们具体要了解使用官方工具报错究竟是什么原因。常见的报错有…

Ae 效果:CC Vignette

风格化/CC Vignette Stylize/CC Vignette CC Vignette&#xff08;CC 暗角&#xff09;效果可以用于各种场景&#xff0c;例如添加电影感、突出焦点、调整色调或增加视觉吸引力。使用 CC Vignette&#xff0c;可以轻松地为图像或视频创建独特的暗角效果&#xff0c;使其更具艺术…

【Git原理与使用】-- 标签管理

目录 理解标签 创建标签 操作标签 删除 推送 理解标签 标签 tag &#xff0c;可以简单的理解为是对某次 commit 的⼀个标识&#xff0c;相当于起了⼀个别名。例如&#xff1a;在项目发布某个版本的时候&#xff0c;针对最后⼀次 commit 起⼀个 v1.0 这样的标签来标识里程碑…

C++初阶之类和对象(下)

类和对象&#xff08;下&#xff09; 1. 再谈构造函数1.1 构造函数体赋值1.2 初始化列表1.3 explicit关键字 2. static成员2.1 概念2.2 特性 3. 友元3.1 友元函数3.2 友元类 4. 内部类5.匿名对象6.拷贝对象时的一些编译器优化结语 1. 再谈构造函数 1.1 构造函数体赋值 在创建…