1 前言

Volley是Goole在2013年Google I/O大会上推出的开源网络通信框架。Volley 的特点是使用简单而且适合数据量小,通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,它的表现就会非常糟糕,因为Volley在解析期间将所有响应保存在内存中。Volley带给我们开发者的是便利,它把Http所有通信细节、线程处理全部封装在内部,我们只需要简单调用就可以完成通信操作。关于如何使用Volley我们在前面文章《Android网络编程(五) 之 Volley框架的使用》已经有所介绍,今天我们主要是为了搞清楚Volley内部是实现原理,揭开它为什么只适用于数据量不大且通信频繁的网络操作以及它内部线程是怎样一个工作原理。

2 使用回顾

Volley的使用是非常的简单,实际上就是5个步聚:

1. 在Gradle中进行com.android.volley:volley:1.1.1 的依赖

2. 在AndroidManifest.xml中添加访问网络权限<uses-permissionandroid:name="android.permission.INTERNET" />

3. 创建请求队列RequestQueue对象

4. 创建请求Request对象

5. 将Request对象add到RequestQueue对象中

示例:

RequestQueue requestQueue = Volley.newRequestQueue(getApplicationContext());
Request stringRequest = new StringRequest(Request.Method.GET, "http://www.xxx.com",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String s) {
                // 请求成功
            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                // 请求失败
            }
        });
requestQueue.add(stringRequest);

3 原理分析

3.1 工作流程

开始前,我们先来看看官网上关于Volley的工作流程图:

 

1. 图中可见三种颜色分别代表三种线程:蓝色是主线程,绿色是缓存线程,而橙色是网络线程。

2. 如上面回顾中代码,我们在主线程中将请求的Request对象加入到请求队列RequestQueue中,Volley就正式开始工作。其实请求队列RequestQueue内部在创建后主要是做了两件事情,创建并启动一条缓存线程 CacheDispatcher和创建并启动N条网络请求线程NetWorkDIspatcher。

3. 结合上图,主线程请求后首先会经过绿色部分,也就是缓存线程CacheDispatcher,该线程中会去做一些判断来确定当前请求的数据是否在缓存中。如果存在,则将数据取出,然后根据传入的Request对象类型进行加工,然后将其返回给主线程;如果不存在,则会将请求交由网络请求线程NetWorkDIspatcher来处理。

4. 网络请求线程NetWorkDIspatcher会有N条,默认是4条,主要是负责进行网络的请求,同时会判断下载到的数据能否进行缓存,当请求成功后,便如缓存线程般,根据传入的Request对象类形进行加工,然后将其返回给主线程。

3.2 RequestQueue的创建和工作原理

我们在上面使用代码可见,一切从Volley.newRequestQueue()创建RequestQueue对象开始,我们来看看该方法源代码:

Volley.java

public static RequestQueue newRequestQueue(Context context) {
    return newRequestQueue(context, (BaseHttpStack) null);
}
public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
    BasicNetwork network;
    if (stack == null) {
        if (Build.VERSION.SDK_INT >= 9) {
            network = new BasicNetwork(new HurlStack());
        } else {
            String userAgent = "volley/0";
            try {
                String packageName = context.getPackageName();
                PackageInfo info =
                        context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
                userAgent = packageName + "/" + info.versionCode;
            } catch (NameNotFoundException e) {
            }
            network = new BasicNetwork(new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
        }
    } else {
        network = new BasicNetwork(stack);
    }

    return newRequestQueue(context, network);
}

newRequestQueue方法是两个静态公开方法,一个参数的方法最后也会调用到两个参数的方法,方法内主要是为了创建一个BasicNetwork对象,并将其传递到newRequestQueue方法中。BasicNetwork的创建分三种情况,首先会对方法第二个参数BaseHttpStack进行判空,先看如果不为空,则直接将其传给BasicNetwork的构造函数进行实例化BasicNetwork对象;如果不为空,即再进一步判断当前Android SDK是否>=9,即是否>=Android 2.3。如果是则创建一个HurlStack对象然后实例化BasicNetwork对象,否则创建一个HttpClientStack对象然后再实例化BasicNetwork对象。

如果你进一步看HurlStack和HttpClientStack的源码,会发现HurlStack是基于HttpURLConnection来实现的,而HttpClientStack是基于HttpClient来关现的。其中原因就是HttpURLConnection在Android2.3之前会非常不靠谱,存在一些Bug,比如在在读取 InputStream时调用 close()方法,就有可能会导致连接池失效了,所以在以前如果在Android2.3之前使用的话,通常的解决办法就是禁用掉连接池的功能,而在Android2.3起,由于已经不存在刚才说的Bug,而且因为HttpURLConnection其API简单、压缩和缓存机制可以有效地减少网络访问的流量,改进了网络速度等优化,故一般情况下都是使用HttpURLConnection代替HttpClient,并且在Android6.0之后已经默认将HttpClient移除SDK中。

回到正题,所以BasicNetwork对象便就是用于网络请求的,在创建出BasicNetwork对象后,下一步就是将其传递给同名的newRequestQueue静态私有方法:

Volley.java

private static RequestQueue newRequestQueue(Context context, Network network) {
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();
    return queue;
}

该方法中首先通过创建的DiskBasedCache对象和传入的Network对象来实例化RequestQueue,并且调用了RequestQueue对象的start方法。先来看看RequestQueue类的构造方法:

RequestQueue.java

private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
public RequestQueue(Cache cache, Network network) {
    this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
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;
}

常规调用的RequestQueue类的构造方法最终会调到接收4个参数的重载方法中,方法中只做了全局变量的赋值,并没有太多逻辑,那么来看看这几个参数的意思。

DiskBasedCache      用于保持对磁盘的响应的缓存

Network                     用于执行HTTP请求的网络接口

threadPoolSize         要创建的网络调度程序线程数,默认是4,所以说Volley是适合数据量小,通信频繁的网络操作

ResponseDelivery    绑定了UI线程Looper的结果分发器

因为RequestQueue对象创建后会首先调用start方法,所以接下来继续看看它的start方法源码:

RequestQueue.java

public void start() {

    // Make sure any currently running dispatchers are stopped.
    stop();
    // 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();
    }
}

public void stop() {
    if (mCacheDispatcher != null) {
        mCacheDispatcher.quit();
    }
    for (final NetworkDispatcher mDispatcher : mDispatchers) {
        if (mDispatcher != null) {
            mDispatcher.quit();
        }
    }
}

start方法做了三件事情,首先调用了stop方法进行mCacheDispatcher和mDispatchers的重置,其次是创建CacheDispatcher对象并启动,最后是for循环创建出N个NetworkDispatcher对象并启动。CacheDispatcher和NetworkDispatcher都是继承Thread线程,从注释中可知,CacheDispatcher是用于缓存调度线程,而NetworkDispatcher是用于网络请求的线程,我们继续来看看它们分别具体是干什么来着。

3.2.1 CacheDispatcher线程

CacheDispatcher构造方法接收mCacheQueue代表缓存队列、mNetworkQueue代表网络请求队列、mCache和mDelivery是RequestQueue类构造方法传入的用于保持对磁盘的响应的缓存对象和结果分发器对象。继续看看该线程内部做了啥事情:

CacheDispatcher.java

@Override
public void run() {
    ……
    while (true) {
        try {
            processRequest();
        } catch (InterruptedException e) {
            ……
        }
    }
}
private void processRequest() throws InterruptedException {
    final Request<?> request = mCacheQueue.take();
    processRequest(request);
}
@VisibleForTesting
void processRequest(final Request<?> request) throws InterruptedException {
    request.addMarker("cache-queue-take");

    // 如果请求取消,则结束流程
    if (request.isCanceled()) {
        request.finish("cache-discard-canceled");
        return;
    }

    // 尝试从缓存中检索数据
    Cache.Entry entry = mCache.get(request.getCacheKey());
    if (entry == null) {
        request.addMarker("cache-miss");
        // 不存在缓存,则将请求添加到网络请求队列去
        if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
            mNetworkQueue.put(request);
        }
        return;
    }

    // 如果缓存过期,则将请求添加到网络请求队列去
    if (entry.isExpired()) {
        request.addMarker("cache-hit-expired");
        request.setCacheEntry(entry);
        if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
            mNetworkQueue.put(request);
        }
        return;
    }

    // 命中缓存,解析缓存数据
    request.addMarker("cache-hit");
    Response<?> response =
            request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
    request.addMarker("cache-hit-parsed");
       // 缓存新鲜度判断
    if (!entry.refreshNeeded()) {
        // 缓存有效不需要刷新,直接进行结果分发
        mDelivery.postResponse(request, response);
    } else {
        // 缓存需要刷新,并要进行网络请求
        request.addMarker("cache-hit-refresh-needed");
        request.setCacheEntry(entry);
        // Mark the response as intermediate.
        response.intermediate = true;

        if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
            // 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) {
                                // Restore the interrupted status
                                Thread.currentThread().interrupt();
                            }
                        }
                    });
        } else {
            // request has been added to list of waiting requests
            // to receive the network response from the first request once it returns.
            mDelivery.postResponse(request, response);
        }
    }
}

CacheDispatcher线程内部是一个死循环,当从缓存队列mCacheQueue中读取到Request后,便开始对Request进行处理。从上面代码注释中可见,缓存经过了:是否存在、是否过期、新鲜度的判断,如果缓存获取失败则会加入mNetworkQueue网络请求队列去,否则就调用ResponseDelivery结果分发器的postResponse分发结果。

3.2.2 NetworkDispatcher线程

NetworkDispatcher构造方法接收mNetworkQueue代表网络请求队列、mNetwork、mCache和mDelivery是RequestQueue类构造方法传入的用于执行HTTP请求的网络接口、用于保持对磁盘的响应的缓存对象和结果分发器对象。继续看看该线程内部做了啥事情:

NetworkDispatcher.java

@Override
public void run() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    while (true) {
        try {
            processRequest();
        } catch (InterruptedException e) {
            ……

        }
    }
}

private void processRequest() throws InterruptedException {
    Request<?> request = mQueue.take();
    processRequest(request);
}

@VisibleForTesting
void processRequest(Request<?> request) {
    long startTimeMs = SystemClock.elapsedRealtime();
    try {
        request.addMarker("network-queue-take");

        // 如果请求取消,则直接返回结果,结束流程
        if (request.isCanceled()) {
            request.finish("network-discard-cancelled");
            request.notifyListenerResponseNotUsable();
            return;
        }

        addTrafficStatsTag(request);

        // 使用mNetwork执行网络请求.
        NetworkResponse networkResponse = mNetwork.performRequest(request);
        request.addMarker("network-http-complete");

        // 如果服务器返回304,表示没有改动过,则直接返回结果,结束流程

        if (networkResponse.notModified && request.hasHadResponseDelivered()) {
            request.finish("not-modified");
            request.notifyListenerResponseNotUsable();
            return;
        }

        // 根据传入的Request子类(StringRequest/JsonRequest…)进行解析出相应的Response对象
        Response<?> response = request.parseNetworkResponse(networkResponse);
        request.addMarker("network-parse-complete");

        // 写入缓存
        if (request.shouldCache() && response.cacheEntry != null) {
            mCache.put(request.getCacheKey(), response.cacheEntry);
            request.addMarker("network-cache-written");
        }

        // 将结果分发
        request.markDelivered();
        mDelivery.postResponse(request, response);
        request.notifyListenerResponseReceived(response);
    } catch (VolleyError volleyError) {
        volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
        parseAndDeliverNetworkError(request, volleyError);
        request.notifyListenerResponseNotUsable();
    } 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);
        request.notifyListenerResponseNotUsable();
    }
}

同样NetworkDispatcher线程内部也是一个死循环,当从网络请求队列mNetworkQueue中读取到Request后,便 开始对Request进行处理。从上面代码注释中可见,使用了mNetwork进行了网络请求、通过parseNetworkResponse方法返回了我们传入的Request对应类型的结果,并进行缓存写入,最后调用结果分发器的postResponse分发结果。

3.3 Request的创建

Request是一个抽象类,它的子类有:StringRequest、JsonRequest、ImageRequest等,我们也可以根据自己实际请求后返回的类型进行自定义Request对象。当我们进行自定义Request继承时,不同的Request最大的差别在于重写parseNetworkResponse方法,也就是上面NetworkDispatcher线程内部做完网格请求后调用的parseNetworkResponse方法返回对应类型结果。如StringRequest类中源码:

StringRequest.java

@Override
@SuppressWarnings("DefaultCharset")
protected Response<String> parseNetworkResponse(NetworkResponse response) {
    String parsed;
    try {
        parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
    } catch (UnsupportedEncodingException e) {
        // Since minSdkVersion = 8, we can't call
        // new String(response.data, Charset.defaultCharset())
        // So suppress the warning instead.
        parsed = new String(response.data);
    }
    return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}

3.4 将Request添加以RquestQueue

当RquestQueue和Request对象都创建好后,最后一步就是将Request对象通过RquestQueue的add方法添加到队列中去,可见源码:

RquestQueue.java

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;
    }
    mCacheQueue.add(request);
    return request;
}

当request被添加到mNetworkQueue和mCacheQueue两个队列后,便有了3.2.1和3.2.2中两种线程在死循环读取自身相应的队列,然后进行相应的逻辑处理。

4 总结

关于Volley的使用和原理就写到这里,Volley的总体设计思路很简单。虽然它使用场景有够为明显的限制,而且目前比较流行的网络请求框架还有Okhttp3,但是毕竟学习优秀的框架设计思想还是非常有必要的。更多关于Volley的介绍,请可以前往其官网

 

 

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐