全面深入OkHttp源码(上)

前言


节前想着丰富下假期,在慕**上花了几百大洋买了个课程,结果三观都掉没了,虽然现在知识付费了,但这tama也叫高级课程!为了省钱,最直接的办法就是自己干,当然我是指学习方面。

一般而言懂得如何使用,此人可以认定为初级;懂得内部逻辑,熟悉流程间衔接,能够在框架提供的框框内玩耍API,世俗认定为此人中高级、已经很不错了;但我认为还不够。

本着不懂就查,不懂就深追的精神!!此文不同于其他讲解(介绍下如何使用、基本的源码流程、责任链和几大拦截器,这也是现状),我剑走偏锋,不是流程上的讲解,而是深入研读每一步的源码了解作者为何及意图,这也意味着你必需要知道OkHttp浅层的知识,否则看起来会很累,文章采用CPU响应中断保护现场的模式书写,以免思路混乱,同时文章也会异常的长长长。。。。

虽然与现在大环境相逆,但我深信:知其然,要知其所以然,这才是真正的工程师,而不是码农。

干货总结


由于文章太长、内容太多,相信大多数人不可能全部读完,因此总结放在前面,希望能够提起兴趣读完全文。

  • 接口适配器模式
  • StrictMode之CloseGuard
  • 双端队列的概念及环形队列的操作


    200
300

    /* 4XX: client error */

    /**
     * HTTP Status-Code 400: Bad Request.
     */
    public static final int HTTP_BAD_REQUEST = 400;

    /**
     * HTTP Status-Code 401: Unauthorized.
     */
    public static final int HTTP_UNAUTHORIZED = 401;

    /**
     * HTTP Status-Code 402: Payment Required.
     */
    public static final int HTTP_PAYMENT_REQUIRED = 402;

    /**
     * HTTP Status-Code 403: Forbidden.
     */
    public static final int HTTP_FORBIDDEN = 403;

    /**
     * HTTP Status-Code 404: Not Found.
     */
    public static final int HTTP_NOT_FOUND = 404;

    /**
     * HTTP Status-Code 405: Method Not Allowed.
     */
    public static final int HTTP_BAD_METHOD = 405;

    /**
     * HTTP Status-Code 406: Not Acceptable.
     */
    public static final int HTTP_NOT_ACCEPTABLE = 406;

    /**
     * HTTP Status-Code 407: Proxy Authentication Required.
     */
    public static final int HTTP_PROXY_AUTH = 407;

    /**
     * HTTP Status-Code 408: Request Time-Out.
     */
    public static final int HTTP_CLIENT_TIMEOUT = 408;

    /**
     * HTTP Status-Code 409: Conflict.
     */
    public static final int HTTP_CONFLICT = 409;

    /**
     * HTTP Status-Code 410: Gone.
     */
    public static final int HTTP_GONE = 410;

    /**
     * HTTP Status-Code 411: Length Required.
     */
    public static final int HTTP_LENGTH_REQUIRED = 411;

    /**
     * HTTP Status-Code 412: Precondition Failed.
     */
    public static final int HTTP_PRECON_FAILED = 412;

    /**
     * HTTP Status-Code 413: Request Entity Too Large.
     */
    public static final int HTTP_ENTITY_TOO_LARGE = 413;

    /**
     * HTTP Status-Code 414: Request-URI Too Large.
     */
    public static final int HTTP_REQ_TOO_LONG = 414;

    /**
     * HTTP Status-Code 415: Unsupported Media Type.
     */
    public static final int HTTP_UNSUPPORTED_TYPE = 415;
image.png

正文


以下内容基于OkHttp3.11.0版本

OkHttp的所有操作,都在当前线程,包括结果回调,这也意味着使用时必须为其开单独的子线程,否则OkHttp内部检查线程时就会报错。

OkHttpClient通过构建者模式创建实例,初始化参数,项目中可以使用单例,如果是多后台项目需要清楚内部参数设置,看完全文后会清楚原因。

Request同理创建实例,初始化参数。并作为参数初始化接口Call

这里可见,Call的实现类为RealCall,且持有OkHttpClient实例对象。

经由静态方法,进入构造方法给变量赋值。

分支开始EventListener,保存RealCall位置

为什么非要经过静态方法呢?给每一个Call对象的eventListener赋值为
经okHttpClient实例对象eventListenerFactory创建的EventListener。

OkHttpClient

EventListener.Factory来自Builder

如果Builder没有主动设置过,则默认生成。

EventListener.Factory接口根据传入的Call生成EventListener

EventListener是什么有什么用?

通过类结构,可以看出EventListener是一个抽象类,采用适配器模式(若定义为接口,则必须实现全部方法,又称接口适配器模式)默认每个方法都是空实现,继承者根据需要实现相应方法。方法名也很好理解,结果表明:这就是是请求生命周期的回调,在请求的不同时期会回调相应的方法。

那Call中的EventListener对象是哪里实现的?

一步步,通过Builder初始化默认eventListenerFactory,再传递到okHttpClient的eventListenerFactory,最终由eventListenerFactory创建EventListener。

哪里啊?没看见!这里确实绕了几个弯,第三、四张图是关键。

第四张图静态方法生成EventListenerFactory,但非常简单,传入的参数是EventListener,什么都没做就返回出去了,表明实际的eventListener对象是外部初始化后传入的。

回来看第三张图,传入的eventListener对象是什么?EventListener.NONE,又回去

eventListener对象为默认的EventListener类内部静态变量,且各个生命周期的回调并没有重写,意味着生命周期回调还是空实现。

这里就需要我们手动继承EventListener实现自己的生命周期监听类,并实现

EventListener.Factory接口,重写create返回自己的生命周期监听类,赋值给OkHttpClient.Builder

这样下来,经由此okHttpClient对象的Call,在相应的生命周期就会回调到自己的生命周期监听类内。

虽然代码绕来绕去,但更能够理解作者的意图,每个okHttpClient对象内不直接赋值或生成EventListener,而是存储eventListenerFactory工厂,让具体的EventListener由具体的eventListenerFactory生成,降低了okHttpClient对象与eventListener对象的耦合。

分支结束EventListener,读取RealCall位置

RealCall的构造方法内还赋值了retryAndFollowUpInterceptor变量,这是拦截器的起点(下面讲)。

Call对象初始化完成,其实就是基本的变量初始化。

执行execute方法,在其实现类RealCall内

每个Call对象只能被执行一次,有变量executed来记录自己是否被执行过,这里加锁为了防止多线程间线程安全,因为每个请求都会放到池子pool内(后面讲)。

分支开始StackTrace,保存RealCall位置

execute内执行captureCallStackTrace,译为"开启Call的栈跟踪"。

Platform类如其意"平台",注意有Android的Logger打印日志类变量,返回的PLATFORM来自findPlatform。

注释也写了,通过运行环境匹配合适的Platform,这里列举了几个平台,除了android其他的都不认识。

如果运行环境平台都不符合,则new Platform();

方法getStackTraceForCloseable返回的是Throwable

若符合android平台,通过反射获取一些证书、Session等网络相关的类。当Platform支持此功能时,便会反射invoke相关功能。

AndroidPlatform

返回AndroidPlatform,其继承自Platform

android平台下getStackTraceForCloseable被重写

这里有个内部类CloseGuard,其实这个类在android源码dalvik.system.CloseGuard包下,注释中也有提到,这里只是利用反射,封装了原CloseGuard的方法。

分支开始CloseGuard,保存StackTrace位置

先看看源码中的CloseGuard类

想多学习的也可以看看 http://duanqz.github.io/2015-11-04-StrictMode-Analysis#All

源码取自android7.0路径看图

注释中解释了此类的作用及如何使用,我再复述一遍。

CloseGuard类用来记录对象是否被关闭,某些情况下如果对象不关闭的话就会造成内存泄露或其他问题,由于CloseGuard类是源码级别的类,很多源码中判断对象是否关闭都有用到它如InputStream,可以看看引用的文章。

CloseGuard类使用也很简单,如例子,在使用对象Foo类中增加变量guard,实例化Foo对象时调用guard的open方法,表示对象开启在被使用,对象关闭调用cleanup,此时guard调用close,表明对象关闭不再使用。此时JVM便会GC此对象调用其finalize方法,如果对象不被使用且没有关闭调用cleanup(即内存泄露),GC时便会发现guard,并警告调用warnIfOpen。

整体思路就是这样,看看源码的实现。

注释写的很好,很明白。

CloseGuard类为final,在JVM方法区运行时常量池中存在NOOP对象,当CloseGuard功能不使用时,可避免浪费内存生成对象。

CloseGuard功能默认是打开的,android在启动的时候关闭了此功能,注释中也有提到。

何为关闭?功能开启情况下一个对象对应一个CloseGuard对象,关闭情况下,所有对象对应一个CloseGuard对象NOOP,即起不到监控对象是开启还是关闭的状态。

get方法直接返回自己或实例化自己

open方法则初始化了Throwable的allocationSite对象,针对不同的对象传入不同的closer信息,可以加速问题的排查

close方法置空了allocationSite对象

warnIfOpen方法在发现功能开启且对象未关闭时,便会报异常信息

而报异常信息的对象是系统级别

分支结束CloseGuard,读取StackTrace位置

CloseGuard通过反射取得源类的Method作为自己的变量,对象调用方法时,再利用反射invoke源类中相应的方法,达到对应的功能

getStackTraceForCloseable方法调用返回的是源CloseGuard对象

源CloseGuard对象

RealCall

又被赋值给责任链的第一环RetryAndFollowUpInterceptor拦截器中,此时的对象关系为:RealCall对象持有RetryAndFollowUpInterceptor对象,RetryAndFollowUpInterceptor对象持有CloseGuard对象。简言之:一个Call对象对应一个CloseGuard对象,当Call对象没有被close时便会报异常。

那这个CloseGuard对象何时被close呢?答案是没有,因为有池子的关系所有的Call都在池子内,当Call真正被回收清除的时候就会通过CloseGuard对象log下。

RetryAndFollowUpInterceptor

CloseGuard对象赋值给RetryAndFollowUpInterceptor对象

RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor对象转手有将它赋值给StreamAllocation对象(后面讲)

StreamAllocation

StreamAllocation对象转手有将它赋值给StreamAllocationReference它自己的弱引用

当池子需要清理了

ConnectionPool

便会取出之前的CloseGuard对象

log一下通知此连接已关闭,但并非真关闭,只是此次的连接请求关闭了,连接本身还留着执行下次的请求,这是pool的功能。

如果CloseGuard对象没有成功log,便会调用android中的Log类。

到此就是整个Call连接的路径追踪,从创建Call到连接流关闭,Platformge根据平台的不同,以不同的方式打印追踪Call连接流的关闭事件。

分支结束StackTrace,读取RealCall位置

eventListener生命周期回调callStart

紧接着httpClient对象调用dispatcher执行call对象

OkHttpClient

dispatcher对象在httpClient通过builder创建时默认生成

OkHttpClient

Dispatcher为异步请求执行策略,且为final类不允许继承

有可能官方后面会有扩充,通过开放继承或实现接口以用来个性化定制。目前版本,此类主要针对异步请求,对同步请求没影响仅做计数功能

Dispatcher中几个重要的变量,从上至下依次(其实变量名起的很明白)

  • 最大请求数
  • 每台主机最大请求数
  • Dispatcher空闲时的回调
  • 线程池
  • 异步等待队列
  • 异步请求队列
  • 同步请求队列

分支开始Deque,保存RealCall位置

什么是Deque?与Queue有什么关系?Queue又是什么?

Queue是数据结构中的队列,Queue提供了队列的基本操作,不同的实现类有具体的实现

ArrayDeque

通过类图看出,Deque接口继承自Queue,Dispatcher中的对象为ArrayDeque,它实现了Deque接口并继承自AbstractCollection。

那不是实现了两次Collection接口吗?

并不是,首先看一下实现与继承的区别:

java是单继承多实现,接口表示具体功能,实现接口意味着类具有某功能。继承为子承父业,父类具有的功能,子类同样具有。实现为具有某功能,继承为具有某功能的实现。

AbstractCollection为抽象类,实现了Collection声明的方法,又将功能合并生成抽象方法;ArrayDeque实现了Collection接口,表示ArrayDeque类具有Collection接口声明的功能,功能的具体实现在父类AbstractCollection中,ArrayDeque只需实现AbstractCollection类合并功能后的抽象方法。

简言之:ArrayDeque实现了Collection接口,但具体的实现在AbstractCollection中。这种设计思路值得学习

Queue为队列提供了队尾插入add、offer功能,队首移除remove、poll功能,还有偷窥队首元素element、peek功能,属于常规的队列操作接口

Deque继承自Queue,但其增加了队列首尾的功能,使队列成为双端队列,即首尾都可插入、移除。其还有栈功能,但不在此次介绍范围内。

Deque

ArrayDeque为双端队列实现的其中一种,简单看下源码:

与大部分集合工具类一样,具体的数据存储在数组中,可以看到transient关键字,表明不能够被序列化,但其内部有writeObject、readObject自己的序列化方式。

ArrayDeque是一种环形队列(源码中的head、tail指针与图中的算法不同,此图仅做参考,ArrayDeque中tail指针指null)

来自LeetCode https://leetcode.com/explore/learn/card/queue-stack/

通过head、tail指针完成整个队列的操作

初始化存储空间,numElements二进制每位或1操作,使initialCapacity为奇数,再自加为偶数,保证initialCapacity一定为2的n次方(英文翻译),正确应是:保证initialCapacity一定为偶数,只有这样在后面的求mask时二进制的各位才能都被与操作到。

如果initialCapacity超出最大长度则分配2^30 elements

双端队列插入操作

ArrayDeque

头部指针指向位置插入元素

红框标示的位置及为什么ArrayDeque的容量必须为偶数,只有偶数减1,必为奇数,而奇数二进制位的最后一位必定位1,这样与操作才不会漏位。

offer的内部逻辑与add是一致的,只不过offer成功会return true,不成功就NullPointerException

接着就是扩容逻辑

断言判断队列是否已满,保存临时变量记录位置,容量扩为原来的2倍,

将队列中head指针右侧的数组,拷贝到新数组的左端,紧接着拷贝剩余位置起点到tail指针的数组,最终新数组左侧填满,右侧空。

poll操作不会抛出异常,而remove不同,会有NoSuchElementException

为什么说tail指null呢?看pollFirst直接取head指针取数据,而pollLast是先计算tail指针再去取

注意get操作,并没有删除数据,但会NoSuchElementException

反倒peek不会

移除第一个匹配项,分为从head循环和tail循环,首次遇到equal项即删除

所有的数组集合工具类一样,删除意味着移动

check了队列的正确性后,移动数组,分了两种情况:

  • head在tail前
  • head在tail后

队列操作的实质

验证了我之前对head和tail的解析

分支结束Deque,读取RealCall位置

Dispatcher使用队列,正式因为队列的FIFO性质,符合网络请求的场景

dispatcher对象将call对象放入同步请求队列runningSyncCalls即结束

然后在请求返回后,调用dispatcher对象将call结束

简单的操作,将call从队列中移除,由于是同步操作所以没有触发promoteCalls,并重新计算此dispatcher(即okHttpclient)中正在运行的call总数

也很easy就是求和,如果没有正在执行的请求就执行Dispatcher空闲时的回调。

现在可能对Dispatcher的作用产生疑问,它到底有什么用?提前剧透,OkHttp中有池子的概念即复用,这会导致不同的请求使用同一个流,如何正确的识别请求而对外屏蔽池子,即是Dispatcher的功能。我们只管向Dispatcher中加请求,完成了就移除,判断请求存在与否时直接使用Dispatcher,即使请求被复用到了其他请求,Dispatcher也会将之前的请求移除,认为是不同的操作。

除了以上Dispatcher还做了请求量的控制和一些基本Call对象操作,接下来异步请求时会涉及

Dispatcher的在同步请求时没做多少事情,到此就休息了。那请求的结果是如何得到的?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容