Android Google的网络请求库Volley的基本使用及原理浅析

版权声明:欢迎来到我的个人博客:http://blog.N0tExpectErr0r.cn https://blog.csdn.net/qq_21556263/article/details/82768567

Google的网络请求库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请求,解析响应结果,写入缓存,并回调主线程。


广告时间
我是N0tExpectErr0r,一名广东工业大学的大二学生
欢迎来到我的个人博客,所有文章均在个人博客中同步更新哦
http://blog.N0tExpectErr0r.cn

猜你喜欢

转载自blog.csdn.net/qq_21556263/article/details/82768567