设计新的Android HTTP请求封装类
由于公司项目需要,需要对现有的okhttp请求工具类进行整理,另外添加新的上传和下载文件的方法,我整理了近几个项目里面用到的一些东西,对新的工具类列举了一下需要达到的目标
- 使用面向对象方式进行设计,以请求类为操作对象,方便进行扩展
- 上传和下载文件具有进度变化通知
- 网络不同状态使用不同的配置,包括请求重试次数,请求超时时间
- 主要支持异步方式,同步方式不再推荐使用
- 内置网络缓存,是否缓存数据由请求调用者控制
- Dns查询失败自动重试
- 异步请求支持取消操作
- 支持请求头的添加
其中Dns自动重试,异步请求,和请求重发的已经应用到现有的工具类,其他的功能也在近几天一一实现了,这里就简单介绍下各个功能的设计和实现方法。
得益于使用okhttp,我们可以方便的进行异步的网络请求,设置请求超时和重试,方法网络上说明的也很多了,不再重施笔墨,这里只探讨下其他几个功能。代码暂不释出,不是小气,大家都是有动手能力的,所以此处只将思路托出。
Dns查询失败自动重试
okhttp的拦截器功能非常强大,可以帮助我们在调用方无感知的情况下进行先处理网络请求中出现的情况,在拦截器中捕捉到UnknowHostException,然后对请求域名进行查询,
//查询域名对应的ip地址
InetAddress.getByName(domainName).getHostAddress();
查询到IP地址之后把原请求的域名替换成IP再次处理,问题就解决了。
通常Dns解析异常是带有一定偶然因素的,手动解析一次就避免了一次用户可见的网络问题。
请求取消
请求取消是okhttp自带的功能,okhttp的网络调用以Call为载体,只要调用call.cancel() 就可以了,还有更方便的,通过tag取消,等等
上传文件,下载文件
下载文件的流处理就比较常见了,比较容易实现进度通知,而对于上传就没有那么简单了,okhttp在请求一级已经隐藏掉了流的特性,使得我们只能采用更好的方法。在网上查阅了一下,发现有大神的实现http://blog.csdn.net/sbsujjbcy/article/details/48194701[原载地址],借鉴了其中RequestBody,发现okio的流处理真是太强大了.之后又扩展了一下回调的接口,我觉得接口应该包括进度通知,和服务器响应(当然包括异常,不过现在已经不再希望上层来处理异常,而是把异常类型包装成报文返回给上层)。
请求头处理
这里要说一下,现在切实感觉到静态类的调用方式有点low了,发起一个请求需要把所有的参数传入,如果又有额外的参数需求就只能再加一个新的请求方法,实在是不合理。
因此决定按照面向对象的方法来设计,需要设置请求头,简单,类中加个变量,保存需要设置的请求头,然后请求头中加上处理这个变量的操作就OK,真的好很多。感觉之前好low。
缓存
最后的重头戏,也就是请求缓存的处理,请求有需要缓存数据的,也有不需要缓存数据的,怎么办?简单,因为我们已经面向对象了,加个变量,是否缓存数据,缓存的回调也干脆再加一个变量,缓存回调与网络请求的回调完全无关。这样在有缓存的时候不管网络是否可用,是否连接成功,是否服务器返回正常的数据,我们都有缓存可以读。
okhttp是自带缓存的,也就是DiskLruCache,当然这个也是定制之后的,存取已经变成了对Request和Response的操作,数据流也变成了Sink和Source,不过这样也好。
但是简单看了一下okhttp内部对缓存的处理,发现其中的缓存机制是典型的HTTP协议的缓存机制,虽然很强大,但是并不是我们想要的啊。我们需要能够缓存我们需要的数据,包括post请求和get请求,而不关心缓存的数据是不是与服务器一致,也不关心服务器是否希望我们缓存数据,所以只能我们手动进行管理了。
折腾了两三天,踩了很多坑,Response写入cache也好说,从cache读出Response也好说,只是有一个问题,Response只能读取一次,这就不好处理了。看了okhttp内部的实现,一开始没搞懂,仔细琢磨了才明白,他也是ForwardingSink类似的实现,不是在写缓存的时候去读Response,而是在读Response的时候去写缓存,真心牛啊。
好歹照猫画虎,这个缓存的功能就这样实现了。
关键的写缓存的代码
private static Response saveCache(Response response1,Request request){
DiskLruCache.Editor editor = null;
try {
editor = cache.edit("123456");
if (editor == null) {
return response1;
}
Sink sink = editor.newSink(0);
Sink body;
final DiskLruCache.Editor finalEditor = editor;
body = new ForwardingSink(sink) {
@Override public void close() throws IOException {
super.close();
finalEditor.commit();
}
};
Response userResponse = response1.newBuilder()
.request(request)
.priorResponse(stripBody(response1.networkResponse()))
.cacheResponse(stripBody(response1.cacheResponse()))
.networkResponse(stripBody(response1.networkResponse()))
.build();
final BufferedSource source = userResponse.body().source();
final BufferedSink cacheBody = Okio.buffer(body);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
// cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close(); // The cache response is complete!
}
return -1;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
@Override public Timeout timeout() {
return source.timeout();
}
@Override public void close() throws IOException {
if (!cacheRequestClosed
&& !discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
// cacheRequest.abort();
}
source.close();
}
};
return userResponse.newBuilder()
.body(new RealResponseBody(userResponse.headers(), Okio.buffer(cacheWritingSource)))
.build();
}catch (Exception e){
e.printStackTrace();
}
return response1;
}
注释掉的两行暂时没发现有什么影响,不过应该对写缓存无碍
后记
又琢磨了一下,对于一些处理应该充分利用okhttp自身的优势,像请求的重试,在拦截器内部实现会更好,另外还有请求日志的打印,这也是有成熟的范例的,非常适合使用拦截器。
最后只想说,生命在于折腾。
欢迎拍砖。