探索发现之Okhttp

最近相对空闲,于是利用这段时间看了okhttp。之前在项目中,采用过许多的网络框架。有自己写的,有用android-async-http,最近用的是volley。应该说,除了自己写的,都是非常优秀的调用框架。前段时间,学长推荐我使用okhttp,说用了就会爱不释手的。自己水平有限,写这篇文章出于自我的探索,看了网上一堆牛人的解释,自己也写写心得罢了。(我应该会不定期的更新,因为是个人的摸索,谈不上分析,所以题目这样取比较合理吧)
其实,在android studio中已经有okhttp了
studio的okhttp
我一开始没太注意版本,2.0.0的已经挺久的了,我去github上下了最新的版本是3.2.0的,前后一对比,发现更改的地方还行挺多的,我也不详细说了。(同步请求添加了队列、call类改成接口类,具体实现交给了RealCall类、添加了拦截器链等)
(1)简单用法
我想先从用法入手,这样比较容易理解。
主要的类OkHttpClient,注解是这么说的
注解
从官方的描述里,总结一下OkHttpClient类:
1、帮助构造Call类(用来发送请求和获得返参信息)。
2、一般一个应用使用一个OkHttpClient就可以。
3、创建方法有默认的new OkHttpClient(),或者使用Builder来进行自己配置。如果是要调整已有的OkHttpClient,可以使用newBuilder()获得当前的配置信息。(例子给出的就是一个调整配置获得新的client)

so,我得出通俗的做法就是在Application类的onCreate里进行OkHttpClient的创建和具体配置(对外提供单例方法)。
于是,我尝试几个简单的例子(1、异步的post ;2、同步get)
异步post
同步get

以上便是最最最最简单的使用okhttp的例子。(可以看我用handler,也猜出他的回调是在子线程里,这里我觉得不是特别好。比如在volley里最后会调用Executor.execute方法来使回调在主线程中)下面将一遍分析一遍改进。

(2)同步的流程(初窥)
既然调用成功了,我必然要看看里面的乾坤。从同步的方法看相对会简单一点。
1、首先是调用了OkHttpClient的 newCall(Request request)方法获得一个RealCall类。它是实现了Call接口(只能请求一次,可以理解为一次性用品吧)
newcall
2、代码直接调用call.execute()方法。
execute
首先判断是否已经使用过。接着就是调用client的dispather类的executed(这里只是把请求放入请求队列中,之后分析吧)。接着调用getResponseWithInterceptorChain方法(从名字里就知道会有拦截器链在里头)。获得的返参后就从队列中把当前的Call清除。

(3)异步的流程(初窥)
enqueue
异步方法,前面的都一样。之后调用client的dispatcher.enqueue,并传入回调类。
dispatcher.enqueue
在diapatcher中判断,队列是否到达最大数量或者同一host是否已饱和。
还有容量,就放入执行队列并直接执行(AsyncCall的execute方法,AsyncCall的父类NamedRunnable的run里调用execute)。否,则放入等待队列中。

在其中,也同样是调用getResponseWithInterceptorChain方法。根据回调来判断触发相应的回调方法,并移除call。

(4)拦截器
通过查看,明显在同步与异步中,最终都是调用了getResponseWithInterceptorChain方法。于是,本着理解的目的继续查看。

  private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
    Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
    return chain.proceed(originalRequest);
  }

发现操作意外得简单,构造一个拦截器链并调用链的proceed方法。

@Override public Response proceed(Request request) throws IOException {
      // If there's another interceptor in the chain, call that.
      if (index < client.interceptors().size()) {
        Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
        Interceptor interceptor = client.interceptors().get(index);
        Response interceptedResponse = interceptor.intercept(chain);

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

        return interceptedResponse;
      }

      // No more interceptors. Do HTTP.
      return getResponse(request, forWebSocket);
    }

起初我不太理解,感觉应该是一个一个的拦截器操作才对,这样写感觉很奇怪。我觉得他大概是以类似递归的形式展开。看了后,网上例子都是在调用拦截器是,返参都是chain.proceed,调用下一个拦截器。这样每一个拦截器调用完后,都会紧接着调用下一个拦截器,直到全部调用过。
举一个官方例子
public void networkInterceptorsCanChangeRequestMethodFromGetToPost() throws Exception {
server.enqueue(new MockResponse());
Interceptor interceptor = new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "abc");
return chain.proceed(originalRequest.newBuilder()
.method("POST", body)
.header("Content-Type", mediaType.toString())
.header("Content-Length", Long.toString(body.contentLength()))
.build());
}
};

(5)关于网上的拦截器运用Gzip
Http协议上的gzip编码是一种用于改进Web应用程序性能的技术。运用gzip压缩技术,一是减少储存空间,二是减少传输时间。
网上关于在okhttp中使用gzip是

@Override
public Response intercept(Chain chain) throws IOException {
//从链中获得原始请求
Request originalRequest = chain.request();
//如果没有需要压缩的内容
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
//压缩
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
//传递给下一个
return chain.proceed(compressedRequest);
}

private RequestBody gzip(final RequestBody body) {
        return new RequestBody() {
            @Override public MediaType contentType() {
                return body.contentType();
            }
            @Override public long contentLength() {
                return -1; // 无法知道压缩后的数据大小
            }
            @Override public void writeTo(BufferedSink sink) throws IOException {
                BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
                body.writeTo(gzipSink);
                gzipSink.close();
            }
        };
    }

但我去github上查找时,有外国朋友写的,我觉得可以。他是在上面的基础上再次获得数据的长度。

class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), forceContentLength(gzip(originalRequest.body())))
        .build();
    return chain.proceed(compressedRequest);
  }

  /** https://github.com/square/okhttp/issues/350 */
  private RequestBody forceContentLength(final RequestBody requestBody) throws IOException {
    final Buffer buffer = new Buffer();
    requestBody.writeTo(buffer);
    return new RequestBody() {
      @Override
      public MediaType contentType() {
        return requestBody.contentType();
      }

      @Override
      public long contentLength() {
        return buffer.size();
      }

      @Override
      public void writeTo(BufferedSink sink) throws IOException {
        sink.write(buffer.snapshot());
      }
    };
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

(6)getResponse方法
遍历完拦截器链后,直接调用getResponse方法。大致步骤:
1、根据请求数据添加header信息。
2、创建HttpEngine类。
3、调用HttpEngine.sendRequest发送数据
4、调用HttpEngine.readResponse解析返回数据
5、释放连接
6、HttpEngine.getResponse获得返回信息
7、engine.followUpRequest();常看返回信息是否包含在需要处理的code范围内
8、在内则进行相应处理,否则返回结果

(7)探索HttpEngine
此类处理单一的 HTTP 请求/响应对。每个 HttpEngine遵循这个生命周期:
1、创建
2、sendRequest()时发送请求。(会判断是调网络还是本地存储的,)
3、readResponse()之后,可以获得返回信息的header和body。并且所有的response都有一个response body input stream。
在浏览sendRequest时,它首先获得本地的对应缓存,根据策略,如果不网络也不本地,会返回504。如果本地,则返回本地的。如果是网络, connect()获得连接,之后传数据。而readResponse是将httpStream.writeRequestHeaders和httpStream.writeRequestBody,将数据写入端口,之后读取返回数据readNetworkResponse。

(8)本地缓存
okhttp在发送前先解析请求,获得其类型:是调用网络还是本地。在HttpEngine.sendRequest方法中,先判断和预处理了header后,

InternalCache responseCache = Internal.instance.internalCache(client);
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

明显应该是获得本地的缓存信息。Internal.instance在OKhttpclient创建时就静态创建了。

InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
  }

判断,有cache就用它的internalCache 否则就用Client的(就是默认的null)。根据这个判断,我大致猜测,cache应该是用户的缓存配置(否则cache为什么默认也是null)。OkHttpClient也给出了cache的设置方法:在builder类里有

/** Sets the response cache to be used to read and write cached responses. */
    void setInternalCache(InternalCache internalCache) {
      this.internalCache = internalCache;
      this.cache = null;
    }

    public Builder cache(Cache cache) {
      this.cache = cache;
      this.internalCache = null;
      return this;
    }

看到这里,傻子都知道要设置本地缓存的方法了 == :

    private static OkHttpClient instance;

    /**
     * 获得Client单例
     */
    public static OkHttpClient getInstance() {
        if (instance == null){
            synchronized (OkhttpUtil.class){
                if (instance == null){
                    instance = new OkHttpClient();
                    File cacheDirectory = FileUtil.getCacheDir("OkHttpCache");
                    if (cacheDirectory != null) {
                        int cacheSize = 10 * 1024 * 1024; // 10 MiB File
                        instance.newBuilder().cache(new Cache(cacheDirectory, cacheSize));
                    }
                }
            }
        }
        return instance;
    }

以上就是如何设置缓存的路径了(具体文件创建就不贴出了)。接下来就是如何设置请求的存储类型了。于是接着从源码看呗。

long now = System.currentTimeMillis();
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;

代码紧接着就是分析请求的指向目标。
Factory方法创建CacheStrategy类。如果上一段代码的缓存信息不为空,就修改存储数据的一些必要信息(比如最近一次修改)。
而get方法就是获得请求相依的参数(是否请求本地缓存,还是只调接口不缓存等等。。)代码太多就不贴了。但是这句代码吸引了我。

CacheControl requestCaching = request.cacheControl();

他就是获得每个request的请求缓存参数。这下就明白,每个单独的请求可以设置其cacheControl。于是

 private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,
      boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds,
      int minFreshSeconds, boolean onlyIfCached, boolean noTransform, String headerValue) {
    this.noCache = noCache;
    this.noStore = noStore;
    this.maxAgeSeconds = maxAgeSeconds;
    this.sMaxAgeSeconds = sMaxAgeSeconds;
    this.isPrivate = isPrivate;
    this.isPublic = isPublic;
    this.mustRevalidate = mustRevalidate;
    this.maxStaleSeconds = maxStaleSeconds;
    this.minFreshSeconds = minFreshSeconds;
    this.onlyIfCached = onlyIfCached;
    this.noTransform = noTransform;
    this.headerValue = headerValue;
  }

一看参数太多了。。。。。没事,因为CacheControl的参数太多,源码已经给了几个常用的。一个是FORCE_NETWORK(只请求网络)、FORCE_CACHE(只请求缓存)。默认情况自然是都会操作。

//创建一个Request
        final Request request = new Request.Builder()
                .cacheControl(CacheControl.FORCE_NETWORK)
                .url("http://xxxxxxxxxxxxx")
                .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonObject.toString()))
                .build();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值