okhttp源码解析

                             OKHttp

一:是什么(What)

     大家都知道HTTP是现代应用网络的方式。在数据和媒体中进行交换。有效地进行HTTP使您的东西加载更快,并节省带宽。那么OkHttp就是默认情况下高效的HTTP客户端。

二:为什么(Why)

      1:缓存响应数据来减少重复的网络请求

       2:可以从很多常用的连接问题中自动恢复;

      Okhttp处理了很多的网络疑难杂症:会从很多        常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败时,OkHttp会尝试连接下一个IP。

       3:使用起来非常的简单,可扩展性非常的强;

三:用来干什么

      1:支持一般的get请求,post请求。

       2:基于Http的文件上传,文件下载,上传下载的进度回调,

       3:加载图片

       4:支持请求回调,直接返回对象,

       5: 表单请求

四:请求流程

1.     生成一个OkHttpClient(用以总的控制)

2.     用各种键值对包装我们的Request

3.     将请求加入队列生成一个Call管理请求

4.     若是同步请求直接执行excute等待处理返回Response,若为异步则实现Callback回调,在onResponse里获取Response参数

 

真正的请求是从创建Call对象开始的:如图

同步请求返回对象 call

异步请求返回对象 AsyncCall

每个 call对象只能被执行一次,如果想要一个完全一样的 call,可以利用 call.clone() 方法进行克隆。

重点:

异步请求是需要创建线程池的,通过创建线程来进行请求;

分析一下它的源码:

@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  captureCallStackTrace();
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

 

这里我们着重分析下Dispatcher,首先我们需要了解下线程池,以及反向代理模式

线程池技术

相比我们对于异步任务的需求应该遇到了不少,首先想到的便是Thread,handler等异步机制,Java已经做了很好的封装,但是当我们需要使用许多异步任务,而这些任务只做了一点点事情就完成了它的使命,当我们不断的创建,销毁线程的时候,对系统的开销是相当大的,因为这些过程也是耗费时间的,这个时候我们就需要用到线程池了,我们只要往池子里放任务,他就会自动帮你管理线程的创建与销毁,利用缓存复用减少线程销毁创建,优化系统性能。以下是线程池的优点:

1.     通过对线程进行缓存,减少了创建销毁的时间损失

2.     通过控制线程数量阀值,减少了当线程过少时带来的CPU闲置(比如说长时间卡在I\O上了)与线程过多时对JVM的内存与线程切换压力
而Disatcher内部的核心即线程池

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

·        int corePoolSize: 最小并发线程数,这里并发同时包括空闲与活动的线程,如果是0的话,空闲一段时间后所有线程将全部被销毁。

·        int maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理

·        long keepAliveTime: 当线程数大于corePoolSize时,多余的空闲线程的最大存活时间,类似于HTTP中的Keep-alive

·        TimeUnit unit: 时间单位,一般用秒

·        BlockingQueue workQueue: 工作队列

·        ThreadFactory threadFactory: 单个线程的工厂,可以打Log,设置Daemon(即当JVM退出时,线程自动结束)等

反向代理模式

为了解决单生产者多消费者问题,OkHttp采用了反向代理模式,来解决非阻塞,高并发问题

OkHttp工作模式

Dispatch模式

·        int corePoolSize: 最小并发线程数,这里并发同时包括空闲与活动的线程,如果是0的话,空闲一段时间后所有线程将全部被销毁。

·        int maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理

·        long keepAliveTime: 当线程数大于corePoolSize时,多余的空闲线程的最大存活时间,类似于HTTP中的Keep-alive

·        TimeUnit unit: 时间单位,一般用秒

·        BlockingQueue workQueue: 工作队列

·        ThreadFactory threadFactory: 单个线程的工厂,可以打Log,设置Daemon(即当JVM退出时,线程自动结束)等
OkHttp内Dispatcher原理
其实当我们调用client.newCall(request).enqueue(newcallback(){...})的时候实质是下图

synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

此时判断线程池内有无空闲,否则进入等待队列,AsyncCall实质是Runnable,当任务执行完毕后,总会调用finally{
client.dispatcher().finished(this);}清除此任务。
我们回头看看OkhttpClient初始化内其它一些参数Expires,Cache-Control,Last-Modified,If-Modified-Since,ETag,If-None-Match...这些都牵扯到缓存问题

Response response = getResponseWithInterceptorChain();

其实这个方法是返回我们的response对象。到这里整个流程已经完毕。

使用OkHttpClient 的时候需要注意以下几点:

1.最好只使用一个共享的OkHttpClient 实例,将所有的网络请求都通过这个实例处理。因为每个OkHttpClient实例都有自己的连接池和线程池,重用这个实例能降低延时,减少内存消耗,而重复创建新实例则会浪费资源。

2.OkHttpClient的线程池和连接池在空闲的时候会自动释放,所以一般情况下不需要手动关闭,但是如果出现极端内存不足的情况,可以使用以下代码释放内存:

    client.dispatcher().executorService().shutdown();   //清除并关闭线程池
      client.connectionPool().evictAll();                 //清除并关闭连接池
      client.cache().close();       

3.如果对一些请求需要特殊定制,可以使用

OkHttpClient eagerClient = client.newBuilder() 
    .readTimeout(500, TimeUnit.MILLISECONDS)
    .build();

1.  这样创建的实例与原实例共享线程池、连接池和其他设置项,只需进行少量配置就可以实现特殊需求。

 

Request

Request是网络请求的对象,其本身的构造函数是private的,只能通过Request.Builder来构建。下面代码展示了常用的设置项。

Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")                          //设置访问url
        .get()                                                                             //类似的有post、delete、patch、head、put等方法,对应不同的网络请求方法
        .header("User-Agent", "OkHttp Headers.java")                                       //设置header
        .addHeader("Accept", "application/json; q=0.5")                                    //添加header
        .removeHeader("User-Agent")                                                        //移除header
        .headers(new Headers.Builder().add("User-Agent", "OkHttp Headers.java").build())   //移除原有所有header,并设置新header
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();                                                                          //构建request

RequestBody

在Request中使用post、patch等方法时,需要传入一个RequestBody参数,除了上一节讲到的构造方法外,RequestBody还有两个子类:FormBody和MultipartBody。

FromBody用于提交表单键值对,其作用类似于HTML中的

标记。

RequestBody formBody = new FormBody.Builder()                             //提交表单键值对
        .add("platform", "android")                                       //添加键值对
        .add("name", "XXX")
        .add("subject", "Hello")
        .addEncoded(URLEncoder.encode("详细","GBK"),                       //添加已编码的键值对
                    URLEncoder.encode("无","GBK"))
        .add("描述","你好")                                                //其实会自动编码,但是无法控制编码格式
        .build();

使用MultipartBody.Builder可以构建与HTML文件上传格式兼容的复杂请求体。多块请求体中每块请求都是一个独立的请求体,都可以定义自己的请求头。这些请求头应该用于描述对应的请求体,例如Content-Disposition,Content-Length,和Content-Type会自动被添加到请求头中。

RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"title\""),
                RequestBody.create(null, "Logo"))
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"image\""),
                RequestBody.create(MediaType.parse("image/png"), new File("website/static/logo.png")))
        .addFormDataPart("discription","beautiful")
        .build();

了上面这些现成的类和方法以外,还可以用继承RequestBody的方式自定义实现,比如下例所示是以流的方式POST提交RequestBody,其中BufferedSink是Okio的API,可以使用BufferedSink.outputStream()来得到OutputStream。

publicstaticfinal MediaType MEDIA_TYPE_MARKDOWN
  = MediaType.parse("text/x-markdown; charset=utf-8");
privatefinal OkHttpClient client = new OkHttpClient();
publicvoidrun() throws Exception {
RequestBody requestBody = new RequestBody() {
  @Override public MediaType contentType() {
    return MEDIA_TYPE_MARKDOWN;
  }
 
  @Override publicvoidwriteTo(BufferedSink sink) throws IOException {
    sink.writeUtf8("Numbers\n");
    sink.writeUtf8("-------\n");
    for (int i = 2; i <= 997; i++) {
      sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
    }
  }
 
  private String factor(int n) {
    for (int i = 2; i < n; i++) {
      int x = n / i;
      if (x * i == n) return factor(x) + " × " + i;
    }
    return Integer.toString(n);
  }
};
 
Request request = new Request.Builder()
    .url("https://api.github.com/markdown/raw")
    .post(requestBody)
    .build();
 
Response response = client.newCall(request).execute();if (!response.isSuccessful()) thrownew IOException("Unexpected code " + response);
 
System.out.println(response.body().string());
}

Call

Call对象表示一个已经准备好可以执行的请求,用这个对象可以查询请求的执行状态,或者取消当前请求。它具有以下方法:

Call call=client.newCall(request);                //获取Call对象
Response response=call.execute();                 //同步执行网络请求,不要在主线程执行
call.enqueue(new Callback());                     //异步执行网络请求
call.cancel();                                    //取消请求
call.isCanceled();                                //查询是否取消
call.isExecuted();                                //查询是否被执行过

要注意的是,每个Call对象只能执行一次请求。如果想重复执行相同的请求,可以:

Call reCall=client.newCall(call.request());       //获取另一个相同配置的Call对象

Response

Response是网络请求的结果下面是一些常用方法:

Response response=call.execute();                //获取Response对象
response.code();                                 //请求的状态码
response.isSuccessful();                         //如果状态码为[200..300),则表明请求成功
Headers headers=response.headers();              //获取响应头
List names=response.headers("name");     //获取响应头中的某个字段
ResponseBody body=response.body();               //获取响应体

其中ResponseBody代表响应体,用于操作网络请求返回的内容。常用方法如下:

body.contentLength();                           //body的长度
String content=body.string();                   //以字符串形式解码bodybyte[] byteContent=body.bytes();                //以字节数组形式解码body
InputStreamReader reader=body.charStream();     //将body以字符流的形式解码
InputStream inputStream=body.byteStream();      //将body以字节流的形式解码

ResponseBody还有一些注意事项

1.      

ResponseBody必须关闭,不然可能造成资源泄漏,你可以通过以下方法关闭ResponseBody,对同一个ResponseBody只要关闭一次就可以了。

 

Response.close();
Response.body().close();
Response.body().source().close();
Response.body().charStream().close();
Response.body().byteString().close();
Response.body().bytes();
Response.body().string();

 

2.

ResponseBody只能被消费一次,也就是string(),bytes(),byteStream()或 charStream()方法只能调用其中一个。

 

3.

如果ResponseBody中的数据很大,则不应该使用bytes() 或 string()方法,它们会将结果一次性读入内存,而应该使用byteStream()或 charStream(),以流的方式读取数据。

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(okhttp源码解析)