Volley基本使用及源码解析
来源:互联网 发布:jquery ajax json 编辑:程序博客网 时间:2024/06/05 11:44
相信大部分开发者在android开发过程中 都使用过volley这个jar包,下面我们就通过这个jar包的源码,对这个jar包进行深度解析,以便我们在使用这个jar包的情况下能够很好的了解这个jar包的利弊。
volley 基本用法
对于volley的基本用法相信很多开发人员都知道,下面我们以String的请求来讲解volley的基本用法
StringRequest的基本用法
1:创建requestQueue对象
2:创建StringRequest
3:将request添加到requestQueue中
GET 用法
下面是具体实例代码:
//1:创建requestQueue对象RequestQueue requestQueue = Volley.newRequestQueue(this); //2:创建StringRequest StringRequest stringRequest = new StringRequest(GETURL, new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d("TAG","requestSuccess:"+response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.d("TAG","requestError:"+error.getMessage()); } }); //将request添加到requestQueue中 requestQueue.add(stringRequest);
程序运行之后我们可以看到其返回的内容:
通过日志的打印我们可以看到整个请求是成功的。
POST
上述请求是GET请求下面我们来看一下post请求是如何完成的
其实Post方法与get方法的基本步骤是一致的,只不过在构造 StringRequest方法中多穿入了一个参数,该参数指明该次的请求模式是get还是post。
//1:创建requestQueue对象 RequestQueue requestQueue = Volley.newRequestQueue(this); //2:创建StringRequest StringRequest stringRequest = new StringRequest(Request.Method.POST,GETURL, new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d("TAG","requestSuccess:"+response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.d("TAG","requestError:"+error.getMessage()); } } ) { @Override protected Map<String, String> getParams() throws AuthFailureError { return super.getParams(); } }; //将request添加到requestQueue中 requestQueue.add(stringRequest);
细心的读者看到了在post请求中,我们同时重写了getParams(),实质上对于我们来说完全可以理解,因为post请求是不允许带有参数的,所以重写getParams()方法,在这里设置POST参数,这就是我们重写getParams() 方法的意义。
运行结果:
到这里我行各位读者对于volley 中使用StringRequest的请求,无论是get方式还是post方法都有初步的了解了。
JsonRequest基本用法
上述我们讲解了StringRequest的基本用法,接下来我们讲解Volley 中JsonRequest的基本用法。
JsonRequest的基本用法与StringRequest的用法基本一致,
1:创建requestQueue对象
2:创建JsonRequest
3:将request添加到requestQueue中
下面是具体实现代码:
//1:创建requestQueue对象 RequestQueue requestQueue = Volley.newRequestQueue(this); //2:创建JsonObjectRequest JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(JSONURL, null, new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Log.d("TAG","requestSuccess:"+response.toString()); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.d("TAG","requestError:"+error.getMessage()); } }){ @Override protected Map<String, String> getParams() throws AuthFailureError { // 创建json串 return super.getParams(); } }; //将request添加到requestQueue中 requestQueue.add(jsonObjectRequest);
注:JsonObjectRequest请求方式根据 参数2进行区别的 jsobObject 第二个参数需要传入的形式为Json形式,如果传入Json,此时JsonRequest请求方式为post请求,如果传入为null 则JsonRequest请求方式为get
上述这点我们可以通过源代码看到:
public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener) { this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest, listener, errorListener); }
程序运行我们可以看到:
至此相信读者对volley中JsonRequest的请求有一定的了解了。
Volley 加载 image
ImageRequest 的基本使用
在volley使用过程中,我们会经常用到网络图片的加载,针对图片加载Volley提供了 ImageRequest加载方式。
实质上ImageRequest方式加载图片与StringRequest以及JsonRequest方式是一样的;
1:创建requestQueue对象
2:创建ImageRequest
3:将request添加到requestQueue中
下面我们来介绍ImageRequest是如何加载图片的;
具体实现代码
RequestQueue requestQueue = Volley.newRequestQueue(this); //String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, Config decodeConfig, Response.ErrorListener errorListener /** *0, // 图片的宽度,如果是0,就不会进行压缩,否则会根据数值进行压缩 *0, // 图片的高度,如果是0,就不进行压缩,否则会压缩 *Config.ARGB_8888, // 图片的颜色属性 */ ImageRequest imageRequest = new ImageRequest(IMAGEUEL, new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { Log.d("TAG","requestSuccess:"+response.toString()); } }, 0, 0, Bitmap.Config.ARGB_8888, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.d("TAG","requestError:"+error.getMessage()); } }); requestQueue.add(imageRequest);
运行程序之后我们可以看到
在这里我们可以通过源码看到ImageRequest加载方式是get方式;
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) { super(Method.GET, url, errorListener); ... }
ImageLoader图片加载方式
如果你觉得ImageRequest已经非常好用了,那我只能说你太容易满足了 ^_^。实际上,Volley在请求网络图片方面可以做到的还远远不止这些,而ImageLoader就是一个很好的例子。ImageLoader也可以用于加载网络上的图片,并且它的内部也是使用ImageRequest来实现的,不过ImageLoader明显要比ImageRequest更加高效,因为它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求。
下面我们来看一下ImageLoader是如何实现图片加载的。
大致可以分为以下四步:
1. 创建一个RequestQueue对象。
2. 创建一个ImageLoader对象。
3. 获取一个ImageListener对象。
4. 调用ImageLoader的get()方法加载网络上的图片。
具体实现代码:
// 创建一个RequestQueue对象。 RequestQueue requestQueue = Volley.newRequestQueue(this); //创建一个ImageLoader对象 ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() { @Override public Bitmap getBitmap(String url) { return null; } @Override public void putBitmap(String url, Bitmap bitmap) { } }); //获取一个ImageListener对象。 /** * 1:图片显示view * 2:加载时默认图片 * 3:加载失败时默认图片 */ ImageLoader.ImageListener imageListener = ImageLoader.getImageListener(imageView, R.drawable.ic_launcher, R.drawable.ic_launcher); //调用ImageLoader的get()方法加载网络上的图片 /** * 200: 设置网络加载之后 图片允许的最大宽 * 200: 设置网络加载之后 图片允许的最大高 */ imageLoader.get(IMAGEUEL, imageListener,200,200);
程序运行结果我们可以看到:
到这里我们对Volley中的基本使用已经介绍完成,下面我们将对Volley源码进行详细解析。
Volley 源码解析
上述文章中已经介绍来Volley各种Request的使用,下面我们根据Volley源码来分析内部是如何具体实现的。
在 volley官网有一张图其实是很明确的表示了volley 的整个请求流程,该图如下图所示;
下面我们就根据上图流程来介绍volley的整个请求流程。
使用Volley的第一步,首先要调用Volley.newRequestQueue(context)方法来获取一个RequestQueue对象,那么我们自然要从这个方法开始看起了,代码如下所示
public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, null); }
可以看到内部实质上是直接调用了 newRequestQueue(…)方法
下面我们来看 newRequestQueue 内部主要做了什么;
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; }
首先我们在代码中看到 如果stack为null 则会根据sdk不同的版本去创建对应的stack对象,创建好stack对象之后,紧接着创建一个Network 它是用于根据传入的HttpStack对象来处理网络请求的,
之后创建一个RequestQueue 对象,创建好之后,调用start();方法,最后将 RequestQueue 对象返回出去,到这里我们 newRequestQueue 方法执行完成了,同时返回了一个RequestQueue对象。
那么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(); } }
在这段代码中我们看到 内部首选创建一个CacheDispatcher 对象,同时在for循环中创建多个 NetworkDispatcher 对象,然后这些对象,调用start方法。而 CacheDispatcher、NetworkDispatcher这两个对象通过源码我们可以看到实质上都是Thread对象。
public class CacheDispatcher extends Thread {}
public class NetworkDispatcher extends Thread{}
而在RequestQueue构造函数中我们可以看到
/** Number of network request dispatcher threads to start. */ private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; public RequestQueue(Cache cache, Network network) { this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); }
通过这个我们可以知道在RequestQueue对象创建时,同时会创建5个线程,分别为1个缓存线程,4个网络线程。
在得到RequestQueue对象,我们构建各种request对象,然后调用add方法将 request添加到 RequestQueue对象中。
下面我们只需要看一下add方法内是如何实现的;
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<Request<?>>(); } 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; } }
add 方法内部代码比较长,我们只需要关注内部比较重点的代码即可,
if (!request.shouldCache()) { mNetworkQueue.add(request); return request; }
在这段代码中我们可以看到首先判断 requset是否可以缓存,如果为false,则 将该request 直接添加网络队列中。
否则
mCacheQueue.add(request);
将request 添加到 缓存队列中。
在默认情况下是将request添加到缓存队列中;我们可以通过
public final Request<?> setShouldCache(boolean shouldCache) { mShouldCache = shouldCache; return this; }
方法来设置该请求是否可以添加到缓存队列中。
在add 方法之后,每条请求添加到缓存队列中,于是后台等待的无论是缓存线程还是网络线程开始运行起来,下面我们分别来看一下CacheDispatcher中的run()方法以及NetworkDispatcher 的run() 方法。
先看一下 CacheDispatcher中的run()方法;
@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; } } }
这个方法有点长,我们看到内部存在一个 while(true)循环,这说明这个缓存线程始终在运行,紧接这在下面这段代码中我们可以看到
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; }
首先从缓存中取出响应结果,如果为空的话则把这条请求加入到网络请求队列中,如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中,否则就认为不需要重发网络请求,直接使用缓存中的数据。
之后调用
Response<?> response = request.parseNetworkResponse( new NetworkResponse(entry.data, entry.responseHeaders));
parseNetworkResponse 对数据进行解析,解析完成之后,通过ResponseDelivery 类 将数据回调出去。
下面我们 来看一下 NetworkDispatcher 的run() 方法是如何实现的;
@Override 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); } } }
这段代码中我们可以看到内部同样有一个 while(true)方法,同样说明这个网络线程始终在运行,然后我们可以看到 内部调用了
Network的performRequest()方法来去发送网络请求,而Network是一个接口,这里具体的实现是BasicNetwork,我们只需要看BasicNetwork 中performRequest 方法的具体实现。
@Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { HttpResponse httpResponse = null; byte[] responseContents = null; Map<String, String> responseHeaders = Collections.emptyMap(); 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) { Entry entry = request.getCacheEntry(); if (entry == null) { return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } // A HTTP 304 response does not have all header fields. We // have to use the header fields from the cache entry plus // the new ones from the response. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 entry.responseHeaders.putAll(responseHeaders); return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, entry.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } // 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, SystemClock.elapsedRealtime() - requestStart); } catch (SocketTimeoutException e) { attemptRetryOnException("socket", request, new TimeoutError()); } catch (ConnectTimeoutException e) { attemptRetryOnException("connection", request, new TimeoutError()); } catch (MalformedURLException e) { throw new RuntimeException("Bad URL " + request.getUrl(), e); } catch (IOException e) { int statusCode; if (httpResponse != null) { statusCode = httpResponse.getStatusLine().getStatusCode(); } else { throw new NoConnectionError(e); } VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); NetworkResponse networkResponse; if (responseContents != null) { networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); } else if (statusCode >= 400 && statusCode <= 499) { // Don't retry other client errors. throw new ClientError(networkResponse); } else if (statusCode >= 500 && statusCode <= 599) { if (request.shouldRetryServerErrors()) { attemptRetryOnException("server", request, new ServerError(networkResponse)); } else { throw new ServerError(networkResponse); } } else { // 3xx? No reason to retry. throw new ServerError(networkResponse); } } else { attemptRetryOnException("network", request, new NetworkError()); } } } }
这段方法中大多都是一些网络请求细节方面的东西,我们并不需要太多关心,需要注意的是内部调用了
httpResponse = mHttpStack.performRequest(request, headers);
这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象。这两个对象的内部实际就是分别使用HttpURLConnection和HttpClient来发送网络请求的,我们就不再跟进去阅读了,之后会将服务器返回的数据组装成一个NetworkResponse对象进行返回。
在NetworkResponse 返回之后,调用了
Response<?> response = request.parseNetworkResponse(networkResponse);
对返回对response数据进行解析,解析完成之后调用
mDelivery.postResponse(request, response);
方法来回调解析出的数据。
下面我们来看数据在获取成功并解析之后是如何通过mDelivery 接口将数据回调出去的。
mDelivery 是 一个ResponseDelivery接口,其实现类是
private final Executor mResponsePoster;public class ExecutorDelivery implements ResponseDelivery { ... @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)); } 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(); } } }
可以看到内部直接调用了postResponse ,而postResponse内部调用了mResponsePoster.execute 方法,在调用execute方法时,执行了run 方法,而在run方法内部我们可以看到
mRequest.deliverResponse(mResponse.result)
将数据回调出去。
至此这里我们就把Volley的完整执行流程全部梳理了一遍,你是不是已经感觉已经很清晰了呢,下面在看一下该图是不是更加清晰了呢?
- Volley基本使用及源码解析
- Volley源码解析(一),基本概述
- 【Volley】Volley源码解析
- Android Volley 基本用法及解析
- Volley源码解析及相关拓展
- android volley封装及源码解析
- Volley源码解析(一)Volley中乱码问题及解决方案
- ARouter解析一:基本使用及页面注册源码解析
- Android Volley框架的基本使用解析
- volley源码解析(一)--volley的使用和架构
- volley源码解析(一)--volley的使用和架构
- Volley源码解析,框架综述,使用简述
- Volley的使用以及源码解析
- Volley完全解析——使用、源码
- volley源码解析
- Volley 源码解析
- Volley 源码解析
- Volley 源码解析
- 笔记
- bash shell:获取本脚本存储位置的绝对路径
- 第11周项目1-验证算法(3)中序线索化二叉树的算法验证
- 打造你的私人聊天机器人
- Spring Bean之JavaConfig自动化装配bean
- Volley基本使用及源码解析
- poj3276(开关问题)
- 《科比传》留下深刻印象的文字
- Leetcode-32. Longest Valid Parentheses
- cs231n:assignment1——Q1: k-Nearest Neighbor classifier(自动生成版)
- 云计算笔记(第六天)
- 小R与手机
- 出现次数最多的字符
- python写的倒计时表达-学习笔记9-嵌套循环