Volley源码分析三部曲
Volley 源码解析之网络请求
Volley 源码解析之图片请求
Volley 源码解析之缓存机制

Volley 是 Google 推出的一款网络通信框架,非常适合数据量小、通信频繁的网络请求,支持并发、缓存和容易扩展、调试等;不过不太适合下载大文件、大量数据的网络请求,因为volley在解析期间将响应放到内存中,我们可以使用okhttp或者系统提供的DownloadManager来下载文件。

一、简单使用

首先在工程引入volley的library:

dependencies {implementation 'com.android.volley:volley:1.1.1'
}
复制代码

然后需要我们打开网络权限,我这里直接贴出官网简单请求的示例代码:

final TextView mTextView = (TextView) findViewById(R.id.text);
// ...// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,new Response.Listener<String>() {@Overridepublic void onResponse(String response) {// Display the first 500 characters of the response string.mTextView.setText("Response is: "+ response.substring(0,500));}
}, new Response.ErrorListener() {@Overridepublic void onErrorResponse(VolleyError error) {mTextView.setText("That didn't work!");}
});// Add the request to the RequestQueue.
queue.add(stringRequest);
复制代码

使用相对简单,回调直接在主线程,我们取消某个请求直接这样操作:

  1. 定义一个标记添加到requests中

    public static final String TAG = "MyTag";
    StringRequest stringRequest; // Assume this exists.
    RequestQueue mRequestQueue;  // Assume this exists.// Set the tag on the request.
    stringRequest.setTag(TAG);// Add the request to the RequestQueue.
    mRequestQueue.add(stringRequest);
    复制代码
  2. 然后我们可以在 onStop() 中取消所有标记的请求

    @Override
    protected void onStop () {super.onStop();if (mRequestQueue != null) {mRequestQueue.cancelAll(TAG);}
    }
    复制代码

二、源码分析

我们先从Volley这个类入手:

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);
}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;
}public static RequestQueue newRequestQueue(Context context) {return newRequestQueue(context, (BaseHttpStack) null);
}
复制代码

当我们传递一个Context的时候,首先为BaseHttpStack为null,会执行到创建BaseHttpStackBaseHttpStack是一个网络具体的处理请求,Volley默认提供了基于HttpURLCollectionHurlStack和基于HttpClientHttpClientStack。Android6.0移除了HttpClient,Google官方推荐使用HttpURLCollection类作为替换。所以这里在API大于9的版本是用的是HurlStack,为什么这样选择,详情可见这篇博客Android访问网络,使用HttpURLConnection还是HttpClient?。我们使用的是默认的构造,BaseHttpStack传入为null,如果我们想使用自定义的okhttp替换底层,我们直接继承HttpStack重写即可,也可以自定义NetworkRequestQueue,Volley的高扩展性充分体现。接下来则创建一个Network对象,然后实例化RequestQueue,首先创建了一个用于缓存的文件夹,然后创建了一个磁盘缓存,将文件缓存到指定目录的硬盘上,默认大小是5M,但是大小可以配置。接下来调用RequestQueuestart()方法进行启动,我们进入这个方法查看一下:

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

开始启动的时候先停止所有的请求线程和网络缓存线程,然后实例化一个缓存线程并运行,然后一个循环开启DEFAULT_NETWORK_THREAD_POOL_SIZE(4)个网络请求线程并运行,一共就是5个线程在后台运行,不断的等待网络请求的到来。

构造了RequestQueue之后,我们调用add()方法将相应的Request传入就开始执行网络请求了,我们看看这个方法:

public <T> Request<T> add(Request<T> request) {//将请求队列和请求关联起来request.setRequestQueue(this);//添加到正在请求中但是还未完成的集合中synchronized (mCurrentRequests) {mCurrentRequests.add(request);}//设置请求的一个序列号,通过原子变量的incrementAndGet方法,//以原子方式给当前值加1并获取新值实现请求的优先级request.setSequence(getSequenceNumber());//添加一个调试信息request.addMarker("add-to-queue");//如果不需要缓存则直接加到网络的请求队列,默认每一个请求都是缓存的,//如果不需要缓存需要调用Request的setShouldCache方法来修改if (!request.shouldCache()) {mNetworkQueue.add(request);return request;}//加到缓存的请求队列mCacheQueue.add(request);return request;
}
复制代码

关键地方都写了注释,主要作用就是将请求加到请求队列,执行网络请求或者从缓存中获取结果。网络和缓存的请求都是一个优先级阻塞队列,按照优先级出队。上面几个关键步骤,添加到请求集合里面还有设置优先级以及添加到缓存和请求队列都是线程安全的,要么加锁,要么使用线程安全的队列或者原子操作。

接下来我们看看添加到CacheDispatcher缓存请求队列的run方法:

@Override
public void run() {if (DEBUG) VolleyLog.v("start new dispatcher");Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);//初始化DiskBasedCache的缓存类mCache.initialize();while (true) {try {processRequest();} catch (InterruptedException e) {if (mQuit) {Thread.currentThread().interrupt();return;}VolleyLog.e("Ignoring spurious interrupt of CacheDispatcher thread; "+ "use quit() to terminate it");}}
}
复制代码

接下来的重点是看看processRequest()这个方法:

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");// 如果请求被取消,我们可以通过RequestQueue的回调接口来监听if (request.isCanceled()) {request.finish("cache-discard-canceled");return;}// 从缓存中获取Cache.EntryCache.Entry entry = mCache.get(request.getCacheKey());//没有取到缓存if (entry == null) {request.addMarker("cache-miss");// 缓存未命中,对于可缓存的请求先去检查是否有相同的请求是否已经在运行中,//如果有的话先加入请求等待队列,等待请求完成,返回true;如果返回false则表示第一次请求if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {//加入到网络请求的阻塞队列mNetworkQueue.put(request);}return;}// 如果缓存完全过期,处理过程跟上面类似if (entry.isExpired()) {request.addMarker("cache-hit-expired");//设置请求缓存的entry到这个request中request.setCacheEntry(entry);if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {mNetworkQueue.put(request);}return;}//缓存命中,将数据解析并返回到request的抽象方法中request.addMarker("cache-hit");Response<?> response =request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders));request.addMarker("cache-hit-parsed");//判断请求结果是否需要刷新if (!entry.refreshNeeded()) {// 未过期的缓存命中,通过ExecutorDelivery回调给我们的request子类的接口中,// 我们在使用的时候就可以通过StringRequest、JsonRequest等拿到结果,// 切换到主线程也是在这个类里执行的mDelivery.postResponse(request, response);} else {request.addMarker("cache-hit-refresh-needed");request.setCacheEntry(entry);// 将这个响应标记为中间值,即这个响应是新鲜的,那么第二个响应正在请求随时到来response.intermediate = true;if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {//发起网络请求,这里为什么直接调用上面的mNetworkQueue.put(request);呢,//主要是为了添加一个已经分发的标记,在响应分发的时候不再回调给用户,//不然就回调了两次mDelivery.postResponse(request,response,new Runnable() {@Overridepublic void run() {try {mNetworkQueue.put(request);} catch (InterruptedException e) {// Restore the interrupted statusThread.currentThread().interrupt();}}});} else {//这里第三个参数传递null,不用再去分发,因为已经有相同的请求已经在执行,//直接添加到了等待请求的列表中,然后返回的时候从已经执行的请求收到响应mDelivery.postResponse(request, response);}}
}
复制代码

这部分主要是对请求的缓存判断,是否过期以及需要刷新缓存。我们调用取消所有请求或者取消某个请求实质上就是对mCanceled这个变量赋值,然后在缓存线程或者网络线程里面都回去判断这个值,就完成了取消。上面的isExpiredrefreshNeeded,两个区别就是,前者如果过期就直接请求最新的内容,后者就是还在新鲜的时间内,但是把内容返回给用户还是会发起请求,两者一个与ttl值相比,另一个与softTtl相比。

其中有一个WaitingRequestManager,如果有相同的请求那么就需要一个暂存的地方,这个类就是做的这个操作

private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {//所有等待请求的集合,键是缓存的keyprivate final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();private final CacheDispatcher mCacheDispatcher;WaitingRequestManager(CacheDispatcher cacheDispatcher) {mCacheDispatcher = cacheDispatcher;}//请求接受到一个有效的响应,后面等待的相同请求就可以使用这个响应@Overridepublic void onResponseReceived(Request<?> request, Response<?> response) {//如果缓存为空或者已经过期,那么就释放等待的请求if (response.cacheEntry == null || response.cacheEntry.isExpired()) {onNoUsableResponseReceived(request);return;}String cacheKey = request.getCacheKey();//等待的请求的集合List<Request<?>> waitingRequests;synchronized (this) {//从map里面移除这个请求的集合waitingRequests = mWaitingRequests.remove(cacheKey);}if (waitingRequests != null) {if (VolleyLog.DEBUG) {VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",waitingRequests.size(), cacheKey);}// 里面所有的请求都分发到相应的回调执行,下面会讲解for (Request<?> waiting : waitingRequests) {mCacheDispatcher.mDelivery.postResponse(waiting, response);}}}//没有收到相应,则需要释放请求@Overridepublic synchronized void onNoUsableResponseReceived(Request<?> request) {String cacheKey = request.getCacheKey();List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);if (waitingRequests != null && !waitingRequests.isEmpty()) {if (VolleyLog.DEBUG) {VolleyLog.v("%d waiting requests for cacheKey=%s; resend to network",waitingRequests.size(), cacheKey);}//下面这个请求执会重新执行,将这个移除添加到Request<?> nextInLine = waitingRequests.remove(0);//将剩下的请求放到等待请求的map中mWaitingRequests.put(cacheKey, waitingRequests);//在request里面注册一个回调接口,因为重新开始请求,需要重新注册一个监听,//后面请求成功失败以及取消都可以收到回调nextInLine.setNetworkRequestCompleteListener(this);try {//从上面if判断方法可以得出:waitingRequests != null && !waitingRequests.isEmpty()//排除了第一次请求失败、取消的情况,后面的那个条件则表示这个等待请求队列必须要有一个请求,//同时满足才会执行这里面的代码,一般只要这里面的请求执行成功一次后续所有的请求都会被移除,//所以这里对多个请求的情况,失败一次,那么后续的请求会继续执行mCacheDispatcher.mNetworkQueue.put(nextInLine);} catch (InterruptedException iex) {VolleyLog.e("Couldn't add request to queue. %s", iex.toString());// Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)Thread.currentThread().interrupt();// Quit the current CacheDispatcher thread.mCacheDispatcher.quit();}}}//对于可以缓存的请求,相同缓存的请求已经在运行中就添加到一个发送队列,//等待运行中的队列请求完成,返回true表示已经有请求在运行,false则是第一次执行private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {String cacheKey = request.getCacheKey();// 存在相同的请求则把请求加入到相同缓存键的集合中if (mWaitingRequests.containsKey(cacheKey)) {// There is already a request in flight. Queue up.List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);//如果包含相同的请求但是有可能是第二次请求,前面第一次请求插入null了if (stagedRequests == null) {stagedRequests = new ArrayList<>();}request.addMarker("waiting-for-response");stagedRequests.add(request);mWaitingRequests.put(cacheKey, stagedRequests);if (VolleyLog.DEBUG) {VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);}return true;} else {//第一次请求那么则插入一个null,表示当前有一个请求正在运行mWaitingRequests.put(cacheKey, null);//注册一个接口监听request.setNetworkRequestCompleteListener(this);if (VolleyLog.DEBUG) {VolleyLog.d("new request, sending to network %s", cacheKey);}return false;}}
}
复制代码

这个类主要是避免相同的请求多次请求,而且在NetworkDispatcher里面也会通过这个接口回调相应的值在这里执行,最终比如在网络请求返回304、请求取消或者异常那么都会在这里来处理,如果收到响应则会把值回调给用户,后面的请求也不会再去请求,如果无效的响应则会做一些释放等待的请求操作,请求完成也会将后面相同的请求回调给用户,三个方法都在不同的地方发挥作用。

我们接下来看看NetworkDispatcher网络请求队列的run方法中的processRequest方法:

@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);// 这里就是执行网络请求的地方NetworkResponse networkResponse = mNetwork.performRequest(request);request.addMarker("network-http-complete");// 如果服务器返回304响应,即没有修改过,//缓存依然是有效的并且是在需要刷新的有效期内,那么则不需要解析响应if (networkResponse.notModified && request.hasHadResponseDelivered()) {request.finish("not-modified");//没有收到来自网络的有效响应,释放请求request.notifyListenerResponseNotUsable();return;}// 在工作线程中解析这些响应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) {...}
}
复制代码

这里才是网络请求的真正执行以及解析分发的地方,重点看两个地方的代码,执行和解析,我们先看看执行网络请求这个代码,执行的地方是BasicNetwork.performRequest,下面看看这个方法:

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {long requestStart = SystemClock.elapsedRealtime();while (true) {HttpResponse httpResponse = null;byte[] responseContents = null;List<Header> responseHeaders = Collections.emptyList();try {// 构造缓存的头部,添加If-None-Match和If-Modified-Since,都是http/1.1中控制协商缓存的两个字段,               // If-None-Match:客服端再次发起请求时,携带上次请求返回的唯一标识Etag值,//服务端用携带的值和最后修改的值作对比,最后修改时间大于携带的字段值则返回200,否则304;// If-Modified-Since:客服端再次发起请求时,携带上次请求返回的Last-Modified值,//服务端用携带的值和服务器的Etag值作对比,一致则返回304Map<String, String> additionalRequestHeaders =getCacheHeaders(request.getCacheEntry());//因为现在一般的sdk都是大于9的,那么这里执行的就是HurlStack的executeRequest方法,//执行网络请求,和我们平时使用HttpURLConnection请求网络大致相同httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);int statusCode = httpResponse.getStatusCode();responseHeaders = httpResponse.getHeaders();// 服务端返回304时,那么就表示资源无更新,可以继续使用缓存的值if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {Entry entry = request.getCacheEntry();if (entry == null) {return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED,/* data= */ null,/* notModified= */ true,SystemClock.elapsedRealtime() - requestStart,responseHeaders);}// 将缓存头和响应头组合在一起,一次响应就完成了List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED,entry.data,/* notModified= */ true,SystemClock.elapsedRealtime() - requestStart,combinedHeaders);}// 如果返回204,执行成功,没有数据,这里需要检查InputStream inputStream = httpResponse.getContent();if (inputStream != null) {responseContents =inputStreamToBytes(inputStream, httpResponse.getContentLength());} else {//返回204,就返回一个空的byte数组responseContents = new byte[0];}// if the request is slow, log it.long requestLifetime = SystemClock.elapsedRealtime() - requestStart;logSlowRequests(requestLifetime, request, responseContents, statusCode);if (statusCode < 200 || statusCode > 299) {throw new IOException();}return new NetworkResponse(statusCode,responseContents,/* notModified= */ false,SystemClock.elapsedRealtime() - requestStart,responseHeaders);} catch (SocketTimeoutException e) {//异常进行重新请求等...}}
}
复制代码

这里主要执行了添加缓存头并发起网络请求,然后将返回值组装成一个NetworkResponse值返回,接下来我们看看是如何解析这个值的,解析是由Request的子类去实现的,我们就看系统提供的StringRequest

@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));
}
复制代码

我们可以看到将值组装成一个String,然后组装成一个Response返回,接下来看看这里如何将这个值回调给用户的这个方法mDelivery.postResponse(request, response),这里我们先重点看看这个类ExecutorDelivery:

public class ExecutorDelivery implements ResponseDelivery {//构造执行已提交的Runnable任务对象private final Executor mResponsePoster;//这里在RequestQueue构造参数中初始化,new ExecutorDelivery(new Handler(Looper.getMainLooper())),//那么这里runnable就通过绑定主线程的Looper的Handler对象投递到主线程中执行public ExecutorDelivery(final Handler handler) {// Make an Executor that just wraps the handler.mResponsePoster =new Executor() {@Overridepublic void execute(Runnable command) {handler.post(command);}};}public ExecutorDelivery(Executor executor) {mResponsePoster = executor;}//这个方法就是我们NetworkDispatcher里面调用的方法,调用下面这个三个参数的构造方法@Overridepublic void postResponse(Request<?> request, Response<?> response) {postResponse(request, response, null);}@Overridepublic void postResponse(Request<?> request, Response<?> response, Runnable runnable) {request.markDelivered();request.addMarker("post-response");//构造了一个ResponseDeliveryRunnable类,传入execute,现在这个runnable就是在主线程里执行mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));}@Overridepublic void postError(Request<?> request, VolleyError error) {request.addMarker("post-error");Response<?> response = Response.error(error);mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));}/** A Runnable used for delivering network responses to a listener on the main thread. */@SuppressWarnings("rawtypes")private static 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")@Overridepublic void run() {//请求取消,那么就不分发给用户if (mRequest.isCanceled()) {mRequest.finish("canceled-at-delivery");return;}// 根据isSuccess这个值来提供相应的回调给用户,调用Response会通过error的值是否为null来确定这个值,//我们调用VolleyError这个构造函数的时候就为这个值就为falseif (mResponse.isSuccess()) {mRequest.deliverResponse(mResponse.result);} else {mRequest.deliverError(mResponse.error);}// 如果这是一个在新鲜的时间内的请求的响应,就添加一个标记,否则就结束if (mResponse.intermediate) {mRequest.addMarker("intermediate-response");} else {mRequest.finish("done");}// 在CacheDispatcher里面当请求第一次请求时直接调用三个参数的构造方法,通过这个runnable就执行run方法if (mRunnable != null) {mRunnable.run();}}}
}复制代码

上面方法主要是将值回调给用户,那么整个网络请求大致就完成了,其中还涉及很多细节的东西,但是大致流程是走通了,不得不说这个库有很多值得我们学习的地方。

三、总结

现在我们看官网的一张图,总结一下整个流程:

  • 蓝色是主线程
  • 绿色是缓存线程
  • 黄色是网络线程

我们可以看到首先是请求添加到RequestQueue里,首先是添加到缓存队列,然后查看是否已经缓存,如果有并且在有效期内的缓存直接回调给用户,如果没有查找到,那么则需要添加到网络请求队列重新请求并且解析响应、写入缓存在发送到主线程给用户回调。

参考以及相关链接

  • 【第1250期】彻底理解浏览器的缓存机制
  • Android Volley完全解析(四),带你从源码的角度理解Volley
  • Volley 源码解析
  • Volley 源码解析

转载于:https://juejin.im/post/5c1c58b35188251f1f320e70

Volley 源码解析之网络请求相关推荐

  1. Volley 源码解析之图片请求

    一.前言 上篇文章我们分析了网络请求,这篇文章分析对图片的处理操作,如果没看上一篇,可以先看上一篇文章Volley 源码解析之网络请求.Volley 不仅仅对请求网络数据作了良好的封装,还封装了对图片 ...

  2. okhttp的应用详解与源码解析--android网络请求框架发展史

    乘5G之势,借物联网之风,Android未来亦可期,Android优势在于开放,手机.平板.车载设备.智能家居等都是Android的舞台,Google不倒,Android不灭,本专栏的同步视频教程已经 ...

  3. http://a.codekk.com/detail/Android/grumoon/Volley 源码解析

    http://a.codekk.com/detail/Android/grumoon/Volley 源码解析

  4. Android之Volley 源码解析

    原文来自:http://www.codekk.com  1. 功能介绍  1.1. Volley Volley 是 Google 推出的 Android 异步网络请求框架和图片加载框架.在 Googl ...

  5. tp5源码分析之网络请求

    1 网络请求 网络请求 对客户端而言,指服务器发起的请求操作. 对服务器端而言,指客户端发起的请求信息. 服务器端主要用来对客户端发起的网络请求进行处理. 2 请求信息 2-1 Url相关 Reque ...

  6. Swift - Alamofire源码解析

    文章目录 Alamofire源码解析 一.Alamofire目录结构 二.使用的基本流程 (不讲解基本使用,从源码解析) 1. 发起请求的源码 (举例:普通请求流程) 2. SessionManage ...

  7. 源码解析-Volley(转自codeKK)

    Volley 源码解析 本文为 Android 开源项目源码解析 中 Volley 部分 项目地址:Volley,分析的版本:35ce778,Demo 地址:Volley Demo 分析者:grumo ...

  8. Spring 注解面面通 之 @CrossOrigin 处理请求源码解析

      @CrossOrigin源码解析主要分为两个阶段:   ① @CrossOrigin注释的方法扫描注册.   ② 请求匹配@CrossOrigin注释的方法.   本文针对第②阶段从源码角度进行解 ...

  9. OkHttp3源码解析(三)——连接池复用

    OKHttp3源码解析系列 OkHttp3源码解析(一)之请求流程 OkHttp3源码解析(二)--拦截器链和缓存策略 本文基于OkHttp3的3.11.0版本 implementation 'com ...

最新文章

  1. 自定义spring schema简化与canal集成
  2. Python语言学习之字母R开头函数使用集锦:random/range/replace/reshape用法之详细攻略
  3. 获取当天0点数据scala实现
  4. mysqlsla日志分析工具
  5. sqlserver 查看锁表,解锁
  6. SQL查询单表数据之排序(二)
  7. Android 8.0学习(8)---内核文件系统优化
  8. python获取当前脚本所在路径并在此基础上创建新的文件路径
  9. .sln文件和.suo文件的解释
  10. Eclipse \ MyEclipse \Scala IDEA for Eclipse里如何将控制台console输出的过程记录全程保存到指定的文本文件(图文详解)...
  11. NMPA已注册肿瘤小Panel试剂盒生物信息学分析内容对比
  12. 区块链之零知识证明(zk-SNARK从小白到明白)
  13. linux修改IP地址的命令
  14. 使用PHP制作 简易员工管理系统之三(管理员登陆界面以及数据库验证)
  15. discuz php单页,Discuz! 单页制作教程
  16. 笔记本电脑计算机的配置表,笔记本组装配置清单_笔记本电脑配置单及价格
  17. Android实现VR图片、视频小程序
  18. VC6.0修改工程名字方法
  19. A002-181-2162
  20. 最小二乘法在编程中的实现

热门文章

  1. typedef 字符串_typedef在C中使用字符数组(定义别名来声明字符串)的示例
  2. pl/sql中的赋值运算符_如何在SQL中使用AND / OR运算符?
  3. c#数组获取元素的索引_获取元素集合 从C#中的指定索引
  4. 任重道远——记2016暑假训练
  5. node.js将buffer对象转换为json对象
  6. BZOJ2809-左偏树合并
  7. 基于单链表的生产者消费者问题
  8. C++11新特性之八——函数对象function
  9. mysql远程连接权限grant all privileges on *.* to ‘root‘@‘%‘ identified by ‘123456‘ with grant option语句报错
  10. 【算法】学习笔记(4):分治思想 归并排序