惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
T
Tailwind CSS Blog
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
Microsoft Azure Blog
Microsoft Azure Blog
爱范儿
爱范儿
P
Proofpoint News Feed
D
Docker
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
云风的 BLOG
云风的 BLOG
有赞技术团队
有赞技术团队
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
U
Unit 42
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 司徒正美
博客园 - Franky
The GitHub Blog
The GitHub Blog
月光博客
月光博客
小众软件
小众软件
Martin Fowler
Martin Fowler
P
Palo Alto Networks Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
IT之家
IT之家
K
Kaspersky official blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
Spread Privacy
Spread Privacy
博客园 - 聂微东
B
Blog
The Hacker News
The Hacker News
Simon Willison's Weblog
Simon Willison's Weblog
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
P
Privacy International News Feed
腾讯CDC
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Threatpost
T
Tenable Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
Stack Overflow Blog
Stack Overflow Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Heimdal Security Blog

音视频开发进阶

音视频教程-第三节 音视频教程-第二节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libjpeg-turbo 编译与实践 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
开源库之 OkHttp 源码分析
音视频开发进阶 · 2018-09-11 · via 音视频开发进阶

一个专注音视频领域的小圈子

分析一波 OkHttp 的源码实现。

简单使用

官方给出了使用例子,具体详情参考 官网

// 创建 OkHttp 请求客户端
OkHttpClient client = new OkHttpClient();
// 构建一个请求
Request request = new Request.Builder()
      .url(url)
      .build();
// 执行网络请求并返回结果      
Response response = client.newCall(request).execute();      
// 得到结果内容
response.body().string();

可以看到,使用过程还是很容易理解的:

首先创建请求客户端 -> 接着创建网络请求 -> 然后发出网络请求 -> 最后得到请求结果。

把网络请求过程抽象化之后就是上面的流水线步骤了,其中最重要的步骤就是发出网络请求了,在 OkHttp 中用到了责任链模式,对发出的网络请求会按照责任链表的顺序依次进行处理,比如请求重试、连接处理、缓存处理、日志处理等,这个地方再写一篇文章详细分析。

下面针对 OkHttp 的调用流程和结构作分析。

创建请求

首先是创建请求客户端 OkHttpClient 和网络请求 Request 。

对于这种要创建某某对象的,上来就是一个建造者模式,建造者模式可以说是在开源项目中最常见的设计模式了。

在 OkHttpClient 和 Request 内部都有一个 Builder 的内部类用来执行具体的构建操作,一般 Builder 类有很多方法,它们都是用来设置具体构建参数的,顺着哪些方法就可以找到都有哪些配置选项,这些选项也就是对外暴露的接口,这算是阅读源码的一个小技巧了。

OkHttpClient 的 Builder 内部配置选项:

      dispatcher = new Dispatcher();
      // 使用默认的选项
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;

重点分析 Dispatcher 类用来管理和分发请求的,其他的如果没有指定的话都是默认选项,等用到时再具体分析。

在 Request 内部的 Builder 也有一些配置选项:

    HttpUrl url;
    String method;
    Headers.Builder headers;
    RequestBody body;
    /** A mutable map of tags, or an immutable empty map if we don't have any. */
    Map<Class<?>, Object> tags = Collections.emptyMap();

urlbodymethod 这些都是 HTTP 请求相关的内容,用一个 Request 类将它们进行封装,到具体执行请求时,都会将它们取出来使用。

同步执行

val response = client.newCall(request).execute()

同步执行的代码如上,首先是 newCall 方法将 request 转换成 RealCall 对象,它实现了 Call 的接口。

Request 类只是封装了请求的信息,比如 GETPOST 方法之类的,但具体的执行通过 Call 这一层来实现了。

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

然后是 Call 的 execute 方法。

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

首先是 synchronized 加锁保证线程安全,然后是通过 Dispatcher 类的 executed 方法将请求 RealCall 添加到请求的同步队列中去。

private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

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

runningSyncCalls 是一个 Deque 类型的队列。Deque 是一个双端队列,它具有队列和栈的特性,队列中的元素可以从两端弹出,而插入和删除操作只能在队列的两端进行。

executed 方法只是将请求添加到了队列中,具体的执行在 getResponseWithInterceptorChain 方法中,在这里就会将请求通过OkHttp 的各种拦截器,按照责任链模式进行调用,并得到请求返回的结果,用 Response 类封装。

当请求结束后,会调用 Dispatcher 类的 finished 方法。

  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) {
      // 将请求从队列中移除
      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();
    }
  }  

finished 方法主要是将请求 RealCall 从队列中 runningSyncCalls 移除。

这里还有个小知识点就是 trycatchfinally 三者的执行顺序,在 try 语句里面执行了 return 语句,但是返回的结果会保存在一个临时区域里面,然后执行 finally 语句,移除队列请求后再继续返回。

网络请求

下面重点看一下 getResponseWithInterceptorChain 方法。

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    // OkHttpClient 提供的默认拦截器
    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));
    // 重点参数是 0 ,表示责任链的开始
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

有一个拦截器 Interceptor 的集合,在创建 OkHttpClient 的时候可以通过 Builder 去添加我们自定义的拦截器,另外 OkHttp 也提供了几个默认的拦截器。

把拦截器添加到集合中,作为 RealInterceptorChain 类的参数,它实现了 Chain 接口,表示为一条。在这条上,每个拦截器都是它的一个节点,并且以链上任何一个拦截器为起点,又可以开始一条新的链。

具体来看看 proceed 方法:

  @Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }
  
  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;
    // 删除一些判断和抛出异常的方法
    
    // Call the next interceptor in the chain.
    // 以拦截器队列中的下一个拦截器为起点,构建新的请求链
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    // 取出当前请求链中的第一个
    Interceptor interceptor = interceptors.get(index);
    // 执行当前拦截器的功能,并且开始子链的请求流程
    Response response = interceptor.intercept(next);

    // 删除一些判断和抛出异常的方法
    return response;
  }

整个拦截器的集合 interceptors 是不会变的,而 index 就对应从拦截器集合中取出拦截器的索引,为 0 表示取出第一个来作为整条 的起点,从而构建一条请求链。

当一个拦截器执行具体功能时,也就是 intercept 方法,会把下一个作为参数传递过去,这样就又会以这条 为起始继续下一步的执行。

比如,看 HttpLoggingInterceptor 内部的实现:

    Request request = chain.request();
    if (level == Level.NONE) {
      return chain.proceed(request);
    }

如果要打印的日志即为为 NONE,也就是不打印,直接就开始 的下一个执行了。

简单地看,这里就是一个递归的调用流程。

异步执行

client.newCall(request).enqueue(object : okhttp3.Callback {

    override fun onFailure(call: okhttp3.Call?, e: IOException?) {
    }
    
    override fun onResponse(call: okhttp3.Call?, response: okhttp3.Response?) {
    }
})

异步请求的代码如上,和同步请求的区别在于后面是 enqueue 方法。

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    // 通过 Dispatcher 管理请求
    // 
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

enqueue 需要传入一个 Callback 的回调接口,用来处理请求成功和失败。另外,在 enqueue 的内部还是通过 Dispatcher 进行请求的管理。

这里传入的参数是 AsyncCall 类,不再是 RealCall 类了,AsyncCall 类继承自 NamedRunnable 类,NamedRunnable 实现了 Runnable 接口。

NamedRunnable 意思就是有名字的线程,会在线程执行时临时改变线程的名字,执行结束后再改回来。

  @Override public final void run() {
    // 保存当前线程的名字
    String oldName = Thread.currentThread().getName();
    // 设置新的名字
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      // 执行后再改为原来的名字
      Thread.currentThread().setName(oldName);
    }
  }

另外,AsyncCall 实现了 NamedRunnable 的抽象方法 execute ,该方法也是网络请求的具体执行部分,问题在于这些异步请求是如何分发和管理的,还是回到 Dispatcher 类中来。

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

首先是判断异步请求队列 runningAsyncCalls 的个数是否超过最大请求数了,另外是判断同一主机的网络请求个数是否超过限制。

如果都不超过,就将请求添加到异步请求队列中,并在线程池中去执行。

如果超过了,就把请求添加到准备就绪的队列 readyAsyncCalls 中,等待后续再去执行。

executorService() 方法的执行就是初始化线程池的。

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

ThreadPoolExecutor 用来提供一个线程池,它的方法原型如下:

    public ThreadPoolExecutor(int corePoolSize,             // 核心线程池的大小
                              int maximumPoolSize,        // 线程池的最大大小
                              long keepAliveTime,         // 线程池空闲时,线程的存活时间
                              TimeUnit unit,             // 存活时间的时间单位
                              BlockingQueue<Runnable> workQueue, // 存放线程任务的队列
                              ThreadFactory threadFactory,      // 创建线程的工厂
                              RejectedExecutionHandler handler) {     // 任务拒绝策略
            // 省略
    }

使用线程池的好处是显而易见的,统一管理网络请求,减少线程频繁创建、销毁带来的开销。

通过线程池的 execute 方法去执行请求,具体就是 Runnable 接口中的 run 方法。

    // 异步请求中的方法
    @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 方法中。

finally 中还是会将异步的请求从队列中移除,不同的是内部最后一个参数为 false 。

  void finished(AsyncCall call) {
    // 与同步请求不同的是,最后一个参数为 True, 同步请求为 false
    finished(runningAsyncCalls, call, true);
  }

因为 True,所以 promoteCalls 就会执行。

  // 异步请求才会执行该方法
  private void promoteCalls() {
    // 优先把 readyAsyncCalls 队列中的请求执行完
    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.
    }
  }

之前提到,如果异步请求队列已经满了,就会把请求放到准备就绪的队列 readyAsyncCalls 中去,那么 promoteCalls 方法就是来处理 readyAsyncCalls 队列中的请求的。

每次从队列中移除异步请求后 size 都会减一,然后再判断队列的数量是否还是超过了最大请求数量,如果是就返回,表示优先把异步队列中的请求消耗完,如果不是,表示有空位,那就从准备就绪的队列中取出请求来执行。

readyAsyncCalls 队列中的请求放到 runningAsyncCalls 队列中去,再通过线程池去执行,并且还是会判断 runningAsyncCalls 的数量是否超过了最大请求,超过了就返回,表示 OkHttp 中整个异步请求的数量不能超过 maxRequests 表示的数量。

就这样实现了 OkHttp 的异步请求管理流程,并且还有一个类似请求排队的机制,

小结

OkHttp 是用来执行网络请求,包括同步和异步的请求。

对于同步请求,直接就执行了,同步阻塞直到返回取得请求结果。

对于异步请求,对请求进行管理,限制请求最大数量,如果超出数量就排队候选,每次执行完一次异步请求,有空位就去处理排队的请求。

对于具体的请求过程,通过责任链的模式,把请求分成多个过程,递归地对请求进行处理。

原创文章,转载请注明来源:    开源库之 OkHttp 源码分析