Volley框架介绍与源码解析

Volley框架介绍

在开发一些调用服务器端API以获得数据的应用时,需要编写繁琐的基于HttpClient或HttpURLConnection的代码与处理异步请求的线程管理操作,如Android Studio自带的LoginActivity模板,就实现了一个继承自AsyncTask的臃肿内部类来处理一个简单的登录请求。

应运而生的诸多网络通信框架中,Volley的优点包括(翻译自google官方文档):

  1. 自动调度网络请求
  2. 可以并发进行多个网络通信
  3. 可操作的标准HTTP缓存(磁盘、内存)
  4. 支持请求优先级
  5. 提供可以取消一个或多个请求的接口
  6. 可以简易地自定义一些组件
  7. 通过排序从网络异步获取数据,正确地填充UI组件
  8. 提供调试与跟踪工具

总的来说,Volley框架在轻量级与可拓展性上的优越表现让我们可以利用它进行一些频繁但数据量小的网络通信,利用内置的HTTP缓存功能也可以实现图片资源的加载与缓存。

配置

Volley的开源地址为https://github.com/google/volley ,一般可以通过在项目里导入module的方式引入clone到本地的Volley库,或者在项目的build.gradle中加入

dependencies {
    ...
    compile 'com.android.volley:volley:1.0.0'
}

的方式添加对volley的依赖。

框架使用的基本方法

Volley的常规使用方法(来自Google官方文档):

final TextView mTextView = (TextView) findViewById(R.id.text);
...

// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";

// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
           new Response.Listener<String>() {
   @Override
   public void onResponse(String response) {
       // Display the first 500 characters of the response string.
       mTextView.setText("Response is: "+ response.substring(0,500));
   }
}, new Response.ErrorListener() {
   @Override
   public void onErrorResponse(VolleyError error) {
       mTextView.setText("That didn't work!");
   }
});
// Add the request to the RequestQueue.
queue.add(stringRequest);

相对来说调用过程十分简洁,主要的组件包括:用于调度、并发执行多个HTTP请求的RequestQueue,volley库中的Request对象,传入Request对象的Listener用于处理成功请求后的response,传入Request对象的ErrorListener用于执行请求错误/异常后的相关操作。

宏观来看,Volley框架的总体运行与调用结构为(图片转载自http://blog.csdn.net/geolo/article/details/43966171 ):

整体调用结构

可以看到,传入RequestQueue的对象可以是现有的,StringRequest(获得String格式的response),JsonObjectRequest(可以传入一个json对象作为post请求的params,并返回json格式的response),ImageRequest(前文提过volley框架因为缓存的存在对图片资源的加载与管理有很好的表现),实现Request抽象类获得的自定义请求对象,实现方式非常简洁,可以参考官方文档https://developer.android.com/training/volley/request-custom.html 的示例,这里不做赘述。

而RequestQueue管理了两种线程,NetworkDispatcher与CacheDispatcher,分别处理网络请求与缓存管理,内部利用底层的HttpClient或HttpURLConnection实现网络通信,利用磁盘缓存(CacheDispathcer运行时读取文件写入HashMap中由内存管理)实现缓存数据的持久化。

request生命周期

上图是来自Google官方文档的,request对象的生命期示意图,这里进行了主线程、缓存线程、网络线程的区分,首先检查该请求是否命中缓存,若命中则返回缓存结果,若丢失则加入NetworkDispatcher线程池进行网络通信,返回结果、写入缓存。

下面通过阅读volley库中的一些源码来详细解析这个过程的具体实现方式。

Volley源码解析

首先我们一般通过调用Volley.newRequestQueue(Context context)静态方法获得RequestQueue对象的实例(当然可以直接传入RequestQueue需要的一些构造参数对象new出来,自己实现一个单例工厂管理RequestQueue对象是一个不错的选择,适用于经常需要网络通信的应用),方法具体实现为:

/**
 * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
 *
 * @param context A {@link Context} to use for creating the cache dir.
 * @param stack An {@link HttpStack} to use for the network, or null for default.
 * @return A started {@link RequestQueue} instance.
 */
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

    String userAgent = "volley/0";
    try {
        String packageName = context.getPackageName();
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
        userAgent = packageName + "/" + info.versionCode;
    } catch (NameNotFoundException e) {
    }

    if (stack == null) {
        if (Build.VERSION.SDK_INT >= 9) {
            stack = new HurlStack();
        } else {
            // Prior to Gingerbread, HttpUrlConnection was unreliable.
            // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
        }
    }

    Network network = new BasicNetwork(stack);

    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();

    return queue;
}

对于RequestQueue需要的两个参数,Cache接口与Network接口,volley库中自带了BasicNetwork与DiskBasedCache作为一种具体实现类。

首先判断设备的版本号是否为9及以上,若是则使用HurlStack作为Network实现类的参数,否则使用HttpClientStack,前者内部封装的网络通信组件为HttpURLConnection,后者为HttpClient,这两个基本组件的使用场景与android的版本迭代过程有关,因此这里不再做细致介绍。

public interface Network {
    /**
     * Performs the specified request.
     * @param request Request to process
     * @return A {@link NetworkResponse} with data and caching metadata; will never be null
     * @throws VolleyError on errors
     */
    NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

Network接口提供了performRequest方法,很直观地传入request获得response的过程,在BasicNetwork中,利用作为参数传入的HttpStack接口,调用底层的网络通信组件(前段已有介绍)获得response,并添加了加入至缓存(后文会解释)、异常处理、记录长时请求、尝试重请求等封装好的功能。

public interface Cache {
Entry get(String key);

    void put(String key, Entry entry);

    void initialize();

    void invalidate(String key, boolean fullExpire);

    void remove(String key);

    void clear();

    class Entry {

        public byte[] data;

        public String etag;

        public long serverDate;

        public long lastModified;

        public long ttl;

        public long softTtl;

        public Map<String, String> responseHeaders = Collections.emptyMap();

        boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

}

Cache接口提供了一个内部类Entry作为缓存的元数据,其中包括一个存储responseHeaders的Map,事实上Volley框架提供的缓存功能便是根据response的headers信息来管理缓存的刷新周期、有效时长等信息,实现HTTP标准缓存。DiskBasedCache利用读写文件实现了基于磁盘的缓存持久化,CacheDispatcher线程会调用intialize()方法加数据从文件加载到内存的HashMap中,并对每个请求进行cache是否命中的操作。

Volley框架的这种设计方法支持了开发者对Cache与Network接口的自定义实现与扩展,我在这只对库中自带的两个实现类简单介绍,底层的实现方式并非是框架运作流程的重点所在,因此不再贴上繁琐的代码与分析以免浪费阅读者的时间。

回到之前在Volley中创建的RequestQueue对象,在创建它的实例后,首先调用其start()方法:

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

之前的介绍中提过,NetworkDispatcher与CacheDispatcher均为由ReuquestQueue管理的线程,他们都继承自Thread类,同时运作的有一个CacheDispatcher与多个NetworkDispatcher(默认4个)。可以看到两种线程都传入了mNetworkQueue(Request对象作为范型的优先队列)与mCache的引用,因此它们可以对每个加入的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);
    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.
    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();
        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<>();
            }
            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.
            mWaitingRequests.put(cacheKey, null);
            mCacheQueue.add(request);
        }
        return request;
    }
}

以上为处理新加入的request的方法,首先根据request.shouldCache()判断请求是否可以加入缓存,若不可以直接加入mNetworkQueue,若可以则加mCacheQueue,途中会有一个判断阻塞中的请求是否与新请求相同的过滤判断。

接下来回到NetworkDispatcher与CacheDispatcher线程,看看他们都在执行什么工作:

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

以上是CacheDispatcher的run()方法,首先initialize()持有的Cache对象,将缓存读入内存,接下来在while(true)的轮询中,每次从mCacheQueue队列中取出一个request,分别判断是否命中缓存、缓存是否过期,若未命中或过期则加入mNetworkQueue,否则直接返回缓存结果。

    public void run() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    while (true) {
        long startTimeMs = SystemClock.elapsedRealtime();
        Request<?> request;
        try {
            // Take a request from the queue.
            request = mQueue.take();
        } catch (InterruptedException e) {
            // We may have been interrupted because it was time to quit.
            if (mQuit) {
                return;
            }
            continue;
        }

        try {
            request.addMarker("network-queue-take");

            // If the request was cancelled already, do not perform the
            // network request.
            if (request.isCanceled()) {
                request.finish("network-discard-cancelled");
                continue;
            }

            addTrafficStatsTag(request);

            // Perform the network request.
            NetworkResponse networkResponse = mNetwork.performRequest(request);
            request.addMarker("network-http-complete");

            // If the server returned 304 AND we delivered a response already,
            // we're done -- don't deliver a second identical response.
            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                request.finish("not-modified");
                continue;
            }

            // Parse the response here on the worker thread.
            Response<?> response = request.parseNetworkResponse(networkResponse);
            request.addMarker("network-parse-complete");

            // Write to cache if applicable.
            // TODO: Only update cache metadata instead of entire record for 304s.
            if (request.shouldCache() && response.cacheEntry != null) {
                mCache.put(request.getCacheKey(), response.cacheEntry);
                request.addMarker("network-cache-written");
            }

            // Post the response back.
            request.markDelivered();
            mDelivery.postResponse(request, response);
        } catch (VolleyError volleyError) {
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            parseAndDeliverNetworkError(request, volleyError);
        } catch (Exception e) {
            VolleyLog.e(e, "Unhandled exception %s", e.toString());
            VolleyError volleyError = new VolleyError(e);
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            mDelivery.postError(request, volleyError);
        }
    }
}

以上为NetworkDispatcher的run()方法,同样是通过while(true)轮询,每次从mNetworkQueue中取出request,调用Network接口的处理请求方法,获得response对象,后续自然也有相应的异常处理、错误码处理、response交给request的parseNetworkResponse()解析等操作,到这里流程就已经比较清晰了,解析操作大部分情况自带的StringRequest与JsonObjectRequest已经足够,volley也支持直接重写该方法,可以参考上文链接中的示例。

源码的解析有些杂乱,宏观上还是线程的管理实现异步请求,加上一个与计算机中的二级缓存机制十分相似的缓存层实现,可以回顾一下之前的request生命周期图,再次理解下三种线程的相互关系。

虽然写了很多,但volley框架使用起来非常简洁,功能也很完善,之后有心情也许会给出一些具体应用项目中的实现场景(你就是想应付第三次博客啊喂!)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容