从 OkHttp 中学点什么

前言

几个月前,跟过 OkHttp 的流程源码,但是时间久了,现在能够回想起来的的,只有几个拦截器了,那我岂不是没什么收获了。所以,好好想想,我从 OkHttp 中能够学到什么

疑问

  1. OkHttp 是怎么拆分功能的,大概有几个模块
  2. 它所用到的责任链模式,在实际开发中适合哪些场景
  3. 它是怎么使用线程池的,这么用有什么好处

OkHttp 是怎么拆分功能的,大概有几个模块

作为一个网络框架,最核心的功能就是发起请求,处理响应了,这俩个是功能部分, OkHttp 使用 Dispatcher 执行任务,内部是一个高并发的线程池,另外整个流程的处理使用到了Interceptor 拦截器

请求执行的调用流程

  • Recall.enqueue(Callback)
  • client.dispatcher().enqueue(new AsyncCall(responseCallback))

停一下,跟进一下分发器器的 enqueue 方法,内部时怎么处理异步请求的

  // 创建 AsyncCall 对象 
  void enqueue(AsyncCall call) {
    synchronized (this) {
    // 添加到待执行队列中,双向队列
      readyAsyncCalls.add(call);
    }
    // 执行的重要代码
    promoteAndExecute();
  }
  
  
    private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));
    // 创建一个可执行队列,目的是限制 64 个最大连接数,每个 Host 最多 5 个连接的限制
    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;
  }

  

首先 AsyncCall 是实现了 Runnable 的一个类,也难怪,毕竟是要放到线程池中执行的

响应执行的调用过程

刚才看了请求入队后,经过筛选,开始遍历放到线程池中去执行,那下来就是等待响应了

具体代码在 Recall 类的 run() 方法中,里面就是我们之前了解到的使用责任链模式-拦截器的代码了

为什么要使用双端队列 ArrayDeque ?

OkHttp 源码系列 之 ArrayDeque - 双端队列

首先 Jdk 提供的双端队列主要有两个:

  1. LinkedList 链表实现的双端队列
  2. ArrayDeque 循环数组实现的双端队列

那为什么偏偏就选了 ArrayDeque 了,因为效率高.
Jdk 的说明中就说了,ArrayDeque 作为队列使用时,将比 LinkedList 更快

ArrayQueue 是线程不安全的,Okhttp 是怎样保证同步问题的? synchronized 关键字

OkHttp 是怎样使用责任链模式的,Android 源码中还有其他地方用到了嘛?

那就首先要补习下责任链模式是什么,以及怎样用代码实现

在网上查了很多文档,有一种表述我觉得很形象,小张去外地出差回来,其中 2w 要去招公司报销,他去找到组长

  1. 组长看到发票,面值超过了权限,说让小张去找主管
  2. 主管一看,自己最大只能签 3k 的,让其去找经理
  3. 经理最大只能批 1w 的,让小张去找老板
  4. 最终老板签字处理

整个流程涉及到多个类(组长、主管、经理等),一级一级的处理,最终处理结束

Android 源码中 View 的事件分发也是使用了责任链模式,其中被分发的 MotionEvent 经过 ViewGroup 层层分发,最终被消费或者重新返回到最上层的 View

Android 事件分发与责任链模式

  • 那 OkHttp 是怎样实现责任链的呢?
    Let is see fucking code ,Woohooo
    // 同步执行的代码中
    @Override protected void execute() {
      boolean signalledCallback = false;
      transmitter.timeoutEnter();
      try {
        // 使用责任链处理请求响应,重点看这里
        Response response = getResponseWithInterceptorChain();
        ......
      }
    }
    
    Response getResponseWithInterceptorChain() throws IOException {
    // 创建一个拦截器队列
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    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 {
      //  proceed 执行后得到响应结果
      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);
      }
    }
  }


这里使用了队列保存所有的拦截器,然后一股脑传进了 RealInterceptorChain 对象中,最后调用 proceed 就有个返回结果,停,一下子就获取到了结果了嘛? 进去看看
RealInterceptorChain 里面是怎么处理的

  public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
      throws IOException {
    ... 省略部分代码    

    // 看到 next 我就想起来链表里的 next,这个 next 是干嘛的呢?
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    // 根据 index 获取对应位置的拦截器
    Interceptor interceptor = interceptors.get(index);
    // 调用拦截器的拦截方法
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed()
   
    ... 省略部分代码    
    return response;
  }

有一个地方没看明白,创建 Chain 后,调用 proceed ,刚开始 index 为 0,那整个队列是怎么遍历的,没看到有循环遍历语句啊

进入 interceptor.intercept(next) 后,一切截然而止了,怀着好奇心,点开了 interceptor 的实现类 CacheInterceptor ,果不其然,在 intercept 方法中,再次看到了 proceed 的身影,终于破案了

 @Override public Response intercept(Chain chain) throws IOException {
  ... 省略部分代码    
  // 每调用一次,内部的 index 自增,就意味着不断的传递到下一级拦截器
  networkResponse = chain.proceed(networkRequest);
  ... 省略部分代码    
  
  return response;

OkHttp 是怎么使用线程池的

最后一个问题,怎么使用线程池的,首先补习下线程池的各个参数的含义,以及线程池的工作原理

  // OkHttp 中的线程池
  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

    // 线程池的构造方法
    // corePoolSize – the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
    // maximumPoolSize – the maximum number of threads to allow in the pool
    // keepAliveTime – when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
    // unit – the time unit for the keepAliveTime argument
    // workQueue – the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
    // threadFactory – the factory to use when the executor creates a new thread
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

创建参数:

  1. 核心线程数,核心线程将保持存活,及时是空闲的,这里是 0
  2. 池中允许的最大线程数,这里是 Int 的最大值,实际到不了这么多,OkHttp 有 64 最大连接数的限制
  3. 等待时间 可以理解为非核心线程等待任务时的超时时间 ,这里为 60 秒
  4. 等待时间的单位
  5. 工作队列,这里是一个同步的阻塞队列,内部没有容器,传入一个时就会阻塞下一个的传入
  6. 线程工厂
  • 为什么 OkHttp 要这么设置线程池,有什么好处呢?
    其实这种参数设置,就是 Excutor.newCachedThreadPool() ,
    首先一个阻塞的同步队列,内部没有容器,意味着什么呢,每当一个网络请求发起,只要核心线程满了,就会在池中创建新的线程

如果线程池中的线程数大于核心线程数且队列满了,且线程数小于最大线程数,则会创建新的线程,刚好 Okhttp 的最大线程数时是一个极大值,那就会不断创建线程,是一个高并发的线程池

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

推荐阅读更多精彩内容