一:是什么(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<Runnable>(), 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 =
newRequest.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(
newHeaders.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中的<form>标记。
RequestBody formBody =
newFormBody.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 =
newMultipartBody.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"),
newFile(
"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");
privatefinalOkHttpClient client =
newOkHttpClient();
publicvoidrun() throws Exception {
RequestBody requestBody =
newRequestBody() {
@Override
public MediaType contentType()
{
return
MEDIA_TYPE_MARKDOWN;
}
@Override
publicvoidwriteTo(BufferedSink sink) throws IOException
{
sink.writeUtf8(
"Numbers\n");
sink.writeUtf8(
"-------\n");
for
(
inti =
2; i <=
997; i++) {
sink.writeUtf8(String.format(
" * %s = %s\n", i, factor(i)));
}
}
private String factor(int n)
{
for
(
inti =
2; i < n; i++) {
int
x = n / i;
if
(x * i == n)
returnfactor(x) +
" × "+ i;
}
return
Integer.toString(n);
}
};
Request request =
newRequest.Builder()
.url(
"https://api.github.com/markdown/raw")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if(!response.isSuccessful())
thrownewIOException(
"Unexpected code "+ response);
System.out.println(response.body().string());
}
Call
Call对象表示一个已经准备好可以执行的请求,用这个对象可以查询请求的执行状态,或者取消当前请求。它具有以下方法:
Call call=client.newCall(request);
//获取Call对象
Response response=call.execute();
//同步执行网络请求,不要在主线程执行
call.enqueue(
newCallback());
//异步执行网络请求
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<String> 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(),以流的方式读取数据。