前言
公司最近新起了一个项目,对喜欢尝鲜的我们来说,好处就是我们可以在真实的项目中想尝试一些新技术,验证想法。新项目对网络框架的选取,我们存在三种方案:
1.和我们之前的项目一样,使用Loader + HttpClient + GreenDao + Gson + Fragment
,优点是可定制性强,由于使用Google
家自己的Loader
和LoaderManager
,代码健壮性强。
缺点是整套代码学习成本较高,使用过程中样板代码较多,(比如每一个Request都需要产生一个新类)
2.Volley
,作为Google
在IO
大会上得瑟过的一个网络库,其实不算什么新东西(2013 IO发布),使用较为简单,请求可以取消,可以提供优先级请求,看起来还是不错的。
3.Retrofit
,一款为了使请求极度简单化的REST API Client
,呼声也很高,使用门槛几乎是小白型。
如何选择呢?首先干掉1,因为对新人的学习成本确实太高,如果要快速开发一个项目,高学习成本是致命的,同时使用起来样板代码很多。
那么如何在Volley
和Retrofit
中选择呢?尽管网上有很多文章在介绍两个框架的使用方法,而对于其原理,特别是对比分析较少,如果你手里有一个项目,如何选择网络模块呢?
这里将分两篇文章从源码的角度对比分析这两个开源框架,希望能对你有所帮助。这是上篇,下篇Retrofit在这里,其中有干货总结哦~。
需要注意的是,这两篇文章并不会是一个入门使用的帮助文档,建议你在看本文的时候,最好看过官方文档和DEMO。
首先说明一下这两个网络框架在项目中的层次:
从上图可知,不管Volley
还是Retrofit
,它们都是对现有各种方案进行整合,并提供一个友好,快速开发的方案,在整合过程中,各个模块都可以自行定制 或者替换。比如反序列化的工作,再比如HttpClient。
需要注意一点的是,这两个开源框架都没有实现
Http
栈,Http
栈作为一个可替换的模块在两个框架中存在(Retrofit 2.0版本仅支持OkHttpClient)。Http栈在客户端常见的实现有apache
的HttpClient
和square
的OkHttpClient
,其中apache HttpClient
不开源,而OkHttpClient
是开源的,GitHub地址。
Volley 源码分析
Volley
现在已经被官方放到AOSP
里面,已经逐步成为Android
官方推荐的网络框架。
类抽象
对
Http
协议的抽象
Requeset
顾名思义,对请求的封装,实现了Comparable
接口,因为在Volley
中是可以指定请求的优先级的,实现Comparable
是为了在Request
任务队列中进行排序,优先级高的Request
会被优先调度执行。
NetworkResponse
Http
响应的封装,其中包括返回的状态码 头部 数据等。
Response
给调用者返回的结果封装,它比NetworkResponse
更加简单,只包含三个东西:数据 异常 和Cache
数据。
Network
对HttpClient
的抽象,接受一个Request
,返回一个NetworkResponse
反序列化抽象
所谓反序列化,就是将网络中传输的对象变成一个Java
对象,Volley
中是通过扩展Request
类来实现不同的反序列化功能,如JsonRequest StringRequest
,我们也可以通过自己扩展一些Request
子类,来实现对请求流的各种定制。请求工作流抽象
RequestQueue
用来管理各种请求队列,其中包含有4个队列
a) 所有请求集合,通过RequestQueue.add()
添加的Request
都会被添加进来,当请求结束之后删除。
b) 所有等待Request
,这是Volley
做的一点优化,想象一下,我们同时发出了三个一模一样的Request
,此时底层其实不必真正走三个网络请求,而只需要走一个请求即可。所以Request1
被add
之后会被调度执行,而Request2
和Request3
被加进来时,如果Request1
还未执行完毕,那么Request2
和 Request3
只需要等着Request1
的结果即可。
c) 缓存队列,其中的Request
需要执行查找缓存的工作
d) 网络工作队列 其中的Request
需要被执行网络请求的工作
NetworkDispatcher
执行网络Request
的线程,它会从网络工作队列中取出一个请求,并执行。Volley
默认有四个线程作为执行网络请求的线程。
CacheDispatcher
执行Cache
查找的线程,它会从缓存队列中取出一个请求,然后查找该请求的本地缓存。Volley
只有一个线程执行Cache
任务。
ResponseDelivery
请求数据分发器,可以发布Request
执行的结果。
Cache
对Cache
的封装,主要定义了如何存储,获取缓存,存取依据Request
中的getCacheKey()
来描述。
提交请求
Volley
通过RequestQueue.add(Request)
来往任务队列中增加请求:
一个Request
被提交之后有几个去处:
1.Set
对应所有请求队列。所有调用add
的Request
必然都会添加到这里面来。
2.PriorityBlockingQueue
对应网络队列。如果一个Request
不需要缓存,那么add之后会被直接添加到网络队列中。
3.PriorityBlockingQueue
对应缓存请求。如果一个Request
需要缓存,并且当前的RequestQueue
中并没有一个Request
的getCacheKey
和当前Request
相同(可以认为一个请求),那么加入缓存队列,让缓存工作线程来处理。
4.Map
对应等待队列。如果RequestQueue
中已经有一个相同请求在处理,这里只需要将这个Request
放到等待队列中,等之前的Request
结果回来之后,进行处理即可。
Volley
提交任务到队列中是不是很简单?下面来说说优先级请求的事情吧,你可能已经注意到了,上面两个存放需要执行任务的队列都是PriorityBlockingQueue
,前面说了Request
现实了Comparable
,看看这个方法:
@Override
public int compareTo(Request other) {
Priority left = this.getPriority();
Priority right = other.getPriority();
//mSequence表示请求序列号,add时,会通过一个计数器来指定
return left == right ?
this.mSequence - other.mSequence :
right.ordinal() - left.ordinal();
}
所以,如果我们的工作线程(NetworkDispatcher,CacheDispatcher
)取任务时,自然会从头部开始取。
这里的优先级,仅仅是保证一个请求比另外一个请求先处理,而并不能保证一个高优先级请求一定会比低优先级的请求先回来
缓存工作线程处理
@Override
public void run() {
//初始化Cache
mCache.initialize();
Request> request;
while (true) {
//阻塞 获取一个Cache任务
request = mCacheQueue.take();
try {
//已经被取消
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
//如果拿cache未果,放入网络请求队列
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
mNetworkQueue.put(request);
continue;
}
//缓存超时,放入网络请求队列
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
//根据Cache构造Response
Response> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
//是否超过软过期
if (!entry.refreshNeeded()) {
// 直接返回Cache
mDelivery.postResponse(request, response);
} else {
request.setCacheEntry(entry);
//设置中间结果
response.intermediate = true;
//发送中间结果
final Request> finalRequest = request;
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
//中间结果完事之后,讲请求放入网络队列
mNetworkQueue.put(finalRequest);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}
} catch (Exception e) {
}
}
}
这里可以看到Volley
确实对缓存封装很到位,各种情况都考虑到了,其中比较重要的两点:
- 取出来的
Cache
并不仅仅是数据,同时还包括这次请求的一些Header
- 硬过期 软过期
我们可以看到Cache
中有两个字段来描述缓存过期:Cache.ttl
vsCache.softTtl
。什么区别呢?如果ttl
过期,那么这个缓存永远不会被使用了;如果softTtl
没有过期,这个数据直接返回;如果softTtl
过期,那么这次请求将有两次返回,第一次返回这个Cahce
,第二次返回网络请求的结果。想想,这个是不是满足我们很多场景呢?先进入页面展示缓存,然后再刷新页面;如果这个缓存太久了,可以等待网络数据回来之后再展示数据,是不是很赞?
NetworkDispatcher
执行网络请求的工作线程,默认有4个线程,它不停地从网络队列中取任务执行。
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request> request;
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
// release previous request object to avoid leaking request object when mQueue is drained.
request = null;
try {
request = mQueue.take();
} catch (InterruptedException e) {
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
//取消
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
//通过Http栈实现客户端发送网络请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// 如果缓存软过期,那么会重新走网络;如果server返回304,表示上次之后请求结果数据本地并没有过期,所以可以直接用本地的,因为之前Volley已经发过一次Response了,所以这里就不需要再发送Response结果了。
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
Response> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
//更新缓存
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
//发送结果
request.markDelivered();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}
Request
Request
中主要封装了一个请求的各类Http
协议信息,比如 URL
,请求方法,请求的优先级,请求重试的策略,缓存策略等。
这里说一下其中比较有意思的重发策略,如果一次请求发生超时异常,比如SocketTimeoutException ConnectTimeoutException
,我们可以为Request
配置一个RetryPolicy
,你可以指定重发这个Request的次数,以及每次失败之后重新设置这个请求的超时时间(第一次失败之后,你可以调整第二次请求的超时时间增加,以减少失败的可能性)。
反序列化
Request
最重要的功能就是提供了内容的反序列化,通过不同的子类来实现不同的序列化功能。比如,如果请求结果是一个Json的对象,我们可以使用JsonObjectRequest
,如果是一个普通字符,使用StringRequest
,同时,我们也可以很方便的定制自己的Request,通过复写Response
方法即可。
默认的JsonRequest
使用org.json
中的Json
解析,我们使用Gson
来进行解析能够构造一个更加通用的处理json
返回的Request
:
public class JsonGRequest extends Request {
private static Gson gson = new Gson();
private Response.Listener mListener;
public JsonGRequest(String url, Response.ErrorListener listener,Response.Listener responseListener) {
super(url, listener);
this.mListener = mListener;
}
public JsonGRequest(int method, String url, Response.ErrorListener listener) {
super(method, url, listener);
}
@Override
protected Response parseNetworkResponse(NetworkResponse response) {
return Response.success(gson.fromJson(new InputStreamReader(new ByteArrayInputStream(response.data)),getType()), HttpHeaderParser.parseCacheHeaders(response))
}
@Override
protected void deliverResponse(T response) {
if(mListener != null) {
mListener.onResponse(response);
}
}
//获取指定的泛型类型
protected Type getType() {
Type superclass;
for(superclass = this.getClass().getGenericSuperclass(); superclass instanceof Class && !superclass.equals(JsonGRequest.class); superclass = ((Class)superclass).getGenericSuperclass()) {
;
}
if(superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
} else {
ParameterizedType parameterized = (ParameterizedType)superclass;
return parameterized.getActualTypeArguments()[0];
}
}
}
ImageRequest
Volley
专门为图片请求提供了ImageRequest
,主要是反序列化了一下数据流到BitMa
p,还可以制定图片的大小,质量等参数。
ImageLoader
是Volley
提供的一个用来加载图片的工具,它的内部还是使用ImageRequest
来实现的,主要新加的功能是增加了内存缓存,你可以通过配置ImageCache
来设置内存缓存。
小结
Volley
整体代码还是比较简单,思路明确,而且提供了不错的可扩展性,而且各个方面考虑得较为全面。下面我们分析一下Retrofit
的源码。