【Android】Volley网络框架的学习及源码分析

【Android】Volley网络框架的学习及源码分析

介绍

Volley是Google在2013年发布的一款Android平台上的网络请求库。

它有如下特点:

  • 使得网络通信更快,更简单
  • GET、POST网络请求及网络图像的高效异步处理请求
  • 可以对网络请求进行排序优先级处理
  • 网络请求的缓存
  • 多级别取消请求
  • 与Activity生命周期的联动

但它也有一定缺点:

  • 不适合进行数据的上传与下载

Volley的使用

建立全局请求队列

首先我们需要一个全局的Application来建立一个全局的请求队列。在这里我们建立一个ContextApplication并继承自Application。并在manifest中指定application的name属性为ContextApplication。

然后我们在ContextApplication中写用于建立请求队列的代码。

public class ContextApplication extends Application {

    public static RequestQueue sQueue;

    @Override
    public void onCreate() {
        super.onCreate();
        sQueue = Volley.newRequestQueue(getApplicationContext());
    }

    public static RequestQueue getHttpQueues(){
        return sQueue;
    }
}

这样就完成了全局请求队列的建立。

GET请求

首先我们学习用StringRequest来请求数据。

此处我们将请求方法设置为了Method.GET,然后填入请求链接,创建请求成功及失败的回调。

之后我们为request设置了一个tag,这样在加入全局队列后可以通过标签找到这个request。

之后将该request放入全局队列。这样一个使用StringRequest的GET请求就成功完成了

String url = "https://www.baidu.com";
StringRequest request = new StringRequest(Method.GET, url, new Listener<String>() {
    @Override
    public void onResponse(String s) {
        //请求成功
    }
}, new ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError volleyError) {
        //请求失败
    }
});
request.setTag("httpGET");
ContextApplication.getHttpQueues().add(request);

如果换做是JsonObjectRequest,基本相同,仅仅是最后的结果返回的是JsonObject而已。传入需要多传入一个JsonObject参数。这个参数是用于POST请求,GET请求可以设置为null

可以看出,相比HttpUrlConnection,Volley实现请求确实简单了不少。

POST请求

用StringRequest进行POST请求,参数与之前大致相同,将方法改为POST。然后传递参数通过实现它的传递参数方法,也就是getParams方法。

可以看到,getParams方法返回的是一个Map,因此我们需要建立一个Map,然后将参数添加到Map中,最后返回该map。

String url = "http://xxxxxx.xx";
StringRequest request = new StringRequest(Method.POST, url, new Listener<String>() {
    @Override
    public void onResponse(String s) {
    }
}, new ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError volleyError) {
    }
}){
    @Override
    protected Map<String, String> getParams() throws AuthFailureError {
        Map<String,String> params = new HashMap<>();
        params.put("username","xxxx");
        params.put("password","xxxxxxxx");
        return params;
    }
};
request.setTag("testPOST");
ContextApplication.getHttpQueues().add(request);

这样就完成了用StringRequest进行POST请求。

如果我们换为使用JsonObjectRequest进行请求,那么我们需要在外部建立一个Map,将参数像刚刚那样传递,然后建立一个JsonObject,将Map放入。最后将jsonObject作为参数传入JsonObjectRequest的构造函数即可。

将Volley生命周期与Activity进行关联。

我们只需在Activity的onStop方法,通过cancelAll方法传入tag,将请求关闭即可。

@Override
protected void onStop() {
    super.onStop();
    ContextApplication.getHttpQueues().cancelAll("testGET");
}

网络图片的加载

网络图片加载需要用到ImageRequest。它需要传入的第一个参数是图片的url,然后是请求成功的回调,之后是图片的最大宽度以及最大高度(设置为0会以原图方式加载)。然后是加载图片的格式,这里选择Config.RGB_565。之后是加载失败的回调。

最后将请求放入队列即可完成网络图片的加载。

private void imageReuqest() {
    String url = "https://www.baidu.com/img/baidu_jgylogo3.gif";
    ImageRequest request = new ImageRequest(url, new Listener<Bitmap>() {
        @Override
        public void onResponse(Bitmap bitmap) {
            mImageView.setImageBitmap(bitmap);
        }
    }, 0, 0, Config.ARGB_8888, new ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError volleyError) {
        }
    });
    ContextApplication.getHttpQueues().add(request);
}

图片的缓存加载

这里我们使用ImageLoader来进行加载图片。这里需要填入的参数是请求队列以及缓存。

请求队列我们填入全局的队列。而ImageCache本身是起不到作用的,我们需要结合LruCache来使用。

我们自定义一个BitmapCache类,实现ImageCache接口。可以看到,它需要实现getBitmap以及putBitmap方法,通过这两个方法,我们就可以实现图片的缓存。加载图片时,会先从缓存中获取图片,获取不到再从网络获取图片。

public class BitmapCache implements ImageCache {
    private static final String CACHE_PATH =
            Environment.getExternalStorageDirectory().getAbsolutePath() + "/ONE";   //本地缓存路径
    private static LruCache<String, Bitmap> sMemoryCache;

    public BitmapCache(){
        //初始化LruCache
        if (sMemoryCache == null) {
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  //将最大内存换算为kb(原来以B为单位)
            final int cacheSize = maxMemory / 8;    //取1/8的内存为LruCache的大小
            //计算LruCache空间大小
            sMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getByteCount() / 1024;
                }
            };
        }
    }


    @Override
    public Bitmap getBitmap(String url) {
        boolean isLoaded = false;
        if (!isLoaded) {
            //先从内存拿
            Bitmap bitmap = getBitmapFromMemoryCache(url);
            if (bitmap != null) {
                isLoaded = true;
                return bitmap;
            }
        }
        if(!isLoaded) {
            //再从本地拿
            Bitmap bitmap = getBitmapFromLocal(url);
            if (bitmap != null) {
                isLoaded = true;
                return bitmap;
            }
        }
        return null;
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        addBitmapToMemoryCache(url,bitmap);
        addBitmapToLocal(url,bitmap);
    }

    /**
     * 内存缓存
     */
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            sMemoryCache.put(key, bitmap);  //图片没有放入时将图片放入内存
        }
    }

    public Bitmap getBitmapFromMemoryCache(String key) {
        return sMemoryCache.get(key);   //从内存取出对应图片
    }

    /**
     * 本地缓存
     */
    public Bitmap getBitmapFromLocal(String url) {
        String fileName = null;
        try {
            //进行MD5加密的原因:不让一些特殊的url影响文件的存储
            //同时让接口不被用户看到
            //把图片的url当做文件名,并进行MD5加密
            fileName = MD5Encoder.encode(url);
            File file = new File(CACHE_PATH, fileName);
            Bitmap bitmap = null;
            if (file.exists()) {
                bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
                addBitmapToMemoryCache(url, bitmap); //添加到内存缓存
            }
            return bitmap;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    public void addBitmapToLocal(String url, Bitmap bitmap) {
        try {
            //进行MD5加密的原因:不让一些特殊的url影响文件的存储
            //同时让接口不被用户看到
            //把图片的url当做文件名,并进行MD5加密
            String fileName = MD5Encoder.encode(url);
            File file = new File(CACHE_PATH, fileName);

            //通过得到文件的父文件,判断父文件是否存在
            File parentFile = file.getParentFile();
            if (!parentFile.exists()) {
                parentFile.mkdirs();
            }

            //把图片保存至本地
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(file));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

之后我们通过构造方法创建ImageLoader,创建好ImageLoader之后,通过ImageLoader的getImageListener方法获取到ImageListener。之后通过imageLoader的get方法进行加载即可。

Volley的源码分析

参考自郭霖大神的博客:http://blog.csdn.net/guolin_blog/article/details/17656437

要解析Volley的源码,我们从第一个用到的方法——newRequestQueue方法来看起,查看newRequestQueue的代码

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    File cacheDir = new File(context.getCacheDir(), "volley");
    String userAgent = "volley/0";
    try {
        String packageName = context.getPackageName();
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
        userAgent = packageName + "/" + info.versionCode;
    } catch (NameNotFoundException var6) {
        ;
    }
    if (stack == null) {
        if (VERSION.SDK_INT >= 9) {
            stack = new HurlStack();
        } else {
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
        }
    }
    Network network = new BasicNetwork((HttpStack)stack);
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();
    return queue;
}

可以看到,这里在第10行判断如果stack是等于null的,则去创建一个HttpStack对象,这里会判断如果手机系统版本号是大于9的,则创建一个HurlStack的实例,否则就创建一个HttpClientStack的实例。

实际上HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的 。

创建好了HttpStack之后,接下来又创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的,紧接着new出一个RequestQueue对象,并调用它的start()方法进行启动,然后将RequestQueue返回,这样newRequestQueue()的方法就执行结束了。

那么RequestQueue的start()方法内部到底执行了什么东西呢?我们跟进去:

public void start() {
    this.stop();
    this.mCacheDispatcher = new CacheDispatcher(this.mCacheQueue, this.mNetworkQueue, this.mCache, this.mDelivery);
    this.mCacheDispatcher.start();
    for(int i = 0; i < this.mDispatchers.length; ++i) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(this.mNetworkQueue, this.mNetwork, this.mCache, this.mDelivery);
        this.mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

这里先创建了一个CacheDispatcher的实例,然后调用了它的start()方法,接着在一个for循环里去创建NetworkDispatcher的实例,并分别调用它们的start()方法。

这里的CacheDispatcher和NetworkDispatcher都是继承自Thread的,而默认情况下for循环会执行四次,也就是说当调用了Volley.newRequestQueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来,其中CacheDispatcher是缓存线程,NetworkDispatcher是网络请求线程。

我们得到RequestQueue后,构建出相应的Request,然后调用RequestQueue的add方法即可完成网络请求,我们可以进入add方法看看。

public Request add(Request request) {
    request.setRequestQueue(this);
    Set var2 = this.mCurrentRequests;
    synchronized(this.mCurrentRequests) {
        this.mCurrentRequests.add(request);
    }
    request.setSequence(this.getSequenceNumber());
    request.addMarker("add-to-queue");
    if (!request.shouldCache()) {
        this.mNetworkQueue.add(request);
        return request;
    } else {
        Map var7 = this.mWaitingRequests;
        synchronized(this.mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (this.mWaitingRequests.containsKey(cacheKey)) {
                Queue<Request> stagedRequests = (Queue)this.mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new LinkedList();
                }
                ((Queue)stagedRequests).add(request);
                this.mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", new Object[]{cacheKey});
                }
            } else {
                this.mWaitingRequests.put(cacheKey, (Object)null);
                this.mCacheQueue.add(request);
            }
            return request;
        }
    }
}

可以看到,在第11行的时候会判断当前的请求是否可以缓存,如果不能缓存则在第12行直接将这条请求加入网络请求队列,可以缓存的话则在第33行将这条请求加入缓存队列。

由此可见,在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一行为。

既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们可以查看CacheDispatcher中的run()方法:


public class CacheDispatcher extends Thread { …… @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; } } } }

run方法中代码比较长,我们可以在11行看到一个while循环,说明这个缓存线程是始终运行的。在32行可以看到在从缓存中取出响应的结果。如果缓存结果为空,就把这条请求加入网络请求队列。不为空的情况下则判断缓存是否过期,过期的话仍然将这条请求加入网络请求队列。否则不需要网络请求,直接使用缓存中的数据。

第39行在调用Request的parseNetworkResponse()方法来对数据进行解析 。之后则是将解析后的数据进行回调。回调的部分我们在这里暂时不介绍,因为它和NetworkDispatcher的逻辑基本相同,等到后面一起分析。

我们来到NetWorkDIspatcher来查看它如何处理网络请求队列。

public class NetworkDispatcher extends Thread {
    ……
    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Request<?> request;
        while (true) {
            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) {
                parseAndDeliverNetworkError(request, volleyError);
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
                mDelivery.postError(request, new VolleyError(e));
            }
        }
    }
}

可以看到,NetworkDispatcher中也是while(true)循环,说明网络请求的线程也是不断运行。看到28行会调用Network的performRequest()方法来发送网络请求。而Network是一个接口,这里具体的实现是BasicNetwork,我们来看下它的performRequest()方法:


public class BasicNetwork implements Network { …… @Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { HttpResponse httpResponse = null; byte[] responseContents = null; Map<String, String> responseHeaders = new HashMap<String, String>(); try { // Gather headers. Map<String, String> headers = new HashMap<String, String>(); addCacheHeaders(headers, request.getCacheEntry()); httpResponse = mHttpStack.performRequest(request, headers); StatusLine statusLine = httpResponse.getStatusLine(); int statusCode = statusLine.getStatusCode(); responseHeaders = convertHeaders(httpResponse.getAllHeaders()); // Handle cache validation. if (statusCode == HttpStatus.SC_NOT_MODIFIED) { return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, request.getCacheEntry() == null ? null : request.getCacheEntry().data, responseHeaders, true); } // Some responses such as 204s do not have content. We must check. if (httpResponse.getEntity() != null) { responseContents = entityToBytes(httpResponse.getEntity()); } else { // Add 0 byte response as a way of honestly representing a // no-content request. responseContents = new byte[0]; } // if the request is slow, log it. long requestLifetime = SystemClock.elapsedRealtime() - requestStart; logSlowRequests(requestLifetime, request, responseContents, statusLine); if (statusCode < 200 || statusCode > 299) { throw new IOException(); } return new NetworkResponse(statusCode, responseContents, responseHeaders, false); } catch (Exception e) { …… } } } }

这段方法中大多都是一些网络请求细节方面的东西,我们并不需要太多关心。需要注意在14行调用了HttpStack的performRequest()方法。

这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象。前面提到,这两个对象的内部分别使用HttpURLConnection和HttpClient来发送网络请求,我们不再需要跟进查看。获取结果之后会将服务器返回的数据组装成一个NetworkResponse对象进行返回。

在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,以及将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。

在解析完了NetworkResponse中的数据之后,又会调用ExecutorDelivery的postResponse()方法来回调解析出的数据:

public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
    request.markDelivered();
    request.addMarker("post-response");
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}

其中,在mResponsePoster的execute()方法中传入了一个ResponseDeliveryRunnable对象,就可以保证该对象中的run()方法就是在主线程当中运行的了,我们看下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;
    }

    @SuppressWarnings("unchecked")
    @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();
        }
   }

在第22行调用了Request的deliverResponse()方法。每一条网络请求的响应都是回调到这个方法中,最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。

以下是Volley库中自带的一张Volley原理图:

Volley

其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: