OkHttp源码解析

OkHttp简单使用

  • gradle依赖配置
 implementation 'com.squareup.okhttp3:okhttp:3.14.1'
  • 网络权限
<uses-permission android:name="android.permission.INTERNET" />
  • GET请求示例
OkHttpClient okHttpClient = new OkHttpClient();

final Request request = new Request.Builder()
        .url("http://www.baidu.com")
        .method("GET", null)
        .build();

okHttpClient.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.d("net", "onFailure=" + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        //成功失败的回调都是在工作线程中
        String content = response.networkResponse().toString();
        Log.d("net", "onResponse=" + content);
    }
});

核心源码解析

  • 网络请求任务调度,RealCall#enqueue
void enqueue(Callback responseCallback, boolean forWebSocket) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    //通过任务调度器执行线程池任务,并控制请求任务执行或等待状态
    client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
}
  • 任务调度类Dispatcher
private int maxRequests = 64;//最大并发请求数
private int maxRequestsPerHost = 5;//每个主机的最大请求数
private ExecutorService executorService;//线程池
//异步请求 等待队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//异步请求 运行队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步请求 运行队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

//带自定义线程池的构造方法
public Dispatcher(ExecutorService executorService) {
  this.executorService = executorService;
}
//默认线程池的构造方法
public Dispatcher() {
}
  • 等待队列和运行队列中任务调度,Dispatcher#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; 
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; 

        i.remove();
        //移除等待队列中任务,添加到运行队列中和线程池任务中。
        asyncCall.callsPerHost().incrementAndGet();
        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;
  }

拦截器原理

拦截器主要用来添加、移除、转换请求或响应的头部信息。比如替换域名为IP地址,在请求头部添加公共参数等。

  • 拦截器接口类,Interceptor#Chain
public interface Interceptor {
  Response intercept(Chain chain) throws IOException;

  interface Chain {
    Request request();

    Response proceed(Request request) throws IOException;
    
    @Nullable Connection connection();
    ...
  }
}

请求体Request request = chain.request();
响应体Response response = chain.proceed(request);

  • 拦截器配置,RealCall#getResponseWithInterceptorChain
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //client中自定义拦截器
    interceptors.addAll(client.interceptors());
    //重定向与失败重连拦截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    //请求报头、响应报头处理,Cookie持久化策略
    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, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      //通过拦截器获取响应结果
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
}

OkHttp的拦截器列表按顺序调用,默认先调用用户自定义拦截器 client.interceptors()。

  • 责任链模式遍历拦截器列表处理请求,RealInterceptorChain#proceed
public Response proceed(Request request, Transmitter transmitter, 
 @Nullable Exchange exchange)throws IOException {
    ...
    //责任链模式,数组内拦截器依次调用Interceptor#intercept(),RealInterceptorChain#proceed()
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, 
        readTimeout, writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    ...
    return response;
}

责任链模式:在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。

网络通信缓存策略

网络请求中的缓存分为:服务器缓存,第三方缓存,客户端缓存。其中客户端缓存是性价比最高的,消耗服务器资源最少。

客户端缓存需要服务器确定数据的有效期,通常在请求报头和响应报头中通知缓存状态。

  • Expires(过期时间点),就是过了某个时间点缓存服务器的数据就认定过期,就要刷新数据。
    局限:Web服务器与缓存服务器时间同步成本;时间格式为GMT时间。
  • Cache-Control(缓存控制),HTTP头信息属性。如Cache-Control: max-age=3600, must-revalidate表示过期时间(基于上次访问数据的时间间隔)为3600秒,且须严格遵循。
  • Last-Modified/If-Modified-Since 客户端自动发送上次访问服务器该数据的时间点,服务器会判断数据在这段时间内是否更改,若没更改意味着客户端缓存(如果有的话)未过期,本次请求服务器只返回状态码304,不再重新发送数据,降低了服务器压力。
  • ETag/If-None-Match缓存策略是数据没有变化就不重新下载数据。服务器返回数据时,会在ETag头信息中附带该数据的hash码,下次请求服务器会匹配hash码,若数据相同则只返回状态码304,不重新发送数据。同Last-Modified/If-Modified-Since。

在OkHttp中的网络通信缓存策略采用的是Last-Modified/If-Modified-Since和Cache-Control结合的,后者优先级高。

  • 请求及响应缓存处理,CacheInterceptor#intercept
@Override public Response intercept(Chain chain) throws IOException {
 ...
 if (cacheResponse != null) {
     //状态码304表示服务器上对应数据未过期(未更改),本地缓存(如果有的话)仍有效
     if (networkResponse.code() == HTTP_NOT_MODIFIED) {
         Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), 
                networkResponse.headers()))
            .sentRequestAtMillis(
                networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(
                networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        //关闭网络请求连接,返回本地缓存的所有信息    
        networkResponse.body().close();
   
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
 ...
}

连接池

频繁的建立和断开的Socket连接(三次握手、四次挥手)是非常消耗网络资源的,HTTP协议中报文属性Keep-Alive对降低延迟,提升效率很有帮助。

复用连接就要维护一个连接池,并对连接的添加、回收和状态检测进行管理。

  • 复用连接池,RealConnectionPool
//连接池中维护一个双端队列Deque来存储连接
private final Deque<RealConnection> connections = new ArrayDeque<>();

/**
 * maxIdleConnections:每个地址的最大空闲连接数,默认5个
 * keepAliveDuration:连接持续时间,默认5分钟
 */
public RealConnectionPool(int maxIdleConnections, 
  long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " 
          + keepAliveDuration);
    }
}

//连接池中维护一个单线程的线程池,用来清理过期的链接(移出双端队列)。
 private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

//如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
 private final Runnable cleanupRunnable = () -> {
    while (true) {
     //遍历链接并标记空闲连接,全部为活跃链接时,过5分钟再检测清理。
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };
  • 空闲连接的甄别,RealConnectionPool#pruneAndGetAllocationCount
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<Transmitter>> references = connection.transmitters;
    //遍历Transmitter弱引用列表(低版本OkHttp是StreamAllocation)
    for (int i = 0; i < references.size(); ) {
      Reference<Transmitter> reference = references.get(i);
      //若引用不为空,则continue
      if (reference.get() != null) {
        i++;
        continue;
      }

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

推荐阅读更多精彩内容