Android okhttp3:原理详解

1、okHttp的工作流程

1.1、整体流程

image

①、OkHttpClient实现 Call.Factory,负责为 Request 创建 Call。
②、 RealCall 为具体的 Call 实现,其 enqueue() 异步接口通过 Dispatcher 利用 ExecutorService 实现,而最终进行网络请求时和同步 execute() 接口一致,都是通过 RealCall中的getResponseWithInterceptorChain()函数实现。
③、getResponseWithInterceptorChain() 中利用 Interceptor 链条,分层实现缓存、透明压缩、网络 IO 等功能。

1.2、各大拦截器介绍

1.2.1、getResponseWithInterceptorChain()方法

Response getResponseWithInterceptorChain()throws IOException {
  // Build a full stack of interceptors.
  List interceptors =new ArrayList<>();
  interceptors.addAll(client.interceptors());      -------> (1)
  interceptors.add(retryAndFollowUpInterceptor);     -------> (2)
  interceptors.add(new BridgeInterceptor(client.cookieJar()));     -------> (3)
  interceptors.add(new CacheInterceptor(client.internalCache()));     -------> (4)
  interceptors.add(new ConnectInterceptor(client));     -------> (5)
  if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());     -------> (6)
  }
  interceptors.add(new CallServerInterceptor(forWebSocket));     -------> (7)
  Interceptor.Chain chain =new RealInterceptorChain(interceptors,null,null,null,0,
  originalRequest,this,eventListener,client.connectTimeoutMillis(),
  client.readTimeoutMillis(),client.writeTimeoutMillis());
  return chain.proceed(originalRequest);
}

(1)、在配置OkHttpClient时设置的interceptors。
(2)、负责失败重试以及重定向的RetryAndFollowUpInterceptor。
(3)、负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的BridgeInterceptor。
(4)、负责读取缓存直接返回、更新缓存的CacheInterceptor。
(5)、负责和服务器建立连接的ConnectInterceptor。
(6)、配置OkHttpClient时设置的networkInterceptors。
(7)、负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor。

1.2.2 RetryAndFollowUpInterceptor

负责请求的重试和重定向

1.2.3 BridgeInterceptor

负责将原始Requset转换给发送给服务端的Request以及将Response转化成对调用方友好的Response。具体就是对request添加Content-Type、Content-Length、cookie、Connection、Host、Accept-Encoding等请求头以及对返回结果进行解压、保持cookie等。

CacheInterceptor

负责读取缓存以及更新缓存。
在请求阶段:
1、读取候选缓存cacheCandidate
2、根据originOequest和cacheresponse创建缓存策略CacheStrategy
3、根据缓存策略,来决定是否使用网络或者使用缓存或者返回错误
结果返回阶段:
负责将网络结果进行缓存(使用于DiskLruCache)。


image.png

强制缓存:当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。
对比缓存:当客户端第一次请求数据时,服务端会将缓存标识(Etag/If-None-Match与Last-Modified/If-Modified-Since)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存
标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示缓存可用,如果返回200,标识缓存不可用,使用最新返回的数据。
ETag是用资源标识码标识资源是否被修改,Last-Modified是用时间戳标识资源是否被修改。ETag优先级高于Last-Modified。

ConnectInterceptor:负责与服务器建立连接

使用StreamAllocation.newStream来和服务端建立连接,并返回输入输出流(HttpCodec),实际上是通过StreamAllocation中的findConnection寻找一个可用的Connection,然后调用Connection的connect方法,使用socket与服务端建立连接。

CallServerInterceptor:负责从服务器读取响应的数据

主要的工作就是把请求的Request写入到服务端,然后从服务端读取Response。
(1)、写入请求头
(2)、写入请求体
(3)、读取响应头
(4)、读取响应体

2、连接池原理

1、在Reuqst的header中将Connection设置为keepalive来复用连接。
2、Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。

2.1、连接池的清理

1、ConectionPool在内部使用一个异步线程来清理连接。
2、当连接池中有连接时:清理任务由cleanup()方法完成,首先执行清理,并返回下次需要清理的间隔时间,调用调用wait() 方法释放锁。等时间到了以后,再次进行清理,并返回下一次需要清理的时间间隔,再次进入wait,以此循环往复。
3、当连接池中没有连接时:cleanup()返回-1,跳出循环,下次有连接加进来时,再次开启线程进行循环清理。
4、之所以连接池线程可以跳出循环,是因为,他是子线程,而looper选择一直阻塞是因为他是主线程,如果跳出,程序执行结束。

3、OkHttp中Dispatcher和线程池

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

1、OkHttp中的线程池是一个 newCachedThreadPool。
2、所以在 OkHttp 中线程池只是一个辅助作用,仅仅是用来做线程缓存,便于复用的。
3、真正控制请求并发数量和执行时机是通过调度器 Dispatcher 完成的。

3.2、OkHttp中Dispatcher

RealCall.execute

@Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    timeout.enter();
    eventListener.callStart(this);
    try {
      //将RealCall加入Dispatcher的runningSyncCalls队列
      client.dispatcher().executed(this);
      //调用getResponseWithInterceptorChain获取Response
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      e = timeoutExit(e);
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      //调用Dispatcher的finished方法,将自身从runningSyncCalls移除
      client.dispatcher().finished(this);
    }
  }

1、先将RealCall加入Dispatcher的runningSyncCalls队列。
2、然后调用getResponseWithInterceptorChain获取Response。
3、最后调用Dispatcher的finished方法,将自身从runningSyncCalls移除。
RealCall.enqueue

@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));
  }
void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);
    }
    promoteAndExecute();
  }
private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));

    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();

        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.

        i.remove();
        executableCalls.add(asyncCall);
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());
    }

    return isRunning;
  }

如果当前正在执行的RealCall的数量小于最大并发数maxRequest(64),并且该call对应的Host上的call小于同一host上的最大并发maxRequestsPerHos(5),则将该call加入runningAsyncCalls,并将这个call放到线程池中进行执行,否则加入readyAsyncCall排队等待。
注意:
同步请求和异步请求执行完成之后,都会调用dispatcher的finished方法,将自身从对应的队列中移除,然后进行轮询readyAsyncCalls队列,取出ready的异步任务在满足条件下放到线程池中执行。

3.3、Dispatcher.中的并发数量及三个队列的作用

1、maxRequests = 64 // 最大并发请求数为64
2、maxRequestsPerHost = 5 //每个主机最大请求数为5
3、ExecutorService executorService //消费者池(也就是线程池)
4、Deque<AsyncCall> readyAsyncCalls: // 异步的缓存,正在准备被消费的(用数组实现,可自动扩容,无大小限制)
5、Deque<AsyncCall> runningAsyncCalls //正在运行的 异步的任务集合,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存
6、Deque<RealCall> runningSyncCalls //正在运行的,同步的任务集合。仅仅是用来引用正在运行的同步任务以判断并发量

4、OkHttp中的设计模式

1、责任链模式:拦截器链
2、单例模式:线程池
3、观察者模式:各种回调监听
4、策略模式:缓存策略
5、Builder模式:OkHttpClient的构建过程
6、外观模式:OkHttpClient封装了很对类对象
7、工厂模式:Socket的生产

5、OkHttp的优势

5.1、功能方面

功能全面,满足了网络请求的大部分需求。

5.2、网络优化方面

1、内置连接池,支持连接复用
2、支持gzip压缩响应体
3、通过缓存避免重复的请求
4、支持http2,对一台机器的所有请求共享同一个socket

5.3、扩展性方面

拦截器模式使得我们很容易添加一个自定义拦截器对请求和返回结果进行处理。

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