Android网络编程(四)OKHttp使用及封装
前言
在当前的市场环境中,已经很少看到不需要连接网络的APP,网络也可以说是一个APP的"灵魂",所以对于一个Android开发者来说是很有必要学好网络这一块的,个人认为它的重要性是可以和View相比的。而OKHttp可以说是目前Android最流行的网络请求框架,所以我准备开几篇文章详细讲解OKHttp,包括基本使用和封装以及源码的解析。
1 Okhttp概述
在真正讲述Okhttp前我先澄清几个概念,在当前Android开发环境中,存在很多优秀的网络请求方案,从早期的HttpUrlConnection、HttpClient到Volley、AsyncHttp再到Okhttp、Retrofit,很多人问这些框架到底哪个最优秀,笔者个人认为是Okhttp,又有人问了,Retrofit现在不也是很火吗,那它跟Okhttp哪个更优秀呢?其实这二者是不能进行比较的,因为这两个框架根本就不在同一个量级,Okhttp是完全基于HTTP实现的一套网络框架,而Retrofit是基于Okhttp的,说白了Retrofit就是对Okhttp进行了一次封装,跟Okhttp相比的应该是HttpUrlConnection和HttpClient,这三者都是基于HTTP实现的网络框架,属同一量级,而Volley、AsyncHttp、Retrofit只是对以上三种网络框的一种封装,所以笔者认为最好的HTTP请求框架是Okhttp,最好用的网络框架是Retrofit。
Okhttp是由著名的Square公司所贡献的一个开源网络请求框架,它存在的优点有以下几种:
- 支持SPDY
- 对同一主机共享Socket
- 对TCP连接进行复用
- 对数据进行缓存
- 自动处理GZip压缩
Okhttp相比于其他网络框架优势还是挺明显的,今天我先来带大家了解一下Okhttp的基本使用。
2 基本使用
使用前首先进行依赖加入:
compile 'com.squareup.okhttp3:okhttp:3.8.1'
compile 'com.squareup.okio:okio:1.7.0'
构建OkhttpClient
//缓存的文件夹
File fileCache = new File(context.getExternalCacheDir(),"response");
int cacheSize = 10*1024*1024;//缓存大小为10M
Cache cache = new Cache(fileCache, cacheSize);
//进行OkHttpClient的一些设置
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10,TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(10,TimeUnit.SECONDS)
.cache(cache)//设置缓存
.build();
OkHttpClient 一般在一个应用中构建一次即可,所以推荐大家使用单例设计模式,同时OkHttpClient 可构建多种特性,比如上面的读取时间、缓存,还可以构建DNS、代理等等,感兴趣的同学可自行了解。
GET同步请求
Request request = new Request.Builder()
.get()//请求方法
.url("http://www.baidu.com")//url
.build();
final Call call = mOkHttpClient.newCall(request);
new Thread(new Runnable() {
@Override
public void run() {
try {
Response response = call.execute();
if(response.isSuccessful()){
Log.i("okhttp",response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
由于Android中不能再主线程中进行网络IO操作,所以同步请求需要在子线程中进行。
请求结果为百度主页的html信息。
GET异步请求
OKHttp也给开发者提供了异步请求方法
Request request = new Request.Builder()
.get()//请求方法
.url("http://www.baidu.com")//url
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i(TAG,"error "+e.toString() +"threadName:"+Thread.currentThread().getName());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG,"success "+response.body().string() +"threadName:"+Thread.currentThread().getName());
}
});
打印结果
百度主页html.....
threadName:OkHttp http://www.baidu.com/...
我们看到响应方法是在子线程中回调的,所以大家如果需要在主线程中操作响应结果需要进行一次线程切换。
POST表单
POST也分为同步请求和异步请求,使用方式和GET完全一致,为了减少篇幅以下例子我都只进行异步请求。
FormBody.Builder builder = new FormBody.Builder();
//将表单信息添加到FormBody中
builder.add("key1","value1");
builder.add("key2","value2");
builder.add("key3","value3");
//构建请求body
RequestBody body = builder.build();
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
Log.i(TAG,"error:"+e.toString());
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
Log.i(TAG,"success:"+response.body());
}
});
通过FormBody.Builder添加表单信息,最后build()构建请求body,实际的键值对跟url根据自己需求传入即可。
POST传JSON
MediaType mediaType = MediaType.parse("application/json;charset=utf-8");
//传入mediaType和json字符串
RequestBody body = RequestBody.create(mediaType,json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
Log.i(TAG,"error:"+e.toString());
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
Log.i(TAG,"error:"+response.body());
}
});
将MediaType 设置为json,自己封装好准备上传的json字符串即可完成请求
GET下载文件
我们通过GET请求下载一张图片并增加进度提示
Request request = new Request.Builder()
.url(url)
.get()
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
Log.i(TAG,"error:"+e.toString());
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
//下载文件进度条可以直接在onResponse中实现
InputStream is = response.body().byteStream();
//文件的总大小(单位字节)
final long contentLength = response.body().contentLength();
long sum = 0;//当前下载到的字节量
File file = new File(Environment.getExternalStorageDirectory(), "girl.png");
FileOutputStream fos;
try {
fos = new FileOutputStream(file);
//数组越小进度的密度越高
byte[] bytes = new byte[128];
int len = 0;
while ((len = is.read(bytes))!=-1){
fos.write(bytes,0,len);
sum+=len;
Log.i(TAG,"progress="+(float)sum/contentLength);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
});
首先获取到字节流和总长度,然后创建一个图片文件,最后字节写入到图片文件中。在循环写入字节的过程中可以做一个进度提示,byte[]长度越小进度的密度越高。来看一下我的本地结果:
Taylor .PNG
可爱的霉霉被我保存在了本地。
POST上传文件
我们来实现一个基于OKhttp实现一个文件上传功能并显示上传进度。
上传文件的进度显示不像下载中那么简单,我们需要重写RequestBody才能实现,先来看一下代码:
public class ProgressRequestBody extends RequestBody {
private RequestBody mBody;
private ProgressListener mListener;
private ProgressSink mProgressSink;
private BufferedSink mBufferedSink;
public ProgressRequestBody(RequestBody body, ProgressListener listener){
this.mBody = body;
mListener = listener;
}
@Override
public MediaType contentType() {
return mBody.contentType();
}
//写入数据的方法
@Override
public void writeTo(BufferedSink sink) throws IOException {
//将Sink重新构造
mProgressSink = new ProgressSink(sink);
//创建输出流体系
mBufferedSink = Okio.buffer(mProgressSink);
//进行流输出操作
mBody.writeTo(mBufferedSink);
mBufferedSink.flush();
}
@Override
public long contentLength(){
try {
return mBody.contentLength();
} catch (IOException e) {
return -1;
}
}
class ProgressSink extends ForwardingSink {
private long byteWrite;//当前写入的字节
public ProgressSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
//必须执行父类方法,否则无法上传
super.write(source, byteCount);
byteWrite+= byteCount;
if(mListener!=null) {
//更新进度
mListener.onProgress(byteWrite, contentLength());
}
}
}
public interface ProgressListener{
void onProgress(long byteWrite, long contentLength);
}
在ProgressSink
类中的write()
方法中累加已上传的字节量,然后通过接口mListener
进行进度的更新。然后在ProgressRequestBody
中的writeTo()方法中进行流的写操作,BufferedSink
属于OKhttp封装的IO流体系,在之后OKHttp源码分析的文章我会提到,此处大家只需了解即可。写了这么多,其实就只是加了一个进度的回调接口。
上传代码实现:
RequestBody requestBody = RequestBody.//表示任意二进制流
//参数:MediaType类型,需要上传的文件
create(MediaType.parse("application/octet-stream"), file);
//因为是文件参数混合上传,所以要分开构建
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.setType(MultipartBody.FORM);
//params为表单键值对Map
if(params!=null) {
for (Map.Entry<String, String> entry : params.entrySet()) {
//将键值对添加到表单中
builder.addFormDataPart(entry.getKey(), entry.getValue());
}
}
RequestBody multipartBody = builder
//key需要服务器提供,相当于键值对的键
.addFormDataPart("image",file.getName(),requestBody)
.build();
ProgressRequestBody countingRequestBody
= new ProgressRequestBody(multipartBody, new ProgressRequestBody.ProgressListener() {
@Override
public void onProgress(long byteWrite, long contentLength) {
//更新进度
Log.i(TAG,"progress:"+byteWrite/contentLength);
}
});
Request request = new Request.Builder()
.url(url)
.post(countingRequestBody)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
Log.i(TAG,"error:"+e.toString());
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
Log.i(TAG,"success "+response);
}
});
一般上传文件的实际需求中需要附加几个键值对参数,所以代码中我实现了键值对和文件混合上传,其他地方注释写的很清楚,就不多做解释。需要注意的是这些回调方法都是在子线程中进行的,如果要进行UI交互需要切换到主线程。
取消请求操作
Okhttp中也支持请求的取消操作,代码如下:
//根据tag取消单个请求
//最终的取消时通过拦截器RetryAndFollowUpInterceptor进行的
public void cancel(Call call){
//queuedCalls()代表所有准备运行的异步任务
for(Call dispatcherCal1:mOkHttpClient.dispatcher().queuedCalls()){
if(call.request().tag().equals(call.request().tag())){
call.cancel();
}
}
//runningCalls()代表所有正在运行的任务(包括同步和异步)
for(Call dispatcherCal1:mOkHttpClient.dispatcher().runningCalls()){
if(call.request().tag().equals(call.request().tag())){
call.cancel();
}
}
}
//取消全部请求
public void cancelAll(){
mOkHttpClient.dispatcher().cancelAll();
}
请求取消的原理我会在OKHttp源码分析的文章中详细讲解,此处大家会用即可。
Okhttp封装
OKhttp的书写还是挺繁琐的,所以真正使用的时候我需要对其进行封装以便使用,笔者也对OKHttp进行了进行了一个简单的封装,由于代码较长所以就托管在了Github,感兴趣的同学可以进去了解一下。
总结
由于Retrofit的简洁、低耦合并能配合RxJava使用,所以很少有开发者去直接使用OKHttp,但毕竟Retrofit是完全基于OKHttp的,所以还是有必要去掌握一下OKHttp的使用的。本篇文章内容较为简单,旨在让不懂OKHttp的同学对其有一个基本的了解,以方便于我讲述后面的OKHttp源码分析,