Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

解剖网络请求框架Volley

$
0
0

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


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<?>>();


该图为mWaitingRequest集合存放的数据:

这里写图片描述

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<T>,另一个是请求失败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的具体实现。

缓存调度线程


CacheDispatcher流程图:

这里写图片描述


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添加到网络请求队列中,重新执行网络请求。

网络调度线程


NetworkDispatcher流程图:


这里写图片描述


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方法部分代码分析。

这里写图片描述

这里写图片描述

截图中第一段箭头指的是执行mHttpStck的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的使用、数据的传递方式、缓存方式、接口编程甚至是设计模式也有了更加清晰的认识。

作者:GULINHAI12 发表于2017/9/21 16:30:27 原文链接
阅读:0 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>