转载请标明出处:【顾林海的博客】

个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!

Volley介绍

Volley是Google推出的网络请求库,包含的特性有JSON、图像等的异步下载、网络请求的排序(scheduling)、网络请求的优先级处理、缓存、多级别取消请求、和Activity和生命周期的联动(Activity结束时同时取消所有网络请求),文章会先将Volley的基本使用,最后会从全局者的角度讲解Volley框架的具体流程以及缓存的相关知识。

Volley用法

StringRequest的用法

StringRequest是Request的子类,用于向服务器请求字符串的操作,定义StringRequest之前需要定义请求队列RequestQueue,RequestQueue内部会保存所有的请求,并以相应的算法并发的执行,因此RequestQueue全局定义一个就可以了,避免资源的消耗。这里我把RequestQueue的初始化放在Application中。

public class MyApplication extends Application {//Volley的全局请求队列public static RequestQueue sRequestQueue;/*** @return Volley全局请求队列*/public static RequestQueue getRequestQueue() {return sRequestQueue;}@Overridepublic void onCreate() {super.onCreate();//实例化Volley全局请求队列sRequestQueue = Volley.newRequestQueue(getApplicationContext());}
}

使用StringRequest步骤如下:

  1. 初始化RequestQueue。
  2. 创建StringRequest。
  3. 将StringRequest添加到请求队列中。
public class MainActivity extends AppCompatActivity {StringRequest request;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);request = new StringRequest(Request.Method.GET, "http://www.sojson.com/open/api/weather/json.shtml?city=北京",new Response.Listener<String>() {@Overridepublic void onResponse(String s) {Logger.json(s);}}, new Response.ErrorListener() {@Overridepublic void onErrorResponse(VolleyError volleyError) {}});}public void getWeather(View view) {MyApplication.getRequestQueue().add(request);}
}

如果请求的方式是POST,提交的参数如何传递呢,可以在StringRequest的匿名类中重写getParams()方法,代码如下:

request = new StringRequest(Request.Method.POST, "url",new Response.Listener<String>() {@Overridepublic void onResponse(String s) {Logger.json(s);}}, new Response.ErrorListener() {@Overridepublic void onErrorResponse(VolleyError volleyError) {}
}) {@Overrideprotected Map<String, String> getParams() throws AuthFailureError {HashMap<String, String> params = new HashMap<>();params.put("params", "value");return params;}
};

扩展GsonRequest的用法

Request是抽象类,除了使用StringRequest,我们还可以定义一个GsonRequest继承自Request,使用Gson进行json转实体类操作。

public class GsonRequest<T> extends Request<T> {private final Response.Listener<T> mListener;private Gson mGson;private Class<T> mClass;public GsonRequest(int method, String url, Class<T> clazz, Response.Listener<T> listener,Response.ErrorListener errorListener) {super(method, url, errorListener);mGson = new Gson();mClass = clazz;mListener = listener;}public GsonRequest(String url, Class<T> clazz, Response.Listener<T> listener,Response.ErrorListener errorListener) {this(Method.GET, url, clazz, listener, errorListener);}@Overrideprotected Response<T> parseNetworkResponse(NetworkResponse response) {try {String jsonString = new String(response.data,HttpHeaderParser.parseCharset(response.headers));return Response.success(mGson.fromJson(jsonString, mClass),HttpHeaderParser.parseCacheHeaders(response));} catch (UnsupportedEncodingException e) {return Response.error(new ParseError(e));}}@Overrideprotected void deliverResponse(T response) {mListener.onResponse(response);}}

使用方式:

request = new GsonRequest(Request.Method.GET, "http://www.sojson.com/open/api/weather/json.shtml?city=北京", WeatherResp.class,new Response.Listener<WeatherResp>() {@Overridepublic void onResponse(WeatherResp response) {Toast.makeText(MainActivity.this, response.city, Toast.LENGTH_SHORT).show();}}, new Response.ErrorListener() {@Overridepublic void onErrorResponse(VolleyError error) {}
});

网上对于Volley如何使用的文章非常多,这里给出一部分的使用方式,大家对Volley的使用感兴趣的话,可以查阅相关文章。

Volley框架解读

RequestQueue创建与开启

RequestQueue称为请求队列,顾名思义,所有的请求都会被添加到这个队列中去,通过Volley类的静态方法newRequestQueue(Context context,HttpStack stack)创建RequestQueue。
在RequestQueue类中定义了四个集合属性:

com.android.volley.RequestQueue:
/*** 重复请求集合(当前请求需要缓存数据时,如果当前mWaitingRequests集合中已经存在该请求,* 就将此次请求添加到mWaitingRequests中key为此次请求地址的队列中,等待下次请求)*/
private final Map<String, Queue<Request<?>>> mWaitingRequests =new HashMap<String, Queue<Request<?>>>();/*** 当前请求的队列*/
private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();/*** 缓存队列(存放在该队列中,优先执行缓存调度线程)* 无界的阻塞队列,按任务优先级*/
private final PriorityBlockingQueue<Request<?>> mCacheQueue =new PriorityBlockingQueue<Request<?>>();/*** 网络队列*/
private final PriorityBlockingQueue<Request<?>> mNetworkQueue =new PriorityBlockingQueue<Request<?>>();
  1. 等待队列 mWaitingRequest是以键值对存放的集合(HashMap<String,Queue<Request<?>>>),以请求(Request)的url为key,value是一个队列,内部存放需要缓存数据的请求,并且该请求已经被在缓存队列中。打个比方,我们创建了一个需要缓存数据的Request,第一次添加时会被存放到缓存队列中并交由缓存调度线程执行,如果此次请求没有结束,后续请求同一个url的Request会被存放到mWaitingRequest中以该url为key的队列中等待下一次执行。
  2. mCacheQueue队列存放的是需要缓存数据的Request,使用了优先级队列PriorityBlockingQueue<Request<?>>,队列中存放的Request类必须实现Comparable接口,并通过该接口的copmareTo方法对缓存队列进行排序(按优先级排序,如果优先级相同,按序列号排序)。
  3. mNetworkQueue队列存放的是需要执行网络请求的Request,与mCacheQueue一样使用了优先级队列PriorityBlockingQueue为Request进行排序。
  4. mCurrentRequest集合存放的是所有的请求,通过add(Request<T> request)方法添加。

    知道了RequestQueue中四个重要的集合属性的用途后,我们看看RequestQueue被创建之前需要准备哪些。

com.android.volley.Volley:
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {//缓存目录File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);//AndroidHttpClient实例时的http请求消息头String userAgent = "volley/0";try {String packageName = context.getPackageName();PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);userAgent = packageName + "/" + info.versionCode;//packageName/version} catch (NameNotFoundException e) {}if (stack == null) {if (Build.VERSION.SDK_INT >= 9) {//2.3.2stack = new HurlStack();//HttpURLConnection实现类} else {stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));//HttpClient实现类}}Network network = new BasicNetwork(stack);RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);queue.start();return queue;
}

newRequestQueue方法主要做了以下几件事:
1. 设置缓存数据的目录(磁盘缓存具体实现由DiskBaseCache类实现)。 2. 选取执行网络请求的方式(按照Android版本,低于2.3.2选择HttpClient,反之选择HttpURLConnection)。 3. 通过RequestQueue的start()方法,开启缓存调度线程和网络调度线程。 4. 最终返回该RequestQueue实例。
Volley的关键实现就是由RequestQueue中start()方法开启的缓存调度线程和网络调度线程,这两种调度线程实现的。我们继续查看RequestQueue中两个重要的属性。

/*** 线程数*/
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;/*** 网络调度线程组*/
private NetworkDispatcher[] mDispatchers;/*** 缓存调度线程*/
private CacheDispatcher mCacheDispatcher;
  1. mDispatchers是一个数组,默认长度为4,内部存放网络调度线程NetworkDispatcher,该类继承自Thread类,并实现 run()方法,主要处理网络请求队列中的Request。
  2. mCacheDispatcher是缓存调度线程,CacheDispatcher也继承自Thread类,并实现run()方法,主要处理缓存队列中的请求。

    ##数据的分发处理

    Volley中所有的Request请求,无论是从缓存中获取数据,还是通过网络请求,都是在子线程中操作的,那么这里就引出一个问题,数据获取到后是如何刷新界面的,我们都知道子线程是不能操作UI的,那Volley是如何处理的呢?

数据的分发处理

Volley中所有的Request请求,无论是从缓存中获取数据,还是通过网络请求,都是在子线程中操作的,那么这里就引出一个问题,数据获取到后是如何刷新界面的,我们都知道子线程是不能操作UI的,那Volley是如何处理的呢?
在上面RequestQueue的构造器中看的mDelivery初始化时传入了UI线程的Handler,也就是说在Volley中子线程刷新UI是通过ExecutorDelivery类来实现的,内部是通过Looper.getMainLooper()获取UI线程的Handler并发送数据的。
ExecutorDelivery的职责是数据的分发,实现了ResponseDelivery接口:

public interface ResponseDelivery {public void postResponse(Request<?> request, Response<?> response);public void postResponse(Request<?> request, Response<?> response, Runnable runnable);public void postError(Request<?> request, VolleyError error);
}

ResponseDelivery接口定义了三个方法,前两个方法是请求成功后数据的分发,最后一个是错误信息的分发。ExecutorDelivery实现了ResponseDelivery接口的三个方法,用于数据的分发,那么这三个方法实现我们有必要了解下,ExecutorDelivery 到底是如何通过Handler来分发数据的。

com.android.volley.ExecutorDelivery:private final Executor mResponsePoster;@Override
public void postResponse(Request<?> request, Response<?> response) {postResponse(request, response, null);
}@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {request.markDelivered();request.addMarker("post-response");mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}@Override
public void postError(Request<?> request, VolleyError error) {request.addMarker("post-error");Response<?> response = Response.error(error);mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
}

很显然,mResponsePoster是一个线程池,通过mResponsePoster.execute(Runnable command)方法执行Runnable的run()方法,上面的ResponseDeliveryRunnable就是一个实现了Runnable接口的类并实现了run()方法,说好的通过Handler来执行数据的分发,那么Handler的数据分发是在哪里执行的呢?其实在ExecutorDelivery的构造器中初始化mResponsePoster时通过实现Executor接口的execute(Runnable command)方法,并在这个方法中通过handler.post(Runnable r)进行数据的分发。

com.android.volley.ExecutorDelivery:private final Executor mResponsePoster;public ExecutorDelivery(final Handler handler) {mResponsePoster = new Executor() {@Overridepublic void execute(Runnable command) {handler.post(command);}};
}

通过给mResponsePoster传入ResponseDeliveryRunnable实例,这个实例最后被handler的post方法处理,归根究底缓存数据和网络请求到的数据都是通过ExecutorDelivery类来分发的,而数据分发的手段是通过Handler来实现,这是因为获取缓存数据和网络请求都是在子线程中操作的,在子线程中并不建议操作UI。
ResponseDeliveryRunnable是ExecutorDelivery的内部类,最后请求结果Response交由它的run()方法实现。

private class ResponseDeliveryRunnable implements Runnable {private final Request mRequest;private final Response mResponse;private final Runnable mRunnable;public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {mRequest = request;mResponse = response;mRunnable = runnable;//null}@SuppressWarnings("unchecked")@Overridepublic void run() {if (mRequest.isCanceled()) {mRequest.finish("canceled-at-delivery");return;}//响应结果if (mResponse.isSuccess()) {//通过Request的deliverResponse方法发请求结果mRequest.deliverResponse(mResponse.result);} else {//通过Request的deliverError方法分发请求结果mRequest.deliverError(mResponse.error);}//判断当前Request是否结束if (mResponse.intermediate) {mRequest.addMarker("intermediate-response");} else {mRequest.finish("done");}if (mRunnable != null) {mRunnable.run();}}
}

run方法中如果请求被设置成取消状态就调用执行取消操作不分发数据,在请求没有被取消的情况下,请求成功,通过Request的deliverResponse方法分发数据,请求失败通过Request的deliverError方法分发错误信息。数据分发完,判断Response的intermediate状态,如果intermediate为true,执行mRunnable.run()(执行缓存调度线程中,在需要刷新数据的情况下,Request会被添加到网络请求队列中,这时intermediate 会被设置成true,这里的添加操作被放入Runnable的run()方法中,并将Runnable实例通过ExecutorDelivery的postResponse方法传入),否则执行取消操作。
大家是否还记得使用Volley进行网络请求时,会先创建一个Request,创建时会传入两个监听,一个是请求成功```Response.Listener```,另一个是请求失败Response.ErrorListener(),请求成功的Listener会在Request的deliverResponse()方法中执行回调,而请求失败的Listener会在Request的deliverError方法中执行回调,可以查看具体的StringRequest类。

com.android.volley.toolbox.StringRequest:
@Override
protected void deliverResponse(String response) {mListener.onResponse(response);
}

错误监听的回调在StringRequest的父类Request类中:

com.android.volley.Request:
public void deliverError(VolleyError error) {if (mErrorListener != null) {mErrorListener.onErrorResponse(error);}
}

StringRequest中的deliverResponse和deliverError方法用于数据的回调,而这两个方法的调用就是通过ExecutorDelivery类来操作的。由此整个数据的分发已经很清晰了.
到了这里我们知道了请求队列中的四个队列的作用,以及缓存调度线程和网络调度线程的作用,最后讨论了数据分发类ExecutorDelivery的实现原理。那么接下来看看缓存调度线程类CacheDispatcher和网络调度线程类NetworkDispatcher的具体实现。

缓存调度线程

com.android.volley.CacheDispatcher:
run()方法:while (true) {try {//1、从请求队列中获取一个Requestfinal Request<?> request = mCacheQueue.take();request.addMarker("cache-queue-take");//2、判断当前Request是否被设置为取消状态if (request.isCanceled()) {//2.1结束请求request.finish("cache-discard-canceled");continue;}//3、从磁盘中获取缓存Cache.Entry entry = mCache.get(request.getCacheKey());if (entry == null) {//3.1、本地缓存空,将Request添加到网络请求队列中,重新执行网络请求request.addMarker("cache-miss");mNetworkQueue.put(request);continue;}//4、判断缓存是否过期(根据服务器响应头中获取设置的)if (entry.isExpired()) {//4.1、如果缓存已经过期了,将Request添加到网络请求队列中,重新执行网络请求request.addMarker("cache-hit-expired");request.setCacheEntry(entry);mNetworkQueue.put(request);continue;}request.addMarker("cache-hit");//5、磁盘中获取的数据会通过Request的parserNetworkResponse方法包装成ResponseResponse<?> response = request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders));request.addMarker("cache-hit-parsed");//6、判断当缓存数据是否需要刷新(根据服务器响应头中获取设置的)if (!entry.refreshNeeded()) {//6.1、不需要刷新原始数据,调用ExecutorDelivery响应传递类的postResponse方法// 进行结果的分发mDelivery.postResponse(request, response);} else {request.addMarker("cache-hit-refresh-needed");//6.2、会将老的数据呈现给用户,再进行网络请求,这样优化用户体验request.setCacheEntry(entry);//6.3、将Request添加到网络请求队列中,重新执行网络请求response.intermediate = true;mDelivery.postResponse(request, response, new Runnable() {@Overridepublic void run() {try {//添加到网络请求队列mNetworkQueue.put(request);} catch (InterruptedException e) {// Not much we can do about this.}}});}} catch (InterruptedException e) {if (mQuit) {return;}continue;}
}

CacheDispatcher继承自Thread,也就是说缓存数据获取是在子线程中操作的,通过上图,总结缓存调度线程的执行逻辑:

  • 整个缓存调度线程会不停的从缓存队列中获取Request。
  • 通过判断该Request是否被取消,如果Request已经被设置成取消,那么跳过此次操作,继续从缓存队列中获取Request。
  • 如果Request没有被取消,会判断磁盘中是否存在。
  • 如果磁盘缓存中数据不存在,将Request添加到网络请求队列中,重新执行网络请求并跳过此次操作,继续从缓存队列中获取Request。
  • 如果磁盘缓存中数据存在,判断缓存是否过期。
  • 如果缓存过期,将Request添加到网络请求队列中,重新执行网络请求并跳过此次操作,继续从缓存队列中获取Request。
  • 如果缓存没有过期,判断磁盘缓存数据是否刷新。
  • 如果磁盘缓存数据不需要刷新,通过ExecutorDelivery进行数据的分发。
  • 如果磁盘缓存数据需要刷新,先将旧数据刷新到界面中,然后将Request添加到网络请求队列中,重新执行网络请求。

网络调度线程

com.android.volley.NetworkDispatcher:
@Override
public void run() {//设置当前线程的优先级为后台线程Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);Request<?> request;while (true) {try {//1、从请求队列中获取一个Requestrequest = mQueue.take();} catch (InterruptedException e) {if (mQuit) {return;}continue;}try {request.addMarker("network-queue-take");//2、判断当前Request是否被设置为取消状态if (request.isCanceled()) {request.finish("network-discard-cancelled");continue;}//添加流量监控addTrafficStatsTag(request);//3、网络请求NetworkResponse networkResponse = mNetwork.performRequest(request);request.addMarker("network-http-complete");//4、服务器返回304(资源没有被修改),并且请求已交付if (networkResponse.notModified && request.hasHadResponseDelivered()) {request.finish("not-modified");continue;}//5、网络请求获取的数据会通过Request的parserNetworkResponse方法包装成ResponseResponse<?> response = request.parseNetworkResponse(networkResponse);request.addMarker("network-parse-complete");//6、判断Request是否需要缓存数据并且数据不为空if (request.shouldCache() && response.cacheEntry != null) {//6.1、如果Request需要缓存数据并且请求数据不为空,// 调用DiskBasedCache类的put方法将我们的请求结果保存在磁盘上mCache.put(request.getCacheKey(), response.cacheEntry);request.addMarker("network-cache-written");}//7、请求交付request.markDelivered();//8、分发数据mDelivery.postResponse(request, response);} catch (VolleyError volleyError) {//8.1请求错误,分发错误信息parseAndDeliverNetworkError(request, volleyError);} catch (Exception e) {//8.2发送异常,分发异常信息mDelivery.postError(request, new VolleyError(e));}}
}private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {error = request.parseNetworkError(error);mDelivery.postError(request, error);
}

NetworkDispatcher 继承自Thread,也就是说网络请求是在子线程中操作的,通过上图,总结网络调度线程的执行逻辑:

  • 整个网络调度线程会不停的从网络请求队列中获取Request。
  • 通过判断该Request是否被取消,如果Request已经被设置成取消,那么跳过此次操作,继续从网络请求队列中获取Request。
  • 如果Request没有被取消,进行网络请求。
  • 服务器返回304(资源没有修改),并Request已经交付,结束此次Request,跳过下面的操作,继续从网络请求队列中获取Request。
  • 服务器没有返回304,Request也没有交付,将请求到的数据通过Request的parserNetworkResponse方法包装成Response。
  • 判断Request是否需要缓存,并数据不为空,如果需要缓存,数据也不为空,进行磁盘缓存处理。
  • 磁盘缓存处理后,请求交付,分发数据。
  • 不需要缓存,直接请求交付,分发数据。

缓存

Volley框架的整体流程已经全部讲完,我们回过头来看看Volley的缓存是如何处理,在Volley中使用缓存可以通过Request的setShouldCache(true)方法。

com.android.volley.Request:
/*** 如果设置成false,说明每次请求都会进行网络请求,否则走缓存调度线程。* @return This Request object to allow for chaining.*/
public final Request<?> setShouldCache(boolean shouldCache) {mShouldCache = shouldCache;return this;
}public final boolean shouldCache() {return mShouldCache;
}

给Request的mShouldCache设置为true,随后会在缓存调度线程中判断Request是否需要从缓存中获取数据,以及在网络调度线程中是否需要缓存数据。

网络调度线程中的数据缓存

//3、网络请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");//4、服务器返回304(资源没有被修改),并且请求已交付
if (networkResponse.notModified && request.hasHadResponseDelivered()) {request.finish("not-modified");continue;
}//5、网络请求获取的数据会通过Request的parserNetworkResponse方法包装成Response
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");//6、判断Request是否需要缓存数据并且数据不为空
if (request.shouldCache() && response.cacheEntry != null) {//6.1、如果Request需要缓存数据并且请求数据不为空,// 调用DiskBasedCache类的put方法将我们 的请求结果保存在磁盘上mCache.put(request.getCacheKey(), response.cacheEntry);request.addMarker("network-cache-written");
}

这段代码在上面分析网络调度线程时已经讲过了,这里要回过头重新分析,缓存数据时首先得知道缓存了哪些数据,因此我们得从网络请求也就是第3处往下分析,那么这里的mNetword是BasicNetwork的实例,执行BasicNetwork的performRequest(request)方法时会返回NetworkResponse实例。为了简化代码,这里截取几段performRequest方法部分代码分析。

performRequest(request,headers)方法发起网络请求并返回HttpResponse,后面就是构造NetworkResponse实例,构造实例分两种情况。

  1. 请求到的状态码为304,说明服务器资源没有修改,也间接表明之前请求过了,那么直接使用缓存中的数据,如果缓存中的数据为空,那么NetworkResponse构造函数的第二个参数传null。
  2. 请求到的状态码不为304,直接使用HttpResponse的返回的数据,来构造NetworkResponse实例。

    NetworkResponse的构造器如下:

/*** @param statusCode  http状态码* @param data        相应体* @param headers     返回的响应头或者为null* @param notModified 如果服务器返回了304,数据已在缓存中,则为true,否则false*/
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,boolean notModified) {this.statusCode = statusCode;this.data = data;this.headers = headers;this.notModified = notModified;
}

网络请求到的数据会被转成字节数组,后面可以根据业务需求转换成相应的类型,继续回到网络调度线程的那段代码:

//5、网络请求获取的数据会通过Request的parserNetworkResponse方法包装成Response
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");//6、判断Request是否需要缓存数据并且数据不为空
if (request.shouldCache() && response.cacheEntry != null) {//6.1、如果Request需要缓存数据并且请求数据不为空,// 调用DiskBasedCache类的put方法将我们 的请求结果保存在磁盘上mCache.put(request.getCacheKey(), response.cacheEntry);request.addMarker("network-cache-written");
}

networkResponse实例拿到后,会被传递给Request的parseNetworkResponse(networkResponse)方法中,这里就是各个Request转换具体类型的地方,我们把上面扩展GsonRequest的parseNetworkResponse方法贴在这里:

    @Overrideprotected Response<T> parseNetworkResponse(NetworkResponse response) {try {String jsonString = new String(response.data,HttpHeaderParser.parseCharset(response.headers));return Response.success(mGson.fromJson(jsonString, mClass),HttpHeaderParser.parseCacheHeaders(response));} catch (UnsupportedEncodingException e) {return Response.error(new ParseError(e));}}

Response是个泛型类,泛型参数T就是转换后的具体类型,GsonRequest的paresNetworkResponse方法将response的data转换成String类型,再通过GSON转实体类,HttpHeaderParser是一个Http头信息解析工具类,该工具类会解析头信息中的是否包含Date字段,来判断响应是否来自缓存,将响应中Date首部的值与当前时间进行比较,如果响应中的日期值比较早,客户端通常就可以认为是来自缓存的,除了解析Date字段,还解析了Cache-Control字段中是否包含no-cache或是no-store,如果这两个字段有一个存在或是全部存在,表明数据内容不被存储,以及Expires过期时间、Etag信息等等。最终通过Response的静态方法success包装成Response对象。
Response构造器:

private Response(T result, Cache.Entry cacheEntry) {this.result = result;this.cacheEntry = cacheEntry;this.error = null;
}

result指最终解析类型;cacheEntry保存的是返回数据,包括Http的头信息;error指的是错误信息。

Response的组成部分我们已经知道了,继续回到之前的网络调度线程代码:

//6、判断Request是否需要缓存数据并且数据不为空
if (request.shouldCache() && response.cacheEntry != null) {//6.1、如果Request需要缓存数据并且请求数据不为空,// 调用DiskBasedCache类的put方法将我们 的请求结果保存在磁盘上mCache.put(request.getCacheKey(), response.cacheEntry);request.addMarker("network-cache-written");
}

当Request需要缓存数据就会通过DiskBasedCache的put(String,Entry)方法来存储数据。

@Override
public synchronized void put(String key, Entry entry) {//判断缓存的数据是否超过最大缓存,如果超过最大缓存,就遍历删除文件,直到小于最大缓存数。pruneIfNeeded(entry.data.length);//创建缓存文件(文件头命名规则:url一半字符的哈希值与url后半段字符的哈希值进行拼接)File file = getFileForKey(key);try {FileOutputStream fos = new FileOutputStream(file);CacheHeader e = new CacheHeader(key, entry);//写入相关信息boolean success = e.writeHeader(fos);if (!success) {fos.close();VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());throw new IOException();}//写入请求数据fos.write(entry.data);fos.close();//内存缓存(不包含请求数据)putEntry(key, e);return;} catch (IOException e) {}boolean deleted = file.delete();if (!deleted) {VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());}
}
private void putEntry(String key, CacheHeader entry) {if (!mEntries.containsKey(key)) {//不包含key,总长度累加mTotalSize += entry.size;} else {//包含key,刷新总长度CacheHeader oldEntry = mEntries.get(key);mTotalSize += (entry.size - oldEntry.size);}//保存(entry不包含请求数据data[])mEntries.put(key, entry);
}

Volley并没有做三级缓存,上面代码中的内存缓存存储的是一些头信息,用来判断磁盘缓存是否存在此次请求的数据,代码注释也比较清晰。

缓存调度线程中的缓存数据获取

开启缓存调度线程时会先调用DiskBasedCache的initialize方法进行初始化。

/*** 读取缓存文件的相关头信息*/
@Override
public synchronized void initialize() {创建缓存目录,默认当前应用缓冲目录下的volleyif (!mRootDirectory.exists()) {if (!mRootDirectory.mkdirs()) {VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());}return;}//获取缓存目录下的所有文件File[] files = mRootDirectory.listFiles();if (files == null) {return;}for (File file : files) {FileInputStream fis = null;try {fis = new FileInputStream(file);//读取相关Http头信息CacheHeader entry = CacheHeader.readHeader(fis);entry.size = file.length();//将磁盘缓存的文件信息保存在mEntries中putEntry(entry.key, entry);} catch (IOException e) {if (file != null) {file.delete();}} finally {try {if (fis != null) {fis.close();}} catch (IOException ignored) {}}}
}

初始化的目的主要是查看磁盘缓存了哪些数据,并把缓存文件中的头信息读取出来,并以键值对的形式保存在mEntries中,根据头信息用来判断服务器的数据是否过期,或者是否返回304等等,并根据mEntries中的请求地址判断当前请求是否缓存过。

private final Map<String, CacheHeader> mEntries =new LinkedHashMap<String, CacheHeader>(16, .75f, true);

同时mEntries的类型是LinkedHashMap,内部实现了Lru算法。
从磁盘缓存中获取到相关头信息后,在缓存调度线程中就可以通过以下代码判断,哪些是过期,哪些需要刷新:

//3、从磁盘中获取缓存
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {//3.1、本地缓存空,将Request添加到网络请求队列中,重新执行网络请求request.addMarker("cache-miss");mNetworkQueue.put(request);continue;
}//4、判断缓存是否过期(根据服务器响应头中获取设置的)
if (entry.isExpired()) {//4.1、如果缓存已经过期了,将Request添加到网络请求队列中,重新执行网络请求request.addMarker("cache-hit-expired");request.setCacheEntry(entry);mNetworkQueue.put(request);continue;
}request.addMarker("cache-hit");
//5、磁盘中获取的数据会通过Request的parserNetworkResponse方法包装成Response
Response<?> response = request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");//6、判断当缓存数据是否需要刷新(根据服务器响应头中获取设置的)
if (!entry.refreshNeeded()) {//6.1、不需要刷新原始数据,调用ExecutorDelivery响应传递类的postResponse方法// 进行结果的分发mDelivery.postResponse(request, response);
} else {request.addMarker("cache-hit-refresh-needed");//6.2、会将老的数据呈现给用户,再进行网络请求,这样优化用户体验request.setCacheEntry(entry);//6.3、将Request添加到网络请求队列中,重新执行网络请求response.intermediate = true;mDelivery.postResponse(request, response, new Runnable() {@Overridepublic void run() {try {//添加到网络请求队列mNetworkQueue.put(request);} catch (InterruptedException e) {// Not much we can do about this.}}});
}

上面第4步判断缓存是否过期,如果过期, 就会上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。就像上面这样,过期或刷新会给Request的setCacheEntry方法传入参数Cache.Entry对象,最后会将Request放入网络请求队列中。回顾上面网络请求时,其中有一段代码就是将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器,具体实现在BasicNetwork的performRequest方法代码中。
判断过期或刷新时是否将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器的代码,addCacheHeaders方法如下:

private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {if (entry == null) {return;}if (entry.etag != null) {headers.put("If-None-Match", entry.etag);}if (entry.serverDate > 0) {Date refTime = new Date(entry.serverDate);headers.put("If-Modified-Since", DateUtils.formatDate(refTime));}
}

关于If-Modified-Since 和 If-None-Match含义如下(摘自网络):

If-Modified-Since,和 Last-Modified 一样都是用于记录页面最后修改时间的 HTTP 头信息,只是 Last-Modified 是由服务器往客户端发送的 HTTP 头,而 If-Modified-Since 则是由客户端往服务器发送的头,可 以看到,再次请求本地存在的 cache 页面时,客户端会通过 If-Modified-Since 头将先前服务器端发过来的 Last-Modified 最后修改时间戳发送回去,这是为了让服务器端进行验证,通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回新的内容,如果是最新的,则 返回 304 告诉客户端其本地 cache 的页面是最新的,于是客户端就可以直接从本地加载页面了,这样在网络上传输的数据就会大大减少,同时也减轻了服务器的负担。
If-None-Match,它和ETags(HTTP协议规格说明定义ETag为“被请求变量的实体值”,或者是一个可以与Web资源关联的记号)常用来判断当前请求资源是否改变。类似于Last-Modified和HTTP-IF-MODIFIED-SINCE。但是有所不同的是Last-Modified和HTTP-IF-MODIFIED-SINCE只判断资源的最后修改时间,而ETags和If-None-Match可以是资源任何的任何属性,不如资源的MD5等。
ETags和If-None-Match的工作原理是在HTTP Response中添加ETags信息。当客户端再次请求该资源时,将在HTTP Request中加入If-None-Match信息(ETags的值)。如果服务器验证资源的ETags没有改变(该资源没有改变),将返回一个304状态;否则,服务器将返回200状态,并返回该资源和新的ETags。
ETag如何帮助提升性能?
聪明的服务器开发者会把ETags和GET请求的“If-None-Match”头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
其过程如下:
1.客户端请求一个页面(A)。
2.服务器返回页面A,并在给A加上一个ETag。
3.客户端展现该页面,并将页面连同ETag一起缓存。
4.客户再次请求页面A,并将上次请求时服务器返回的ETag一起传递给服务器。
5.服务器检查该ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304(未修改——Not Modified)和一个空的响应体。

总结

如果阅读的源码十分庞大,这时我们应该分清主次,以主要流程为主,至于一些细节,可以在了解完整体框架思想后再回过头来细细研究。 Volley整体来说,代码不是很多,在阅读完源码后,可以发现很多功能都是以面向接口形式编写的,这样写的好处时便于扩展,比如HttpStack定义了网络请求的具体业务,可以通过实现HttpStack实现自己的网络请求操作,也可以把OkHttp引入其中。 再比如Cache定义了缓存存储的具体细节,可以通过实现Cache接口实现自己的缓存方案。包括模板方法模式Request定义了请求的操作逻辑,将请求到的数据交由子类处理,在子类中可以将数据转换成业务需要的数据样式(String、bitmap、Gson实体类),当然阅读完Volley源码所得到知识不止这些,通过源码,我们对怎样编写一套网络框架有更加清晰的认识,对线程的使用、HttpURLConnection和HttpClient的使用、数据的传递方式、缓存方式、接口编程甚至是设计模式也有了更加清晰的认识。

Android之解剖网络请求框架Volley相关推荐

  1. Android新的网络请求框架volley源码解释及示例

    最近遇到一个问题:我想用HttpClient来访问网络,发现怎么都无法new出HttpClient的对象,这我就有点摸不着头脑了.记得我之前都是可以使用这个类的,怎么突然间就用不了了.因为不知情,一下 ...

  2. Android应用中网络请求库Volley的使用

    接上文,这次来说一下如何使用Volley,会给出一些范例,和原理 Volley使用 StringRequest // 初始化一个请求队列,RequestQueue是volley库的类 RequestQ ...

  3. Android网络请求框架Volley的使用

    Volley是Google在2013年推出的一款网络请求的框架,主要解决数据量小但是请求频繁的情况,对于大流量的比如文件上传下载,流媒体等则表现不佳.一般用于json获取,图片加载. 要想使用voll ...

  4. 一个整合OkHttp 、Retrofit 、Volley 、RxJava、Novate多种开源网络框架的项目,高度的封装和集成,Android中Web网络请求一行代码解决

    一个整合OkHttp .Retrofit .Volley .RxJava.Novate多种开源网络框架的项目,高度的封装和集成,Android中Web网络请求一行代码解决 AndroidHttp 一个 ...

  5. okhttp的应用详解与源码解析--android网络请求框架发展史

    乘5G之势,借物联网之风,Android未来亦可期,Android优势在于开放,手机.平板.车载设备.智能家居等都是Android的舞台,Google不倒,Android不灭,本专栏的同步视频教程已经 ...

  6. Android主流网络请求框架

    一.Volley google推出的异步网络请求框架和图片加载框架.特别适合数据量小,通信频繁的网络操作.android绝大多数都属于这种类型,但是对于数据量比较大的操作,比如:下载,就不太适用了. ...

  7. android post请求_Vue 网络请求框架 axios 使用教程

    点击上方"代码集中营",设为星标 优秀文章,第一时间送达! 前期回顾 1. Vue 学习入门指南 2. Vue 入门环境搭建 3. Visual Studio Code 使用指南 ...

  8. Retrofit网络请求框架使用简析——Android网络请求框架(四)

    题记:-- 很累,累到想要放弃,但是放弃之后将会是一无所有,又不能放弃, 唯有坚持,唯有给自忆打气,才能更勇敢的走下去,因为无路可退,只能前行, 时光一去不复返,每一天都不可追回,所以要更珍惜每一存光 ...

  9. Android网络请求框架之Retrofit(二)

    前面一篇文章介绍了Retrofit的基本用法,没有看过的童鞋可以移步:Android网络请求框架之Retrofit(一),现在我们来继续介绍Retrofit配合RxJava.RxAndroid的用法. ...

最新文章

  1. 百度危矣:乱评程苓峰《360的章鱼手要抢谁家饭碗?》
  2. jquery设置输入框为只读_将SQL中几张表设为只读,这是什么奇怪需求?
  3. web动画_Web动画简介
  4. 【干货】JS版汉字与拼音互转终极方案,附简单的JS拼音输入法
  5. Overflow属性详解(转载)
  6. 给“大学生IT博客大赛”参赛博主的一封信
  7. 江苏计算机类事业编总分多少,必看!江苏事业单位统考三类岗位分值分布
  8. sap销售发货的流程_SAP系统销售流程
  9. 云基础架构|部署分类|IaaS|PaaS|SaaS——浅析云计算架构
  10. Appkey is not configured or configured incorrectly
  11. libguestfs java_rhel7.0(libguestfs) 挂载windows虚机镜像
  12. C语言基本的语法规定
  13. Excel函数大全(Excel Function List)-Part 1
  14. Wireshark抓取TCP三次握手包
  15. JAVA怎么批量更新mysql_java 批量添加、批量更新 操作数据库
  16. 最好的免费WordPress音频播放器插件
  17. Thinkphp 5.1 PC和手机端加载不同路径下的模板~功能实现
  18. kal linux 刻录光盘,kali linux安装之旅
  19. 计算机管理 没有适当的权限,电脑管家没有合适的权限打开是怎么回事?
  20. S7-1200使用集成库FB285对V90伺服驱动器进行速度控制的基本步骤

热门文章

  1. python 人脸识别_手把手教你用python实现人脸识别,识别率高达99.38%
  2. k8s使用glusterfs存储报错type 'features/utime'
  3. Winform控件:打开文件对话框(OpenFileDialog)
  4. PostgreSQL client's startup packet different between logical and normal stream replication
  5. Linux下vsftpd服务器
  6. 微软职位内部推荐-Software Engineer II-Data Mini
  7. 分支1-CentOS6.5下 正/反向域名解析之yum安装/编译安装 的教程
  8. uva 10152 ShellSort
  9. PHP与SQL注入***(实战篇五)
  10. 238. Product of Array Except Self