阅读 428

常用轮子之Okhttp基本使用及原理

不忘初心 砥砺前行, Tomorrow Is Another Day !

相关文章

本文概要:

  1. 基本使用
  2. 基本原理

一. 基本使用

okhttp的请求和响应大多数采用建造者模式设计.

1. GET同步请求

public void syncGetRequest() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        final OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                .get()
                .url(url)
                .build();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = okHttpClient.newCall(request).execute();
                    Log.d(TAG, "syncGetRequest: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).star
复制代码

2. GET异步请求

public void asyncGetRequest() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        final OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .get()
                .url(url)
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "Callback-Thread: " + Thread.currentThread());
                String result = response.body().string();
                Log.d(TAG, "asyncGetRequest: " + result);
                //通过handler或者runOnUiThread方式切换线程.
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        idtv.setText("jasonhww");
                    }
                });
                /*
                 *如果获取的是文件/图片
                 */
                //方式一:通过获取流
                //InputStream inputStream = response.body().byteStream();
                //方式二:通过获取字节数组
                //byte[] bytes = response.body().bytes();
            }
        });
    }
复制代码

注意事项:

  • Callback的回调方法是在子线程执行的,如需更新UI,可通过runjOnUiThread或者handler切换到主线程.

3. POST提交表单

可以使用FormBody.Builder构建一个表单请求体

  
    public void asyncPostForm(){
        String url = "http://api.k780.com/";
        final OkHttpClient okHttpClient = new OkHttpClient();
        //构建表单请求体
        RequestBody requestBody = new FormBody.Builder()
                .add("app","weather.future")
                .add("weaid","1")
                .add("appkey","10003")
                .add("sign","b59bc3ef6191eb9f747dd4e83c99f2a4")
                .add("format","json")
                .build();
        //创建POST请求
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "asyncPostForm: "+response.body().string());
            }
        });
    }
复制代码

4. POST提交JSON字符串

指定请求体的媒体类型为"application/json"


    public void asyncJson() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        String json = "{code:1,result:null}";
        OkHttpClient okHttpClient = new OkHttpClient();
        //指定媒体类型
        MediaType mediaType = MediaType.parse("application/json,charset=utf-8");
        RequestBody requestBody = RequestBody.create(mediaType, json);
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "asyncJson: " + response.body().string());
            }
        });

    }

复制代码

5. 缓存

缓存使用相对比较简单,只需指定一下缓存目录及大小即可.

 //伪代码
 Cache cache = new Cache(cacheDirectory,cacheSize);
 OkHttpClient okHttpClient = new OkHttpClient.Builder()
                      .cache(cache)
                      .build();
 
 
复制代码

6. 拦截器

通过拦截器我们可以很方便的去修改请求和响应的相关信息,如修改请求头,请求体等.

  • 应用拦截器
    拦截应用层与okhttp之间的请求和响应
  • 网络拦截器
    拦截okhttp与网络层之间的请求和响应,此时网络连接已经建立
public class LoggingInterceptor implements Interceptor {
    private static final String TAG = "LoggingInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException {
        //获取请求
        Request request = chain.request();
        //打印url,连接状态,请求头
        Log.d(TAG, "LoggingInterceptor: url = " + request.url() +
                "\nconnectionStatus = " + chain.connection() +
                "\nrequestHeaderInfo = " + request.headers());
        //执行请求
        Response response = chain.proceed(request);
         //打印url,响应头
        Log.d(TAG, "LoggingInterceptor: url = " + response.request() +
                "\nresponseHeaderInfo = " + response.headers());
        return response;
    }
}


public class NetInterceptor implements Interceptor {
    private static final String TAG = "LoggingInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException {
    
        Request request = chain.request();
        Response response = chain.proceed(request);
        //演示设置响应头
        CacheControl.Builder builder = new CacheControl.Builder()  
        .maxAge(10,TimeUnit.MINUTES);
        return response.newBuilder()
                        .header("Cache-Control",builder.build().toString)
                        .build();
    }
}



复制代码

    public void asyncCacheInterceptor() {
        String url = "http://api.k780.com/?app=weather.future&weaid=1&&appkey=10003" +
                "&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json";
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())//应用拦截
                .addNetworkInterceptor(new NetInterceptor())//网络拦截器
                .build();

        //通过拦截器修改请求头
        Request request = new Request.Builder()
                .get()
                .header("user-Agent","Interceptor example")
                .url(url)
                .build();

        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "asyncCacheInterceptor: " + response.body().string());
            }
        });
 
    }
    
复制代码

二. 基本原理

采用OkHttp源码版本为3.8.1

1. OkHttpClient的创建

OkHttpClient作为okhttp的入口,一般将OkHttpClient采用单例模式.
初始化两种方式

  1. 采用直接"new"的方式
  2. 采用建造者模式

对应源码

//内部也是通过建造者进行初始化
OkHttpClient okhttpClient = new OkHttpClient();

OkHttpClient okhttpClient = new OkHttpClient.Builder()
                            .build();
复制代码

通过初始化okhttpclient,完成对许多成员变量的初始化工作.

对应源码

 OkHttpClient(Builder builder) {
    //分发器对象,记录请求执行情况,内部维护一个线程池来执行异步请求
    this.dispatcher = builder.dispatcher;
    this.proxy = builder.proxy;
    this.protocols = builder.protocols;
    this.connectionSpecs = builder.connectionSpecs;
    //应用拦截器集合
    this.interceptors = Util.immutableList(builder.interceptors);
    //网络拦截器集合
    this.networkInterceptors = Util.immutableList(builder.networkInterceptors);
    this.eventListenerFactory = builder.eventListenerFactory;
    this.proxySelector = builder.proxySelector;
    //Cookie瓶
    this.cookieJar = builder.cookieJar;
    //磁盘缓存
    this.cache = builder.cache;
    this.internalCache = builder.internalCache;
    this.socketFactory = builder.socketFactory;
   
   //HTTPS相关成员变量初始化
    ......
    
    }
复制代码

2. Call的创建

当okhttpclient初始化后,再通过okHttpClient.newCall(request),创建一个Call对象,最终实际返回了一个RealCall对象.

对应源码

@Override public Call newCall(Request request) {
    
    return new RealCall(this, request, false /* for web socket */);
  }
复制代码
2.1 同步请求时

调用RealCall的execute方法

对应源码

  /**
   * 1. 首先看RealCall的execute方法.
   */
@Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    try {
      //实际调用了分发器的executed方法,传入RealCall对象.
      client.dispatcher().executed(this);
      //最终通过拦截器链获取响应.
      //最终通过拦截器链获取响应.
      //最终通过拦截器链获取响应.
      //重要的话说三遍.
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }
  
/**
 * 2. 接着看dispatcher的executed方法
 */
synchronized void executed(RealCall call) {
    //标识已经执行了请求
    runningSyncCalls.add(call);
  }
复制代码
2.1 异步请求时

同样调用RealCall的enqueue方法

对应源码

  /**
   * 1.首先看RealCall的enqueue方法.
   */
@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    //实际调用了分发器的enqueue方法,传入AsyncCall对象
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
  
  /**
   * 2.接着看dispatcher对象的enqueue方法.
   */
  synchronized void enqueue(AsyncCall call) {
    //检测请求是否超过最大请求数和一个host对应最大的请求数
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //标识已经执行了请求(这里和同步方法一样操作,可以对比上一点方法)
      runningAsyncCalls.add(call);
      //使用线程池执行请求
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }
  
   /**
    * 3.最后我们看AsyncCall的实现.
    */
   //AsyncCall是RealCall的内部类,继承自NamedRunnable
   //NamedRunnable的实现比较简单就是修改线程名.提供一个execute方法在run用调用,这里就不再晒出具体源码了.
   //AsyncCall的实现主是获取响应,进行回调.
   
   final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    //....省略部分代码
    
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        //最终通过拦截器链获取响应.
        //最终通过拦截器链获取响应.
        //最终通过拦截器链获取响应.
        //重要的话说三遍.
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
           //回调失败
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          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 {
        client.dispatcher().finished(this);
      }
复制代码

最后将Realcall对同步与异步请求处理流程做个小结:

  • 同步时.

    1. 调用RealCall的execute,实际调用了分发器的executed方法,传入RealCall对象.
      • 分发器的executed方法,仅仅标识已经执行了请求.
    2. 最终通过拦截器链获取响应.
  • 异步时.

    • 调用RealCall的enqueue,实际调用了分发器的enqueue方法,传入AsyncCall对象.
      • 分发器的executed方法,先检查请求,满足则标识已经执行了请求,最终使用线程池执行请求.不满足则进人等待队列
    • AsyncCall的实现
      • AsyncCall是一个NamedRunnable子类
      • 通过拦截器链获取响应,回调成功与失败.

3.拦截器链的构建与启动

整个okhttp的核心设计之处

  1. 遵循单一原则,每个拦截器只做一项处理.
  2. 拦截器之间通过拦截器链(RealInterceptorChain)进行连接,构建出处理请求和响应的流水链.

请求通过层层拦截器处理,最后发送出去,反之响应.

对应源码

 //通过拦截器链获取响应
 Response getResponseWithInterceptorChain() throws IOException {
 
    /**
     * 初始化拦截器集合
     */
    List<Interceptor> interceptors = new ArrayList<>();
    //添加7种类型拦截器
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    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, null, null, null, 0, originalRequest);
    //启动拦截器链
    return chain.proceed(originalRequest);
  }
  
  /**
   * 拦截器链的启动
   */
    public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
      
      //......省略部分代码
      
      
    // 创建一个新的拦截器链,拦截器位置指向将index+1.即下一个.
    
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    
    //通过下标index获取当前要执行的拦截器    
    Interceptor interceptor = interceptors.get(index);
    
    //执行拦截器intercept方法
    //如果当前拦截器执行完后,则调用传入的next(新链对象)的proceed方法,执行下一个拦截器.依次类推.
    Response response = interceptor.intercept(next);

    
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

   
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
    }
复制代码

这7种拦截器执行顺序为:

  1. 应用拦截器
  2. retryAndFollowUpInterceptor
  3. BridgeInterceptor
  4. CacheInterceptor
  5. ConnectInterceptor
  6. 网络拦截器
  7. CallServerInterceptor

其中带下划线的拦截器,就是我们之前自定义的拦截器.接下来就依次讲解7种拦截器的作用.

3.1 应用拦截器

不再讲述

3.2 retryAndFollowUpInterceptor

处理错误重试和重定向

对应源码

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true;
      try {
        //执行下一个拦截器,获取响应
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        //满足条件,则重试
        continue;
      } catch (IOException e) {
      // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        //满足条件,则重试
        continue;
      } finally {
        //出现异常释放资源,continue在finally语句块执行后才执行
        // We're throwing an unchecked exception. Release any resources.

        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
      
     //检测是否满足定向的要求代码
     ......
     
      //重新赋值为重定向的请求
      request = followUp;
      priorResponse = response;
    }
  }
复制代码
3.3 BridgeInterceptor

桥接应用层和网络层

A. 将请求进行深加工,使其成为真正请求

  • 对一些请求头的设置

B. 并且也对网络响应做响应处理.

  • 解压缩,去掉不必要的响应头

对应源码

@Override public Response intercept(Chain chain) throws IOException {
    //获取请求
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
    //对一些请求头的设置.
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      //如果没有配置,默认配置gzip.获取响应时需要解压缩.
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
    
    //执行下一个拦截器获取响应
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      //用于解压缩
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      //返回应用层去掉不需要的响应头
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }
复制代码
3.4 CacheInterceptor

承担缓存的查找和保存职责

A. 不需要网络请求,缓存可用,直接返回

B. 缓存不可用,执行下一个拦截器获取响应;
如果用户配置了需要缓存,将响应写入缓存,并返回.

对应源码

 @Override
  public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //检查缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //networkRequest不为空,需要发送请求
    Request networkRequest = strategy.networkRequest;
    //cacheResponse不为空,则缓存可用
    Response cacheResponse = strategy.cacheResponse;

   //......省略部分代码

    // If we don't need the network, we're done.
    //不需要网络请求,缓存可用,直接返回
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      //缓存不可用,执行下一个拦截器获取响应
      networkResponse = chain.proceed(networkRequest);
    } finally {
      //......省略部分代码
    }
    //......省略部分代码
    
    //包装网络响应
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    
    //用户配置了缓存
    if (cache != null) {
      //判断条件
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        //网络响应写入缓存,并返回
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }
复制代码
3.5 ConnectInterceptor

给请求提供一个连接

  • TCP的连接,HTTPS的连接全部在此完成.

对应源码

 @Override 
 public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //获取HTTP编码解码器
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    //获取一个链接
    RealConnection connection = streamAllocation.connection();
    //执行下一个拦截器
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
复制代码
3.6 网络拦截器

不再讲述

3.7 CallServerInterceptor

将请求发送出去

对应源码

 @Override 
  public Response intercept(Chain chain) throws IOException {
   
    //.......省略部分代码
  
    //发送网络请求,httpCodec则是在上一个ConnectInterceptor拦截器获取到的
    httpCodec.finishRequest();

    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }
    
    //构建网络响应对象
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      //写入响应体
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }
复制代码

由于本人技术有限,如有错误的地方,麻烦大家给我提出来,本人不胜感激,大家一起学习进步.

关注下面的标签,发现更多相似文章
评论