Android之解剖网络请求框架Volley

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

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


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;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //实例化Volley全局请求队列
        sRequestQueue = Volley.newRequestQueue(getApplicationContext());
    }
}

使用StringRequest步骤如下:
  1. 初始化RequestQueue。
  2. 创建StringRequest。
  3. 将StringRequest添加到请求队列中。

public class MainActivity extends AppCompatActivity {
   
    StringRequest request;

    @Override
    protected 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>() {
                    @Override
                    public void onResponse(String s) {
                        Logger.json(s);
                    }
                }, new Response.ErrorListener() {
            @Override
            public 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>() {
            @Override
            public void onResponse(String s) {
                Logger.json(s);
            }
        }, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError volleyError) {

    }
}) {
    @Override
    protected 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);
    }

    @Override
    protected 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));
        }
    }

    @Override
    protected 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>() {
            @Override
            public void onResponse(WeatherResp response) {
                Toast.makeText(MainActivity.this, response.city, Toast.LENGTH_SHORT).show();
            }
        }, new Response.ErrorListener() {
    @Override
    public 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.2
            stack = 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() {
        @Override
        public 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")
    @Override
    public 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、从请求队列中获取一个Request
        final 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方法包装成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() {
                @Override
                public 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、从请求队列中获取一个Request
            request = 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方法包装成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");
            }
            //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方法贴在这里:
    @Override
    protected 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() {
    创建缓存目录,默认当前应用缓冲目录下的volley
    if (!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() {
        @Override
        public 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的使用、数据的传递方式、缓存方式、接口编程甚至是设计模式也有了更加清晰的认识。
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页