OKhttp源码分析

作者: 千涯秋瑟 | 来源:发表于2017-12-13 18:24 被阅读0次

    一、Okhttp的使用

    1、创建一个OKHttpClient对象

    2、创建一个request对象,通过内部类Builder调用生成Request对象

    3、创建一个Call对象,调用execute/enqueue。

    这个使用不在叙述,详情请参考Android网络编程(六)OkHttp3用法全解析

    OkHttp3源码分析

    OkHttp是一个高效的Http客户端,有如下的特点:

    支持HTTP2/SPDY黑科技

    socket自动选择最好路线,并支持自动重连

    拥有自动维护的socket连接池,减少握手次数

    拥有队列线程池,轻松写并发

    拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)

    基于Headers的缓存策略

    二、Okhttp源码剖析

    主要对象

    Connections: 对JDK中的物理socket进行了引用计数封装,用来控制socket连接

    Streams: 维护HTTP的流,用来对Requset/Response进行IO操作

    Calls: HTTP请求任务封装

    StreamAllocation: 用来控制Connections/Streams的资源分配与释放

    第一部分   工作流程的概述

    RealCall.java

    当我们用OkHttpClient .newCall(request)进行execute/enenqueue时,实际是将请求Call放到了Dispatcher中,okhttp使用Dispatcher进行线程分发,它有两种方法,一个是普通的同步单线程;另一种是使用了队列进行并发任务的分发(Dispatch)与回调,我们下面主要分析第二种,也就是队列这种情况,这也是okhttp能够竞争过其它库的核心功能之一。

    Dispatcher的结构

    Dispatcher维护了如下变量,用于控制并发的请求

    private intmaxRequests=64;// 最大并发请求数为64

    private intmaxRequestsPerHost=5;// 每个主机最大请求数为5

    /**  消费者池(也就是线程池) */

    private  ExecutorServiceexecutorService;

    /** 正在准备的异步请求 */

    private final  Deque  readyAsyncCalls=newArrayDeque<>();

    /**正在运行的异步请求*/

    private final  DequerunningAsyncCalls=newArrayDeque<>();

    /** 正在运行的同步请求. */

    private finalDequerunningSyncCalls=newArrayDeque<>();

    在异步enqueue()请求中,如果满足(runningRequests<64 && runningRequestsPerHost<5),那么就直接把AsyncCall直接加到runningCalls的队列中,并在线程池中执行。如果消费者缓存满了,就放入readyAsyncCalls进行缓存等待。如图。

    Socket管理(StreamAllocation)

    这个环节主要讲了http的握手过程与连接池的管理,分析的对象主要是StreamAllocation。

    1. 选择路线与自动重连(RouteSelector)

    此步骤用于获取socket的ip与端口,各位请欣赏 源码中next()的迷之缩进与递归,代码如下:

    2. 连接socket链路(RealConnection)

    当地址,端口准备好了,就可以进行TCP链接了(也就是我们常说的TCP三次握手)。

    最终调用了socket.connect(address,connectTimeout)

    3、释放socket链路(release)  StreamAllocation.java

    尝试从缓存的连接池中删除(remove)

    如果没有命中缓存,就直接调用jdk的socket关闭

    第二部分 复用连接池  连接池的使用与分析

    Call: 对http的请求封装,属于程序员能够接触的上层高级代码

    Connection: 对jdk的socket物理连接的包装,它内部有List>的引用

    StreamAllocation: 表示Connection被上层高级代码的引用次数

    ConnectionPool: Socket连接池,对连接缓存进行回收与管理,与CommonPool有类似的设计

    Deque: Deque也就是双端队列,双端队列同时具有队列和栈性质,经常在缓存中被使用,这个是java基础

    2.1. 实例化

    在源码中,我们先找ConnectionPool实例化的位置,它是直接new出来的,而它的各种操作却在OkHttpClient的static区实现了Internal.instance接口作为ConnectionPool的包装。


    2.3 put/get操作


    第三部分 [缓存策略]

    HTTP缓存基础知识

    1. Expires

    表示到期时间,一般用在response报文中,当超过此事件后响应将被认为是无效的而需要网络连接,反之而是直接使用缓存

    Expires: Thu, 12 Jan 2017 11:01:33 GMT

    2. Cache-Control

    相对值,单位是秒,指定某个文件被续多少秒的时间,从而避免额外的网络请求。比expired更好的选择,它不用要求服务器与客户端的时间同步,也不用服务器时刻同步修改配置Expired中的绝对时间,而且它的优先级比Expires更高。比如简书静态资源有如下的header,表示可以续31536000秒,也就是一年。

    Cache-Control: max-age=31536000, public

    3. 修订文件名(Reving Filenames)

    如果我们通过设置header保证了客户端可以缓存的,而此时远程服务器更新了文件如何解决呢?我们这时可以通过修改url中的文件名版本后缀进行缓存,比如下文是又拍云的公共CDN就提供了多个版本的JQuery。

    upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js

    4. 条件GET请求(Conditional GET Requests)与304

    如缓存果过期或者强制放弃缓存,在此情况下,缓存策略全部交给服务器判断,客户端只用发送条件get请求即可,如果缓存是有效的,则返回304 Not Modifiled,否则直接返回body。

    以上内容是作为一个服务器开发或者客户端的常识,下图是网上找的总结,注意图中的 ETag 和 Last-Modified 可能有优先级的歧义,你只需要记住它们是没有优先级的。

    源码分析

    OkHttp中使用了CacheStrategy实现了上文的流程图,它根据之前的缓存结果与当前将要发送Request的header进行策略分析,并得出是否进行请求的结论。

    第四部分  OkHttp3   [DiskLruCache]

    1. Cache的简介

    缓存,顾名思义,也就是方便用户快速的获取值的一种储存方式。

    缓存载体与持久载体总是相对的,容量远远小于持久容量,成本高于持久容量,速度高于持久容量。

    需要实现排序依据,在java中,可以使用Comparable作为排序的的接口

    需要一种页面置换算法(page replacement algorithm)将旧页面去掉换成新的页面,如最久未使用算法(LFU)、先进先出算法(FIFO)、最近最少使用算法(LFU)、非最近使用算法(NMRU)等

    如果没有命中缓存,就需要从原始地址获取,这个步骤叫做“回源”,CDN厂商会标注“回源率”作为卖点。

    在OkHttp中,使用FileSystem作为缓存载体(磁盘相对于网络的缓存),使用LRU作为页面置换算法(封装了LinkedHashMap)。

    2. LinkedHashMap原理

    待续。。。

    总结

    OkHttp通过对文件进行了多次封装,实现了非常简单的I/O操作

    OkHttp通过对请求url进行md5实现了与文件的映射,实现写入,删除等操作

    OkHttp内部维护着清理线程池,实现对缓存文件的自动清理

    第五部分  OkHttp3源码分析[任务队列]

    1. 线程池基础

    在初学Java的时候,各位可能会用new Thread + Handler来写异步任务,它的坑网上已经烂大街了,比如不能自动关闭,迷之缩进难以维护,导致目前开发者几乎不怎么用它。而现在很多框架,比如Picasso,Rxjava等,都帮我们写好了对应场景的线程池,但是线程池到底有什么好呢?

    线程池好处都有啥?

    线程池的关键在于线程复用以减少非核心任务的损耗。

    多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。但如果对多线程应用不当,会增加对单个任务的处理时间

    假设在一台服务器完成一项任务的时间为T。

    T1 创建线程的时间

    T2 在线程中执行任务的时间,包括线程间同步所需时间

    T3 线程销毁的时间

    显然T = T1+T2+T3。

    可以看出T1,T3是多线程本身的带来的开销(在Java中,通过映射pThead,并进一步通过SystemCall实现native线程),我们渴望减少T1,T3所用的时间,从而减少T的时间。但一些线程的使用者并没有注意到这一点,所以在程序中频繁的创建或销毁线程,这导致T1和T3在T中占有相当比例。显然这是突出了线程的弱点(T1,T3),而不是优点(并发性)。

    线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。

    1. 通过对线程进行缓存,减少了创建销毁的时间损失。

    2. 通过控制线程数量阀值,减少了当线程过少时带来的CPU闲置(比如说长时间卡在I/O上了)与线程过多时对JVM的内存与线程切换时系统调用的压力。

    在OkHttp,使用如下构造了单例线程池

    public synchronized  ExecutorService  executorService() {

    if(executorService==null) {

    executorService=new     ThreadPoolExecutor(0,Integer.MAX_VALUE,60,TimeUnit.SECONDS,

    newSynchronousQueue(),Util.threadFactory("OkHttp Dispatcher", false));

    }

    returnexecutorService;

    }


    参数说明如下:

    int corePoolSize: 最小并发线程数,这里并发同时包括空闲与活动的线程,如果是0的话,空闲一段时间后所有线程将全部被销毁。

    int maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理

    long keepAliveTime: 当线程数大于corePoolSize时,多余的空闲线程的最大存活时间,类似于HTTP中的Keep-alive

    TimeUnit unit: 时间单位,一般用秒

    BlockingQueue workQueue: 工作队列,先进先出,可以看出并不像Picasso那样设置优先队列。

    ThreadFactory threadFactory: 单个线程的工厂,可以打Log,设置Daemon(即当JVM退出时,线程自动结束)等。设置为false说明是用户线程。。

    现在简单介绍Thread.setDaemon方法。

    java中线程分为两种类型:用户线程守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。

    所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

    可以看出,在Okhttp中,构建了一个阀值为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒,它使用了一个不存储元素的阻塞工作队列,一个叫做"OkHttp Dispatcher"的线程工厂。

    也就是说,在实际运行中,当收到10个并发请求时,线程池会创建十个线程,当工作完成后,线程池会在60s后相继关闭所有线程。

    在而在OkHttp中,它使用Dispatcher作为任务的派发器,线程池对应多台后置服务器,用AsyncCall对应Socket请求,用Deque对应Nginx的内部缓存。

    相关文章

      网友评论

        本文标题:OKhttp源码分析

        本文链接:https://www.haomeiwen.com/subject/tuoaixtx.html