Android 网络框架 Volley 源码解析

Volley 是 Google 官方推出的一套 Android 网络请求库,特别适用于通信频繁、数据量较小的网络请求。Volley 能够根据当前手机版本选择 HttpClient (2.3 以下) 或者 HttpUrlConnection。

除了 Volley,Android 常用的网络加载库还有 OkHttp,Retrofit 等,关于这几个的区别请移步 stormzhong 的ANDROID开源项目推荐之「网络请求哪家强]

Volley 框架扩展性很强,其源码值得我们好好学习。

先了解一些 Http 协议

Http 协议规范了客户端和服务端数据请求的一套规则。Http 协议规范了 请求(Request)和响应(Response)。

请求包含请求行,请求头(Header),body(可选)。如下图所示(图片来源于网络):


Http Request

请求行必须指定请求方式,常见的有 GET,POST等。

响应和请求类似。也包含三部分,状态行(含响应码),响应头(Header),body(可选)。如下图所示(图片来源于网络):


Http Response

关于 Http 协议大致了解这些即可。

Volley 对 Http Request 和 Response 的封装。

请求封装

Http 请求包含三部分,请求行,请求头,body。Volley 将其封装成 Request,Request 有几个实现类,如 StringRequest,JsonObjectRequest 等。Request 部分源码如下:

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

    public interface Method {
        int DEPRECATED_GET_OR_POST = -1;
        int GET = 0;
        int POST = 1;
        int PUT = 2;
        int DELETE = 3;
        int HEAD = 4;
        int OPTIONS = 5;
        int TRACE = 6;
        int PATCH = 7;
    }
    // 对应请求头中的 Method
    private final int mMethod;

    // 对应请求头中的 URL
    private final String mUrl;
    // 对应请求头
    public Map<String, String> getHeaders() throws AuthFailureError {
        return Collections.emptyMap();
    }
    // 对应请求实体
    public byte[] getBody() throws AuthFailureError {
        Map<String, String> params = getParams();
        if (params != null && params.size() > 0) {
            return encodeParameters(params, getParamsEncoding());
        }
        return null;
    }
}

响应封装

Http 响应包含三部分,状态行,响应头,body。
Volley 对 Http 响应用 NetworkResponse 封装,NetworkResponse 部分源码如下:

public class NetworkResponse {
    /** 状态行中的响应码 */
    public final int statusCode;

    /** body */
    public final byte[] data;

    /** 响应头 */
    public final Map<String, String> headers;

    /** True if the server returned a 304 (Not Modified). */
    public final boolean notModified;

    /** Network roundtrip time in milliseconds. */
    public final long networkTimeMs;
}

NetworkResponse 直观的表示 Http 响应返回的数据。但是我们的应用程序不能直接使用其中的 body(字节数组),需要解析成某个 Bean 对象,这样程序就可以直接使用 Bean 对象。因此还需要对响应做进一步的封装。显然这个 Bean 的数据类型不可知,可以使用 java 中的泛型。

Volley 对响应进一步封装成 Response,Response 部分源码码如下:


public class Response<T> {
  
    /** Parsed response, or null in the case of error. */
    public final T result;

    /** Cache metadata for this response, or null in the case of error. */
    public final Cache.Entry cacheEntry;

    /** Detailed error information if <code>errorCode != OK</code>. */
    public final VolleyError error;
    
}

由 NetworkResponse 到 Response 需要一个方法将 body 解析成某个对象。Volley 将这个方法定义在 Request 中,而且是一个抽象方法。看下 StringRequest 对这个方法的实现:

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

其实就是对返回的字节数组(body)解析成某个对象,StringRequest 则解析成 String,JsonObjectRequest 则解析成 JsonObject 等。我们可以定义一个自己的 Request,在 parseNetworkResponse 方法中将字节数组转成 String,再用 Gson 解析成我们的对象。

发起一个请求

Volley 的简单使用如下:

RequestQueue mQueue = Volley.newRequestQueue(context);

StringRequest stringRequest = new StringRequest("http://www.baidu.com",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.d("TAG", response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e("TAG", error.getMessage(), error);
            }
        });
mQueue.add(stringRequest);

Volley.newRequestQueue(context) 会创建一个 RequestQueue,并调用RequestQueue #start()

RequestQueue 表示请求队列,是 Volley 框架的核心类。RequestQueue 包含一个网络队列 mNetworkQueue 和一个缓存队列 mCacheQueue ,作为其成员变量。

RequestQueue 部分源码如下:

public class RequestQueue {
    /** 缓存队列 */
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
        new PriorityBlockingQueue<Request<?>>();

    /** 网络队列 */
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
        new PriorityBlockingQueue<Request<?>>();  
        
    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();
        }
    }
}

在 start 方法中,开启一个缓存调度线程 CacheDispatcher,用来处理缓存。默认开启四个网络调度线程 NetworkDispatcher,用来处理网络请求。CacheDispatcher 不断的从 mCacheQueue 中取走 Request 并进行分发处理。NetworkDispatcher 不断的从 mNetworkQueue 取走 Request 并进行分发处理。如果队列为空,则阻塞等等,这些线程都是常驻内存随时待命的。显然一个程序中最好只能有一个 RequestQueue,如果采用多个,应该在适当的时候调用 RequestQueue#stop()销毁线程释放内存。

当我们向 RequestQueue 添加一个 Request 时,如果 Request 可缓存则添加到 mCacheQueue ,否则添加到 mNetworkQueue 。CacheDispatcher 拿到这个 Request,如果缓存存在并且还没过期,则解析数据并提及给主线程,否则添加到 mNetworkQueue 交给 NetworkDispatcher 处理。流程如下:


这里写图片描述

缓存如何处理?

app 网络数据缓存

Cache 的实现类为 DiskBasedCache。DiskBasedCache 采用磁盘缓存和内存缓存,但两者缓存的数据不一样。内存缓存只缓存 CacheHeader,而磁盘缓存的是 Entry,只不过是将 Entry 中的数据按一定规则写到文件中,读取缓存时再按照同样的规则读取到 Entry 中。另外,Entry 比 CacheHeader 多了一个字节数组,显然这是比较占内存的,因此内存缓存并没有缓存 Entry。

当缓存满了之后如何处理呢?

DiskBasedCache 中有个方法是pruneIfNeeded(int neededSpace),每次执行 put 的时都会先调用该方法。这个方法就会删除较早的缓存。内存缓存保存在 mEntries 中。我们看下这个成员变量:

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

LinkedHashMap 有个构造函数为 LinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder)最后一个参数 accessOrder 就表示是否按照访问顺序排列。当 accessOrder 为 true,最后执行 get 或者 put 的元素会在 LinkedHashMap 的尾部。

这样 pruneIfNeeded 方法就很容易找到较早的缓存并将其删除。

服务端缓存

当客户端发起一个 Http 请求,如果服务端返回 304,表示请求的资源缓存仍能有效。这样就能减少数据传输。当然,这种方式需要客户端携带额外的头信息,Volley 已经帮我们做了这部分。直接看相应的源码:

public class BasicNetwork implements Network {
    @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        // 省略...
        Map<String, String> headers = new HashMap<String, String>();
        addCacheHeaders(headers, request.getCacheEntry());
        // before request
        httpResponse = mHttpStack.performRequest(request, headers);
        StatusLine statusLine = httpResponse.getStatusLine();
        int statusCode = statusLine.getStatusCode();
        responseHeaders = convertHeaders(httpResponse.getAllHeaders());
        // Http 304
        if (statusCode == HttpStatus.SC_NOT_MODIFIED) {

        }
    }
    private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
        // If there's no cache entry, we're done.
        if (entry == null) {
            return;
        }

        if (entry.etag != null) {
            headers.put("If-None-Match", entry.etag);
        }

        if (entry.lastModified > 0) {
            Date refTime = new Date(entry.lastModified);
            headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
        }
    }
}

从上可以看到 Volley 在请求头添加了一个 etag 和 lastModified,这些数据来自上次请求的响应头中,Volley对其做了缓存,并且在下一次请求时添加到请求头中。这样服务端就能比较客户端发送的 etag 和自己的 etag,如果相等,说明请求的资源未发生变化,服务端返回304。客户端则对响应码做判断,如果为 304,说明本地缓存有效。

主线程回调

CacheDispatcher 和 NetworkDispatcher 都是运行在非主线程当中的,而我们的 UI 必须在主线程中更新。Volley 采用的是 Handler 来通知主线程更新 UI。

Request 中有一个 deliverResponse 和 deliverError,一个是成功回调,另一个是失败回调。那么这两个方法是什么时候被执行的呢?

看下 NetworkDispatcher 部分源码(省略部分代码)

public class NetworkDispatcher extends Thread {
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        while (true) {
            try {
                request = mQueue.take();
            } catch (InterruptedException e) {
            }

            try{
                // 发起网络请求
                NetworkResponse networkResponse = mNetwork.performRequest(request);
                // 解析
                Response<?> response = request.parseNetworkResponse(networkResponse);
                // 写缓存
                if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                }
                // 分发结果,通知主线程
                mDelivery.postResponse(request, response);

            } catch (VolleyError volleyError) {
                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                // 分发失败
                parseAndDeliverNetworkError(request, volleyError);
            } catch (Exception e) {
                VolleyError volleyError = new VolleyError(e);
                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                // 分发失败
                mDelivery.postError(request, volleyError);
            }
        }
    }
}

和主线程相关的就是 mDelivery.postResponse(request, response); 和 mDelivery.postError(request, volleyError)

我们看下 NetworkDispatcher 中 mDelivery 是如何创建的。看下 NetworkDispatcher 源码发现是在构造函数,于是去 RequestQueue 找 NetworkDispatcher 对象的创建过程。在 start 方法中会创建 NetworkDispatcher ,并传入一个 mDelivery 对象,而 mDelivery 在 RequestQueue 的构造函数中已经完成了初始化,看下相关源码:

    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

    public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }

由此可见在 NetworkDispatcher 中 mDelivery 的实际类型是 ExecutorDelivery。ExecutorDelivery 的构造函数接收一个 Handler 用来往主线程发消息。

看下 ExecutorDelivery 部分源码:

public class ExecutorDelivery implements ResponseDelivery {
    /** Used for posting responses, typically to the main thread. */
    private final Executor mResponsePoster;

    public ExecutorDelivery(final Handler handler) {
        // Make an Executor that just wraps the handler.
        mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };
    }

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

postResponse 和 postError 最终都会提交 一个 Runnable 给 mResponsePoster 执行,而 mResponsePoster 则将这个 Runnable 提交给 Handler 去执行。Handler 接收到 Runnable 之后最终会执行 mRequest.deliverResponse(mResponse.result) 或者 mRequest.deliverError(mResponse.error)完成主线程的回调。

小结

关于 Volley 的源码大致先解读这些,重点在于理顺整个逻辑,其他的像 HttpClient,HttpUrlConnection 的网络操作建议直接阅读源码。

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

推荐阅读更多精彩内容