Android源码系列三:Volley源码剖析

Volley is an HTTP library that makes networking for Android apps easier and most importantly, faster.

Volley是Google在2013年推出来的HTTP库,旨在帮助开发者更快更简便的实现网络请求。说说为什么要分析Volley的源码吧,因为Volley中线程的转换时通过 ThreadHandler 来实现的,跟之前的两篇都有着很大的联系(ps:Okhttp和Retrofit都撕不动_),哈哈,后面会一步一步的给大家带来Okhttp和Retrofit等更多的源码分析!

执行一个网络请求

我们先整体看下 Volley 是如何进行一个完整的网络请求的:

val requestQueue: RequestQueue = Volley.newRequestQueue(context)
val url = "https://www.baidu.com"
val request = StringRequest(url,
        Response.Listener<String> {
            Log.d("taonce", "request result is: $it")
        },
        Response.ErrorListener { })

requestQueue.add(request)

上面代码主要做了三件事:

  • 创建一个请求队列 RequestQueue : Volley.newRequestQueue(context)
  • 创建一个请求 Request : StringRequest(String url, Listener<String> listener, @Nullable ErrorListener errorListener)
  • Request 加入到 RequestQueue : requestQueue.add(request)

接下来通过源码的方法来看看这三步内部做了什么操作。

创建 RequestQueue 和 Request进入 Volley.newRequestQueue(context) 源码:

进入 Volley.newRequestQueue(context) 源码:

public static RequestQueue newRequestQueue(Context context) {
    // 实际上是调用了另外一个构造方法
    return newRequestQueue(context, (BaseHttpStack) null);
}

继续查看 newRequestQueue(Context context, BaseHttpStack stack) :

BasicNetwork network;
if (stack == null) {
    // 判断是否大于等于 Android 2.3 版本
    if (Build.VERSION.SDK_INT >= 9) {
        // 如果是 Android 2.3 及其以上,就用 HurlStack() 进行网络请求
        network = new BasicNetwork(new HurlStack());
    } else {
        // 如果是 Android 2.3 以下,那么就采用 HttpClientStack(HttpClient) 进行网络请求
        network = new BasicNetwork(
                        new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
    }
} else {
    // 如果stack不为空,那么就采用传进来的stack
    network = new BasicNetwork(stack);
}

return newRequestQueue(context, network);

可以得出:

  • 创建一个 BasicNetwork 对象
  • stack 为空 ----> Android 2.3 及其以上创建 HurlStack() 对象,并且传给 networkHurlStack() 采用的是 HttpURLConnetion 进行网络请求的。
  • stack 为空 ----> Android 2.3 以下创建 HttpClientStack() 对象,并且传给 networkHttpClientStack() 采用的则是 HttpClient 进行网络请求,不过现在( 当前版本1.1.1 ) new HttpClientStack(HttpClient client) 已经被标记了 @Deprecated 了,因为它采用的 HttpClientGoogle 在 Android 6.0 中移除了对 Apache HTTP 客户端的支持,并且从 Android P 开始,org.apache.legacy 库将从 bootclasspath 中删除。
  • stack 不为空 ----> 直接将 stack 传给 network
  • 创建一个 RequestQueue() 对象并返回

到此为止,大家只要记住上面几个对象就好,接下来会慢慢的讲解对象分别做了什么工作。

这里还是要着重介绍下 newRequestQueue() 方法:

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;
}

创建 RequestQueue() 的过程中有一个很重要的点,我们来看看它的构造方法:

public RequestQueue(Cache cache, Network network) {
    // 默认 Network Thread 数目为 DEFAULT_NETWORK_THREAD_POOL_SIZE = 4
    this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
    this(
            cache,
            network,
            threadPoolSize,
            // 注意点就是这:创建 Handler 的时候,传递的是 Looper.getMainLooper(),也就是后面将请求结果回调到主线程的关键。
            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;
}

上面第二个构造方法中创建了一个 ExecutorDelivery() , 这个对象实现了 ResponseDelivery 接口的类,用来将网络请求的结果或者缓存中的结果分发到主线程。

再来看看上面 queue.start() 方法做了什么操作:

// 开启5个线程
public void start() {
    // 如果5个线程不为空,先停止它们
    stop(); 
    // 创建 CacheDispatcher,并开启它
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();

    // 创建4个NetworkDispatcher,并开启它们
    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher =
                new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

到此,我们前奏分析完了,有一些概念先不急着看,只要知道它的作用就行,后面我们来一个一个击破。

添加请求到请求队列中 : RequestQueue.add( request )

废话不多了,直接进入源码:RequestQueue.add(request)

public <T> Request<T> add(Request<T> request) {
    // 将request和当前的RequestQueue绑定
    request.setRequestQueue(this);
    // 将request添加到set集合中,用于执行 cancelAll() 操作
    synchronized (mCurrentRequests) {
        mCurrentRequests.add(request);
    }

    // 给request添加一些标记
    request.setSequence(getSequenceNumber());
    request.addMarker("add-to-queue");
    sendRequestEvent(request, RequestEvent.REQUEST_QUEUED);

    // 判断request是否需要缓存,对于 Get 以外的请求,默认关闭缓存  
    if (!request.shouldCache()) {
        mNetworkQueue.add(request);
        return request;
    }
    mCacheQueue.add(request);
    return request;
}

添加 request 的操作算是很简单的了,无非是根据 request 是否需要缓存,将 request 添加到 CacheQueue 或者 NetworkQueue 中。

看到这,我们是不是在想,怎么添加之后就没有动作了?非也非也,我们回顾下,在创建 requestQueue 的同时,是不是创建了5个子线程并且开启了它们,它们内部使用的 CacheQueue 或者 NetworkQueue 不就是上面提到的么。接下来,我们先看看 CacheDispatcher 内部做了什么。

CacheDispatcher 调度器剖析:

CacheDispatcher 就是一个继承了 Thread 的类,我们在执行 RequestQueue.start() 的时候创建了它,并开启了它,现在我们来看看它的 run() 方法:

@Override
public void run() {
    // 设置线程优先级为后台线程
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    // 实则是执行了DiskBasedCache的initialize()方法
    mCache.initialize();
    while (true) {
        try {
            processRequest();
        } catch (InterruptedException e) {
            // 中断当前线程,并且退出死循环,mQuit在调用CacheDispatcher的quit()方法之后会被赋值为true
            if (mQuit) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }
}

开了一个死循环,然后调用了 processRequest() , 我们接着看这个方法:

    private void processRequest() throws InterruptedException {
        // 从CacheQueue中取出一个可用的request
        final Request<?> request = mCacheQueue.take();
        processRequest(request);
    }

    @VisibleForTesting
    void processRequest(final Request<?> request) throws InterruptedException {
        request.addMarker("cache-queue-take");
        request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED);

        try {
            //request如果被取消了,就直接返回
            if (request.isCanceled()) {
                request.finish("cache-discard-canceled");
                return;
            }

            Cache.Entry entry = mCache.get(request.getCacheKey());
            // 没有缓存就把request添加到NetworkQueue中
            if (entry == null) {
                request.addMarker("cache-miss");
                // 没有缓存,并且等待队列中也没有此request,那么就直接加入到NetworkQueue中
                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                    mNetworkQueue.put(request);
                }
                return;
            }

            // 如果缓存过期了,也是一样把request添加到NetworkQueue中
            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);
                response.intermediate = true;
                // 判断是否有相同缓存键的任务在执行
                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                    // 需要更新结果,先将结果调度到主线程,然后执行new runnable(){}
                    // runnable中就是将request添加到NetworkQueue中,更新一下内容
                    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已经加入到mWaitingRequests中
                    // 直接把结果调度到主线程
                    mDelivery.postResponse(request, response);
                }
            }
        } finally {
            request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
        }
    }

我们在 processRequest 中可以看到有一个方法经常出现,那就是 mWaitingRequestManager.maybeAddToWaitingRequests(request) ,它的作用是判断当前这个 request 是否有存在相同缓存键的请求已经处于运行状态,如果有,那么就将这个 request 加入到一个等待队列中,等到相同缓存键的请求完成。

总结一下 CacheDispatcher 主要步骤:

  • CacheQueue 中循环取出 request
  • 如果缓存丢失,加入到 NetworkQueue 中;
  • 如果缓存过期,加入到 NetworkQueue 中;
  • 将缓存中的数据解析成 Response 对象;
  • 如果不需要更新,直接将结果回调到主线程,回调操作等介绍完NetworkDispatcher之后一起深入剖析;
  • 如果需要更新,先将结果回调到主线程,然后再将 request 加入到 NetworkQueue 中。

NetworkDispatcher 调度器剖析:

NetworkDispatcherCacheDispatcher 十分类似,都是 Thread 的子类,下面重点看下它的 run() 方法:

@Override
public void run() {
    // 设置线程优先级为后台线程
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    while (true) {
        try {
            processRequest();
        } catch (InterruptedException e) {
            // 调用quit()方法之后,mQuit就会被赋值true
            if (mQuit) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }
}

继续看 processRequest() 方法:

private void processRequest() throws InterruptedException {
    // 从NetworkQueue中取出request
    Request<?> request = mQueue.take();
    processRequest(request);
}

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

        // 如果request被取消了,那么就不执行此request
        if (request.isCanceled()) {
            request.finish("network-discard-cancelled");
            request.notifyListenerResponseNotUsable();
            return;
        }

        addTrafficStatsTag(request);

        // 还记得这个mNetwork么,它就是Volley.newRequestQueue()方法里的BasicNetwork对象,一会我们来看看mNetwork.performRequest()方法是如何得到NetworkResponse的
        NetworkResponse networkResponse = mNetwork.performRequest(request);
        request.addMarker("network-http-complete");

        // notModified是服务端返回304,hasHadResponseDelivered()是request已经回调过了
        if (networkResponse.notModified && request.hasHadResponseDelivered()) {
            request.finish("not-modified");
            request.notifyListenerResponseNotUsable();
            return;
        }

        // 将NetworkResponse解析成Response对象,在子线程中执行
        Response<?> response = request.parseNetworkResponse(networkResponse);
        request.addMarker("network-parse-complete");

        // 将request写入缓存
        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();
    } finally {
        request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED);
    }
}

通过 NetworkDispatcher.run() 方法可以发现,主要分为以下几步:

  • 通过 BasicNetwork.performRequest(request) 得到 NetworkResponse 对象;
  • 通过 request.parseNetworkResponse(networkResponse) 解析得到 Response 对象;
  • 通过 mDelivery 将成功结果或者失败结果回调到主线程。

现在我们依次来分析下这三步:

  1. 请求网络,得到 NetworkResponse

    BasicNetwork 实现了 Network 接口,其主要代码集中在 NetworkResponse performRequest(Request<?> request) throws VolleyError 中,也就是执行特定的请求。

    @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 {
                // 得到请求头信息
                Map<String, String> additionalRequestHeaders =
                        getCacheHeaders(request.getCacheEntry());
                // 具体的网络请求是靠BaseHttpStack执行的
                httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
                int statusCode = httpResponse.getStatusCode();
    
                responseHeaders = httpResponse.getHeaders();
                // 下面就是根据不同的状态码返回不同的NetworkResponse对象了,具体就不分析了
                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);
                    }
                }
                // 省略大部分代码...
    }
    

    通过上面源码可以看出,BasicNetwork 就是封装了一下 NetworkResponse 对象蛮,并没有涉及到网络请求,我们继续深入到 BaseHttpStack.executeRequest(request, additionalRequestHeaders) 源码中。

    public abstract class BaseHttpStack implements HttpStack {
        public abstract HttpResponse executeRequest(
                Request<?> request, Map<String, String> additionalHeaders)
                throws IOException, AuthFailureError;
    }
    

    我们发现 BaseHttpStack 是一个抽象类,那么具体的请求是在哪呢,不知道你是否还有印象,在Volley.newRequestQueue() 中,我们创建 BasicNetwork 的时候是根据 Android 版本传入不同的 BaseHttpStack 子类,Android 2.3 以上我们传入的是 HurlStack 对象,下面就看看 HurlStack.executeRequest() 方法吧。

    @Override
    public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {
        // 得到url
        String url = request.getUrl();
        HashMap<String, String> map = new HashMap<>();
        map.putAll(additionalHeaders);
        // Request.getHeaders() takes precedence over the given additional (cache) headers).
        map.putAll(request.getHeaders());
     ...
        URL parsedUrl = new URL(url);
        // 通过HttpURLConnection来请求网络
        HttpURLConnection connection = openConnection(parsedUrl, request);
    }
    

    其实 HurlStack.executeRequest() 方法里,就是借助了 HttpURLConnection 对象来请求网络,并根据不同的条件返回不同的 HttpResponse 对象的。

  2. 解析 NetworkResponse , 得到 Response

    解析过程是定义在 Request 抽象类的 protected abstract Response<T> parseNetworkResponse(NetworkResponse response) 抽象方法中,对于每一种具体的 Request 类,比如:StringRequestJsonObjectRequest 等等都有自己的实现,下面我们来看看 StringRequest 中的 parseNetworkResponse() 方法:

    @Override
    @SuppressWarnings("DefaultCharset")
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            // 将response中的data信息取出来
            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);
        }
        // 封装成Response对象
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }
    
  3. 回调主线程,如果你看过我之前的 Handler源码剖析 文章话,那么这一步就很简单了,我们来理一理:

    回调是通过 ResponseDelivery mDelivery 对象来执行的,这个对象最早是在创建 RequestQueue() 的时候初始化的,我在那一小节特意标注了,再来回顾下:

    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
            this(
                    cache,
                    network,
                    threadPoolSize,
                 // 创建ExecutorDelivery对象,传入一个Handler对象,并且Handler绑定的是主线程的Looper
                    new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }
    
    public class ExecutorDelivery implements ResponseDelivery {
        private final Executor mResponsePoster;
        /**
         * ExecutorDelivery构造函数,内部初始化了mResponsePoster接口
         * 并且在execute()方法里调用了handler.post()方法
         */
        public ExecutorDelivery(final Handler handler) {
            // 初始化Execute对象
            mResponsePoster =
                    new Executor() {
                        @Override
                        public void execute(Runnable command) {
                            handler.post(command);
                        }
                    };
        }
    }
    

    知道了它的初始化,我们再来看看它是如何实现回调的:

    Volley 中回调是通过postResponse()方法的 :

    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));
    }
    

    postResponse() 最终会调用 mResponsePoster 对象的 execute() 方法,传入了一个 ResponseDeliveryRunnable 对象,它实现了 Runnable 接口,execute() 方法会通过 Handler.post(runnable)ResponseDeliveryRunnable 放入消息队列。最后我们来看看这个 ResponseDeliveryRunnablerun() 方法在主线程中做了什么操作:

    @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;
        }
    
        if (mResponse.isSuccess()) {
            // 执行成功的回调,在具体的Request实现类中,比如StringRequest就会调用listener.onResponse(string)回调
            mRequest.deliverResponse(mResponse.result);
        } else {
            // 执行失败的回调,在request中,直接回调了listener.onErrorResponse(error)
            mRequest.deliverError(mResponse.error);
        }
    
        // intermediate默认为false,但是在CacheDispatcher的run()中,如果需要更新缓存,那么就会置为true
        if (mResponse.intermediate) {
            mRequest.addMarker("intermediate-response");
        } else {
            mRequest.finish("done");
        }
    
        // 如果传入了runnable不为空,那就就执行runnable.run()方法
        // 回忆下在CacheDispatcher的run()方法中,如果request有缓存,但是需要更新缓存的时候,mDelivery是不是调用的带runnable的方法
        if (mRunnable != null) {
            mRunnable.run();
        }
    }
    

    分析到这的时候,大家可以借助下方的流程图理一理Volley的整体流程,主要理解一下缓存线程和网络线程的转换,子线程和主线程的转换:


    Volley主流程

    源码分析的文字还在不断的更新,如果本文章你发现的不正确或者不足之处,欢迎你在下方留言或者扫描下方的二维码留言也可!


    扫描关注
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 注:本文转自http://codekk.com/open-source-project-analysis/deta...
    Ten_Minutes阅读 1,285评论 1 16
  • 博文出处:Volley框架源码解析,欢迎大家关注我的博客,谢谢! 0001B 在 2013 年的 Google I...
    俞其荣阅读 2,175评论 10 45
  • 我们再来看看volley是怎么工作的。首先还是要带着重点去看源码,我们要关注的地方除了最核心的工作流程之外,还有一...
    反复横跳的龙套阅读 486评论 0 1
  • Volley 的中文翻译为“齐射、并发”,是在2013年的Google大会上发布的一款Android平台网络通信库...
    万剑阅读 1,271评论 0 3
  • 我的博客: Volley 源码分析 Volley 的使用流程分析 官网示例 创建一个请求队列 RequestQue...
    realxz阅读 2,033评论 1 11