Volley源码分析

Dec 24 2014

Volley是一个可以让你在Android应用中以非常简单的方式来发送http请求并且处理服务器返回的数据,这样你就可以花更多的时间关心你的业务实现了

前段时间用了一下Volley,发现非常好用,就很好奇Volley是怎么实现的,因为它是Google的人开发的,也好奇大牛们写的代码都是怎么样的?抱着学习的心态来分析一下Volley的源码,也可以为我以后的工作中积累一些好用的解决方案。以后我也会看一下Android-async-http-client是如何来处理发送http请求这类问题的

Volley的使用可以看这篇博客:Volley库的使用介绍 ,你可以在这篇博客中找到更多关于Volley的学习资料:Android开发资源

首先说明一下,在Volley中每发送一个请求都需要创建一个Request对象,然后将这个Request放入一个RequestQueue中,然后Volley会自动帮你处理这个Request,你只需要处理返回的结果即可

从Volley的源码中可以看出,Volley就是实现了一个生产者消费者问题(Producer–consumer problem)。从源码里看,一个消费者(CacheDispatcher)也可以是下一个消费者(NetworkDispatcher)的生产者

##0x0 Volley架构
总的来说Volley库的架构非常清晰,源代码也非常好理解,Volley的架构可以用下面这张图来说明:

volley

这张图是Google I/O 2013 大会上演讲的图,我是从Youtube的视频上截下来的。

从这张图上可以很清晰的看见,一个Request被放到RequestQueue中后,马上被CacheDispatcher处理,从名字就可以看出来这是检查Cache的,如果Cache中有数据并且没过期的话Volley就直接将数据返回了

如果CacheDispatcher中没有数据或,数据已经过期了,那么Volley就通过NetworkDispatcher发送一个http请求来获取最新的数据,从图中可以看出有多个线程来发送http请求,也就是说Volley可以同时发送多个http请求

##0x1
上面将了Volley处理一个Request的大致流程,那么从源码角度来看Volley是怎么处理的呢?

###0x10 Request
首先来看一下Volley的Request类的签名:

public abstract class Request<T> implements Comparable<Request<T>>

Request是一个范型,其中T表示这个Request返回的数据类型,并且这个Request是可以相互比较的(实现Comparable接口),Request类实现的Comparable接口就是比较两个Request的priority

Volley自带的Request包含了一下几个:

  • ImageRequest
  • JsonArrayRequest
  • JsonObjectRequest
  • JsonRequest
  • StringRequest
  • ClearCacheRequest

从字面上可以基本看出每个Request是用来干什么的。

你也可以自定义一个Request<T>,比如说你可以定义一个GsonRequest<T>,当Volley发送一个请求后返回的是一个Java对象(通过Gson将json字符串自动转换成一个Java对象),这样的话写代码就非常方便了

自定义一个Request<T>需要实现Request类的两个抽象方法:

  • Response<T> parseNetworkResponse(NetworkResponse response)
  • void deliverResponse(T response)

第一个方法用来说明如何解析这个Response,说白了就是将NetworkResponse转换成Response对象。比如ImageRequest类就是需要将reponse数据转换成一个Bitmap对象

第二个方法是用来告诉Volley如何传递这个解析以后的结果,一般是用一个Response.Listener<T>回调接口来传递这个结果

##0x2 RequestQueue
上面讲了如何使用Request类,现在讲讲RequestQueue

创建一个RequestQueue,需要4个参数

  • Cache:Volley使用的缓存,默认是DiskBasedCache(这个Cache为了减少gc,自己实现了一个简单的序列化方法)
  • Network : 这个是RequestQueue用来发送http请求的工具,后面会详细讲
  • 发送http请求的线程数(默认是4个):NetworkDispatcher 线程数量
  • ResponseDelivery :用于传递Response,一般是被CacheDispatcherNetworkDispatcher用于传递从Cache中拿到的或Network返回的HTTP Response

####RequestQueue包含的其他东西
上面讲的是Volley的RequestQueue可以传入的参数,一个RequestQueue还需要以下东西才能正常运行:

AtomicInteger mSequenceGenerator 是一个序列号生成器,它为每一个加入到队列中的Request打上一个序列号

PriorityBlockingQueue<Request<?>> mCacheQueue 表示在Cache队列中的Request,从最上面的图中可以看到,一个Request被加到队列中后,它首先就进入mCacheQueue

PriorityBlockingQueue<Request<?>> mNetworkQueue 表示等待通过Network发送HTTP请求的Request队列。这个和上面的mCacheQueue都是一个优先队列,每一个加入到优先队列中的Request都会按照它的优先级进行排序(如果优先级一样的话,就通过mSequenceGenerator生成的序号进行排序,这部分代码在Request.compareTo中可以看到)

Set<Request<?>> mCurrentRequestsRequestQueue当前正在执行或正在等待的Request,在mCacheQueuemNetworkQueue中的Request都会被存放到mCurrentRequests列表中。这个列表中的Request可以用来执行RequestQueue.cancelAll操作

Map<String, Queue<Request<?>>> mWaitingRequests 这个对象也很有意思,这个map的key是Request的cacheKay,value是一个对应这个cacheKey所有的Request

###0x20 创建一个RequestQueue

一般创建一个RequestQueue的方法就是通过下面这句代码:

RequestQueue queue = Volley.newRequestQueue(Context);

当然,如果你需要自定义的话,就可以通过RequestQueue的构造函数自己new一个RequestQueue。一般来说,一个app中有一个RequestQueue就够了,没必要生成多个队列,生成多个队列会浪费系统的线程资源

在创建一个RequestQueue的时候,Volley已经充分帮我们考虑了一些事情

Android中用来发送HTTP请求的类有HttpUrlConnectionAndroidHttpClient,一般会使用前者,因为Google对它做了很多优化,但是在Android Gingerbread(Android 2.3,API LEVEL = 9)以前,HttpUrlConnection会有一些bug,那么应该使用后者。那为什么不直接使用后者呢?因为Google对前者做了很多优化…=。=

Volley对此也做了相应的判断,Android 2.3以前使用AndroidHttpClient,Android 3.0之后使用HttpUrlConnection,这样就会有相对更好的性能,更节省流量(看这里)

Network接口对他们进行了封装,对RequestQueue而言只有Network接口,但是真正发送请求的地方在HurlStack.performRequestHttpClientStack.performRequest方法中,前者使用HttpUrlConnection,后者使用AndroidHttpClient

####好处
RequestQueue只依赖于Network接口,而不依赖于实现,它不必关心Network.performRequest具体的实现方式

###0x21 启动和停止RequestQueue

RequestQueue生成以后就需要调用RequestQueue.start方法启动

RequestQueue.start方法就是启动每一个Dispatcher(每一个Dispatcher就是一个线程)

/**
 * Starts the dispatchers in this queue.
 */
public void start() {
    stop();  // Make sure any currently running dispatchers are stopped.
    // Create the cache dispatcher and start it.
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();

    // Create network dispatchers (and corresponding threads) up to the pool size.
    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

同理RequestQueue.stop方法就是停止每一个Dispatcher,代码就不贴了

###0x22 向RequestQueue中添加Request

每创建一个Request都需要通过RequestQueue.add方法添加到队列中。调用RequestQueueadd方法,这就是生产者消费者问题中,生产者(producer)向池子(queue)里面存放物品(goods),然后消费者从池子(queue)里拿东西

先看代码:

/**
 * Adds a Request to the dispatch queue.
 * @param request The request to service
 * @return The passed-in request
 */
public <T> Request<T> add(Request<T> request) {
    // Tag the request as belonging to this queue and     add it to the set of current requests.
    request.setRequestQueue(this);
    // A
    synchronized (mCurrentRequests) {
        mCurrentRequests.add(request);
    }

    // Process requests in the order they are added.
    request.setSequence(getSequenceNumber());
    request.addMarker("add-to-queue");

    // If the request is uncacheable, skip the cache queue and go straight to the network.
    // B
    if (!request.shouldCache()) {
        mNetworkQueue.add(request);
        return request;
    }

    // Insert request into stage if there's already a request with the same cache key in flight.
    synchronized (mWaitingRequests) {
        String cacheKey = request.getCacheKey();
        // C
        if (mWaitingRequests.containsKey(cacheKey)) {
            // There is already a request in flight. Queue up.
            Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
            if (stagedRequests == null) {
                stagedRequests = new LinkedList<Request<?>>();
            }
            stagedRequests.add(request);
            mWaitingRequests.put(cacheKey, stagedRequests);
            if (VolleyLog.DEBUG) {
                VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
            }
        } else {
            // Insert 'null' queue for this cacheKey, indicating there is now a request in flight.
            // D
            mWaitingRequests.put(cacheKey, null);
            mCacheQueue.add(request);
        }
        return request;
    }
}

上面代码就是添加一个Request到队列中的全部操作。我加了A、B、C、D关键处标记,

  • A : 首先,将Request加入到mCurrentRequests,表示有一个Request将要被处理,然后这是这个Request的序列号和一个测试标记

  • B : 检查这个Request是否可以被Cache,如果不可以,就直接放入mNetworkQueue中,由NetworkDispatcher处理,直接发送HTTP请求

  • C : 接下来,表示Request都可以被Cache的,检查Request的cacheKey字段,如果存在相同cacheKey的Request,就表示可以不处理这个Request了,直接拿别的Request的结果即可,这样可以减少相同Request的发送,减少流量消耗~

  • D : 如果C不满足的话,就将这个Request放入mCacheQueue队列中,由CacheDispatcher来处理

###0x23 如何传递Response

CacheDispatcherNetworkDispatcher拿到Response以后(不管是Cache中的还是请求HTTP返回的),都会通过一个叫做ResponseDelivery的东西传递出去。

其中,传递HTTP Response真正的地方就在ResponseDeliveryResponseDeliveryRunnable类中,下面是ResponseDeliveryRunnable类的run方法:

 @Override
 public void run() {
     // If this request has canceled, finish it and don't deliver.
     if (mRequest.isCanceled()) {
         mRequest.finish("canceled-at-delivery");
         return;
     }

     // Deliver a normal response or error, depending.
     if (mResponse.isSuccess()) {
         mRequest.deliverResponse(mResponse.result);
     } else {
         mRequest.deliverError(mResponse.error);
     }

     // If this is an intermediate response, add a marker, otherwise we're done
     // and the request can be finished.
     if (mResponse.intermediate) {
         mRequest.addMarker("intermediate-response");
     } else {
         mRequest.finish("done");
     }

     // If we have been provided a post-delivery runnable, run it.
     if (mRunnable != null) {
         mRunnable.run();
     }
}

这个方法就会检查这个Response,如果Response是成功的,那么就通过Request.deliverResponse方法传递。

在本文最上面可以看到,Request.deliverResponse是一个抽象方法,每个Request都需要自己实现处理Response的代码,一般都是通过调用Response.Listener 接口来处理结果

##0x3 CacheDispatcher

CacheDispatcher是一个专门用来检查Cache的线程,它其实既是消费者也是生产者。它需要以下几样东西:

  • BlockingQueue<Request<?>> mCacheQueue : 等待检查Cache 的Request队列
  • BlockingQueue<Request<?>> mNetworkQueue : 等待发送HTTP请求的Request队列
  • Cache mCache : Volley使用的Cache,默认就是DiskBasedCache
  • ResponseDelivery mDelivery :如果检查的Cache符合要求(没有过期),就用这个来传递Response

从上面图中可以看出,CacheDispatcher对于app来说是一个消费者,它从mCacheQueue中拿Request来检查Cache。对NetworkDispatcher来说,又是一个生产者(如果检查的Cache没有或者Cache已经过期,就向mNetworkQueue中添加Request)

下面就是CacheDispatcherrun方法,从源代码里看一看到CacheDispatcher会一直运行,当mCacheQueue中没有需要处理的Request时,此线程会一直被mCacheQueue.take()方法阻塞,直到RequestQueuemCacheQueue添加了一个请求。

@Override
public void run() {
    if (DEBUG) VolleyLog.v("start new dispatcher");
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

    // Make a blocking call to initialize the cache.
    mCache.initialize();

    while (true) {
        try {
            // Get a request from the cache triage queue, blocking until
            // at least one is available.
            final Request<?> request = mCacheQueue.take();
            request.addMarker("cache-queue-take");

            // If the request has been canceled, don't bother dispatching it.
            if (request.isCanceled()) {
                request.finish("cache-discard-canceled");
                continue;
            }

            // Attempt to retrieve this item from cache.
            Cache.Entry entry = mCache.get(request.getCacheKey());
            if (entry == null) {
                request.addMarker("cache-miss");
                // Cache miss; send off to the network dispatcher.
                mNetworkQueue.put(request);
                continue;
            }

            // If it is completely expired, just send it to the network.
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
            }

            // We have a cache hit; parse its data for delivery back to the request.
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");

            if (!entry.refreshNeeded()) {
                // Completely unexpired cache hit. Just deliver the response.
                mDelivery.postResponse(request, response);
            } else {
                // Soft-expired cache hit. We can deliver the cached response,
                // but we need to also send the request to the network for
                // refreshing.
                request.addMarker("cache-hit-refresh-needed");
                request.setCacheEntry(entry);

                // Mark the response as intermediate.
                response.intermediate = true;

                // Post the intermediate response back to the user and have
                // the delivery then forward the request along to the network.
                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) {
            // We may have been interrupted because it was time to quit.
            if (mQuit) {
                return;
            }
            continue;
        }
    }
}

##0x4 NetworkDispatcher

NetworkDispatcherCacheDispatcher非常类似,它也需要4个东西:

  • BlockingQueue<Request<?>> mNetworkQueue : 等待发送HTTP请求的Request队列
  • Network mNetwork : 用于发送HTTP请求的,Network是一个接口
  • Cache mCache : Volley使用的Cache,默认就是DiskBasedCache
  • ResponseDelivery mDelivery :如果检查的Cache符合要求(没有过期),就用这个来传递Response

其中操作的过程也和CacheDispatcher类似,就不贴源码了

##0x5 ImageLoader
上面讲的东西大致就构成了Volley库最核心的东西,一般的请求json或图片是没有太大问题的。但是Google的工程师又想到为我们广大码农减少工作量,增加了ImageLoaderNetworkImageView这两个东西

其实ImageLoader这个工具和Volley库的整个架构是没有太大关系的,它就是为了更加方便的让我们加载网络图片

创建一个ImageLoader需要两个东西:

  • RequestQueue : 这个在上面已经说了好多了
  • ImageCache : 这是一个接口,Volley没有提供具体的实现,需要自己实现。一般都会实现一个基于内存的LruImageCache,网上也有各种栗子。可以参考Picasso源码中的实现

ImageLoader也是对RequestQueue的一个封装,它也会为创建一个Request<ImageView>,然后把这个Request放到RequestQueue中去下载图片

###0x50 在Volley中如何方便的加载一张图片

ImageLoader loader = new ImageLoader(RequestQueue, ImageCache);
NetworkImageView imageView = (NetworkImageView)findViewById(R.id.iv_news_pic);
imageView.setImageUrl(picUrl, loader);

上面3句代码就能够加载网络图片了,是不是很方便。NetworkImageView会计算当前ImageView的宽高,然后创建一个ImageRequest,放入RequestQueue中下载图片。

###0x51 NetworkImageView

NetworkImageView是继承ImageView的,比传统的ImageView多了一个加载网络图片的功能

##0x6 最后
本文就是详细的分析了Volley库的结构了流程。本文觉得这个库设计的非常好,不管是扩展性还是代码可读性都非常好。

RequestQueue就表示Volley运行的整个生命周期,它维护了很多个队列。CacheDispatcherNetworkDispatcher 分别完成检查Cache和发送HTTP请求的工作。当拿到NetworkResponse数据的时候,会调用Request.parseNetworkResponse方法解析Response结果,最后调用ResponseDeleivery对象来传递解析后的Response结果。

Google还在Volley中加入了ImageLoaderNetworkImageView让我们广大码农以更加方便的方式加载网络图片

###最后的最后

Volley的整个架构实现就是最基本的生产者消费者模式(Producer–consumer problem)。这种架构虽然很简单,但是用途也非常广泛,非常值得学习和借鉴!!

也不是说Volley就完美了,最近本人又在学习Picasso的源码。这是一个非常好的专门用于加载图片的库,它不光可以加载网络图片,还可以用它来加载res目录下的图片、SD卡上的图片、Asset目录下的图片、手机图片库中的图片,而且扩展性也非常好!不说了,下次有机会就写一篇关于Picasso的源码分析博客