移动架构学习——手写okhttp架构

依赖

implementation("com.squareup.okhttp3:okhttp:3.14.2")

简单使用例子

               final OkHttpClient client = new OkHttpClient();

       final Request request = new Request.Builder()
                .url("http://www.kuaidi100.com/query?type=yuantong&postid=11111111111")
                .get()
                .build();
       //异步请求
        Call asyncCall = client.newCall(request);
        asyncCall.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.e(TAG, "get响应体: " + response.body().string());
                Log.d(TAG,System.currentTimeMillis()+""+Thread.currentThread().getName());
            }
        });
        //同步请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = null;
                    response = client.newCall(request).execute();//得到Response 对象
                    if (response.isSuccessful()) {
                        Log.d(TAG,"response.code()=="+response.code());
                        Log.d(TAG,"response.message()=="+response.message());
                        Log.d(TAG,"res=="+response.body().string());
                        //此时的代码执行在子线程,修改UI的操作请使用handler跳转到UI线程。
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

Okhttp基本使用步骤如下:

  1. 创建 OkHttpClient 实例
  2. 创建 Request 实例
  3. 通过 OkHttpClient 和 Request 封装成一个请求 Call
  4. 同步请求调用 Call.execute(),异步请求调用 Call.enqueue()

源码分析

new OkHttpClient ()和new Request.Builder()比较简单,跳过这个步骤,从client.newCall(request)开始分析

  /**
   * Prepares the {@code request} to be executed at some point in the future.
   */
  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

Call是一个接口,newCall()方法返回的对象类型是Call的实现类RealCall.也就是说同步执行请求的execute()和异步执行请求的enqueue()实现都在RealCall内.

execute()

  //RealCall.java
  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.timeoutEnter();
    transmitter.callStart();
    try {
      client.dispatcher().executed(this);
      //向服务器发送 request 并获得服务器的 response
      return getResponseWithInterceptorChain();
    } finally {
      client.dispatcher().finished(this);
    }
  }
  //RealCall.java
  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    //创建一个拦截器链
    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 {
      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);
      }
    }
  }

这里引入了拦截器链,Request 需要通过拦截器链接收相应的处理最后才会发送到服务器并获取服务器返回的响应。

拦截器链的添加顺序与拦截器执行顺序一致:

  1. 应用拦截器:开发者添加的拦截器
  2. retryAndFollowUpInterceptor:负责失败重连操作,以及重定向,如果 call 被取消会抛出 IOException
  3. BridgeInterceptor:作为网络层应用层之间的桥梁,把(应用层请求)user request转化为(网络层请求)network request,然后向服务器发送network request,得到(网络层响应)network reseponse后转化为(应用层响应) user response
  4. CacheInterceptor:处理缓存中的 requests 以及把 responses 写到缓存
  5. ConnectInterceptor:负责与目标服务器建立连接
  6. 网络拦截器:开发者添加的拦截器
  7. CallServerInterceptor:拦截器链的最后一个拦截器,负责向服务器发送请求和从服务器获取响应数据

再来看看chain.proceed(originalRequest)的实现原理

 //RealInterceptorChain.java
 @Override public Response proceed(Request request) throws IOException {
    return proceed(request, transmitter, exchange);
  }

  public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
      throws IOException {

    ...

    // Call the next interceptor in the chain.
    //创建执行目标为下一个拦截器的RealInterceptorChain对象
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    //获取当前目标拦截器
    Interceptor interceptor = interceptors.get(index);
    //调用当前拦截器的intercept()并传入下一个拦截器链
    Response response = interceptor.intercept(next);

    ... 
    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
  }

拦截器链的思路和工作原理:

  • 把根据功能细分为多个拦截器。
  • 按照拦截顺序组成一个拦截器链。
  • Reuqest 拦截器链中传递(可被修改)并在每个拦截器中调用Interceptor.intercept()完成各自的功能。
  • 最后返回从服务器获取的响应Response

问题:拦截器链如何而实现按序调用Interceptor.intercept()?

主要涉及两个方法

/** 
* Interceptor
* chain : 下一个拦截器 
*/
Response intercept(Chain chain) throws IOException;
/**
* Interceptor.Chain
* request : 我们传的 request ,可以被拦截器修改,给最后的 CallServerInterceptor 向服务器发送请求
*/
Response proceed(Request request) throws IOException;

1.chain.proceed(request)会创建一个目标拦截器为下一个拦截器的拦截器链InterceptorChain对象 next ,然后调用当前目标拦截器的intercept(next)

2.除了最后一个拦截器,其余拦截器的都在intercept()内直接或间接调用chain.proceed(request)重新回到上一步,然后重复刚才的步骤。这样就达到遍历拦截器链且按照顺序执行拦截器的具体操作的目的。

3.getResponseWithInterceptorChain()返回的是服务器的响应 Response,所以 CallServerInterceptor 必须在拦截器链的最后一个,因为它是真正向服务器发送请求和获取响应的拦截器。

enqueue()

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.callStart();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
public final class Dispatcher {
  //同步请求和异步请求之和最大值
  private int maxRequests = 64;
  //同一个host请求数最大值
  private int maxRequestsPerHost = 5;
  private @Nullable Runnable idleCallback;

  /** Executes calls. Created lazily. */
  //用于执行请求的线程池,且是懒加载的
  private @Nullable ExecutorService executorService;

  /** Ready async calls in the order they'll be run. */
  //等待被执行的异步请求
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  //正在执行的异步请求
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  //正在执行的同步请求,包括已经被取消但未完成的请求
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }

  public Dispatcher() {
  }

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
    //核心线程数为0,最大线程数为Integer.MAX_VALUE,空闲线程最多存活60s
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

...


  void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);

      // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
      // the same host.
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    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 (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

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

  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

  ...
    
}

Dispatcher 是任务调度器,内部建立了一个线程池 ExecutorService ,而且维护了三个集合:

  • readyAsyncCalls : 等待被执行的异步请求集合
  • runningAsyncCalls : 正在执行的异步请求集合,包括已经被取消但未完成的请求
  • runningSyncCalls : 正在执行的同步请求集合,包括已经被取消但未完成的请求

所有异步请求都交由线程池 ExecutorService 来执行。

线程池其实是 ThreadPoolExecutor ,且核心线程数为 0 、最大线程数为Integer.MAX_VALUE、空闲线程存活最大时间为60秒,即所有线程执行完之后空闲60秒就会被销毁,而且存在线程过多导致内存溢出问题等问题,但是在 Dispatcher 的调度下是不会发生线程过多情况的,因为 Dispatcher 限制了正在执行的请求数(同步和异步之和)最大为64,同一个host下请求同时存在数最大值为 5 。

AsyncCall是一个实现了Runnable接口的类,通过线程池调用最终会执行AsyncCall的execute()方法

final class AsyncCall extends NamedRunnable {
    ...
    @Override protected void execute() {
      boolean signalledCallback = false;
      transmitter.timeoutEnter();
      try {
        //通过拦截器链得到从服务器获取的响应 Response
        Response response = getResponseWithInterceptorChain();
        signalledCallback = true;
        responseCallback.onResponse(RealCall.this, response);
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        // 最后要通知 dispatcher 标记该任务已完成
        client.dispatcher().finished(this);
      }
    }
  }

AsyncCall的execute()逻辑很简单,调用getResponseWithInterceptorChain()方法得到Response对象,判断回调onResponse还是onFailure,因为是在线程池中调用的,所以enqueue()方法的回调是在子线程中被调用的,最后还要调用finished()通知Dispatcher该任务已经完成。getResponseWithInterceptorChain()方法在上面已经介绍过了

 /** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    call.callsPerHost().decrementAndGet();
    finished(runningAsyncCalls, call);
  }

  /** Used by {@code Call#execute} to signal completion. */
  void finished(RealCall call) {
    finished(runningSyncCalls, call);
  }

  private <T> void finished(Deque<T> calls, T call) {
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      idleCallback = this.idleCallback;
    }

    boolean isRunning = promoteAndExecute();

    if (!isRunning && idleCallback != null) {
      idleCallback.run();
    }
  }

同步请求和异步请求执行后最终都会调用 dispatcher.finish()

  /**
   * Promotes eligible calls from {@link #readyAsyncCalls} to {@link #runningAsyncCalls} and runs
   * them on the executor service. Must not be called with synchronization because executing calls
   * can call into user code.
   *
   * @return true if the dispatcher is currently running calls.
   */
  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 (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

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

promoteAndExecute起到一个调度作用,将readyAsyncCalls中的任务放到runningAsyncCalls中去,并判断runningAsyncCalls是否超过允许最大值,满足条件则将call交给线程池执行。

手写okhttp架构参考代码:
https://github.com/games2sven/okhttp

附上okhttp的时序图


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

推荐阅读更多精彩内容