手撕 Volley(二)

手撕Volley(一)

rick-and-morty-9.png

來繼續(xù)我們的源碼之旅嗽仪,這邊文章將會(huì)包括一下內(nèi)容

添加請(qǐng)求——RequestQueue 的 add 方法
緩存調(diào)度——CacheDispacher 的 run 方法
網(wǎng)絡(luò)調(diào)度——NetWorkDispacher 的 run 方法
網(wǎng)絡(luò)請(qǐng)求——BasicNetwork 的 performRequest 方法

添加請(qǐng)求

前面說到了 Volley 的入口是創(chuàng)建一個(gè) RequestQueue 隊(duì)列又跛,然后開啟一個(gè)緩存線程和一組網(wǎng)絡(luò)線程煌茬,等待用戶 add 新的 request响牛。那我們現(xiàn)在看一下 add 方法里面持钉,RequestQueue 做了哪些事情帅戒。

 /**
     * Adds a Request to the dispatch queue.
     * @param request The request to service
     * @return The passed-in request
     */
    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;
        }
    }

這是一個(gè)泛型方法膘魄,來看一下他的流程圖

add_flow.png

被添加到緩存隊(duì)列中的 Request 就可以去緩存里面進(jìn)行緩存調(diào)度查找匹配了。先看剛才流程哺呜,RquestQueue舌缤、Cache、mCurrentRequests某残、mWaitingRequests 手撕Volley(一)類圖有介紹国撵,那么疑問來了:

  • Request 是啥
  • cacheKey 是怎么生成的
/**
 * Base class for all network requests.
 *
 * @param <T> The type of parsed response this request expects.
 */
public abstract class Request<T> implements Comparable<Request<T>> {
 public Request(int method, String url, Response.ErrorListener listener) {
        mMethod = method;
        mUrl = url;
        mIdentifier = createIdentifier(method, url);
        mErrorListener = listener;
        setRetryPolicy(new DefaultRetryPolicy());

        mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
    }

首先可以看到 Request 是一個(gè)抽象類,是所有請(qǐng)求的基類玻墅。
來看 Requst 的構(gòu)造器介牙,method、url 分別對(duì)應(yīng) Http 協(xié)議報(bào)文里面的 method 和 url澳厢。HTTP 協(xié)議就不展開說了环础,細(xì)節(jié)還是要看我手撕Volley(一)里面HTTP協(xié)議相關(guān)的的內(nèi)容。

 private static String createIdentifier(final int method, final String url) {
        return InternalUtils.sha1Hash("Request:" + method + ":" + url +
                ":" + System.currentTimeMillis() + ":" + (sCounter++));
    }

而 mIdentifier 是根據(jù)當(dāng)前毫秒和 method 以及 url 計(jì)算的哈希作為唯一標(biāo)識(shí)剩拢。

/** Default tag for {@link TrafficStats}. */
    private final int mDefaultTrafficStatsTag;
 /**
     * @return The hashcode of the URL's host component, or 0 if there is none.
     */
    private static int findDefaultTrafficStatsTag(String url) {
        if (!TextUtils.isEmpty(url)) {
            Uri uri = Uri.parse(url);
            if (uri != null) {
                String host = uri.getHost();
                if (host != null) {
                    return host.hashCode();
                }
            }
        }
        return 0;
    }

mDefaultTrafficStatsTag 是 host (域名)的一個(gè)哈希线得,有啥用暫時(shí)未知。

/** The retry policy for this request. */
    private RetryPolicy mRetryPolicy;
/**
 * Retry policy for a request.
 */
public interface RetryPolicy {

    /**
     * Returns the current timeout (used for logging).
     */
    public int getCurrentTimeout();

    /**
     * Returns the current retry count (used for logging).
     */
    public int getCurrentRetryCount();

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     * @param error The error code of the last attempt.
     * @throws VolleyError In the event that the retry could not be performed (for example if we
     * ran out of attempts), the passed in error is thrown.
     */
    public void retry(VolleyError error) throws VolleyError;
}

RetryPolicy 也是一個(gè)接口徐伐,定義了默認(rèn)超時(shí)時(shí)間以及重連次數(shù)贯钩。他的默認(rèn)實(shí)現(xiàn)是 DefaultRetryPolicy,里面定義了幾個(gè)常量當(dāng)作默認(rèn)實(shí)現(xiàn)办素。

 /** The default socket timeout in milliseconds */
    public static final int DEFAULT_TIMEOUT_MS = 2500;

    /** The default number of retries */
    public static final int DEFAULT_MAX_RETRIES = 0;

    /** The default backoff multiplier */
    public static final float DEFAULT_BACKOFF_MULT = 1f;

最后角雷,附上類圖:


request.png
retry_policy.png

到了這里,add 方法我們就基本理解了性穿,剛才說到勺三,add 方法的最后 request 被添加到 緩存隊(duì)列里面去匹配,那下面就來看緩存隊(duì)列里做了什么

緩存調(diào)度

還記的前面說過 CacheDispacher 繼承了 Thread需曾,是一個(gè)線程類吗坚,他的 run 方法是一個(gè) while true 死循環(huán)祈远,有一個(gè)標(biāo)記位 mQuit 來退出循環(huán)。
先看成員變量

 private static final boolean DEBUG = VolleyLog.DEBUG;

    /** The queue of requests coming in for triage. */
    private final BlockingQueue<Request<?>> mCacheQueue;

    /** The queue of requests going out to the network. */
    private final BlockingQueue<Request<?>> mNetworkQueue;

    /** The cache to read from. */
    private final Cache mCache;

    /** For posting responses. */
    private final ResponseDelivery mDelivery;

    /** Used for telling us to die. */
    private volatile boolean mQuit = false;
CacheDispather.png
  • mCacheQueue 和 mNetworkQueue
    阻塞隊(duì)列
  • ResponseDelivery 接口用來 post response 或者 error
    終于 Volley 的核心代碼之一

  @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();

        Request<?> request;
        while (true) {
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try {
                // Take a request from the queue.
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
            try {
                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.
                    final Request<?> finalRequest = request;
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(finalRequest);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
            }
        }
    }

run方法流程圖如下:

cachedispacher_run.png

我們注意到

  • continue 大量使用
  • log 信息記錄很詳細(xì)
 /**
     * Data and metadata for an entry returned by the cache.
     */
    public static class Entry {
        /** The data returned from cache. */
        public byte[] data;

        /** ETag for cache coherency. */
        public String etag;

        /** Date of this response as reported by the server. */
        public long serverDate;

        /** The last modified date for the requested object. */
        public long lastModified;

        /** TTL for this record. */
        public long ttl;

        /** Soft TTL for this record. */
        public long softTtl;

        /** Immutable response headers as received from server; must be non-null. */
        public Map<String, String> responseHeaders = Collections.emptyMap();

    /** True if the entry is expired. */
        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        /** True if a refresh is needed from the original data source. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

這里再貼一次 Cahce 接口的內(nèi)部類 Entry商源,因?yàn)樗娴奶匾税砗覀兛催B個(gè)判斷過期和需要刷新的方法分別是,兩個(gè)成員變量跟當(dāng)前時(shí)間的對(duì)比炊汹。而 data 是二進(jìn)制數(shù)組,我們都知道在 HTTP 中 start line 和 headers 是明文存儲(chǔ)的逃顶,而 Entity 是沒有規(guī)定的讨便,一般我們都用二進(jìn)制流傳輸,可以減少傳輸流量以政,并且安全霸褒,data 這里就是用來保存 Entity 的。
Cache 的默認(rèn)實(shí)現(xiàn)是 DiskBasedCache盈蛮,

* Cache implementation that caches files directly onto the hard disk in the specified
 * directory. The default disk usage size is 5MB, but is configurable.
 */
public class DiskBasedCache implements Cache {

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

可以看到默認(rèn)大小為5M废菱,但是可以自己配置,內(nèi)部用了一個(gè) LinkedHashMap 來保存 request 的 CacheHeader 抖誉,CacheHeader 是為了存儲(chǔ) Entity 中的 header 和 data 的 size殊轴,我覺得是為了避免存大量的 data 吧。
LinkedHashMap 是為了實(shí)現(xiàn) FIFO 的緩存替換策略袒炉,我們知道旁理,在空間不足時(shí)向 HashMap 中 put 數(shù)據(jù)就需要?jiǎng)h除一些內(nèi)容用來保證最新 put數(shù)據(jù)的成功。

 /**
     * Prunes the cache to fit the amount of bytes specified.
     * @param neededSpace The amount of bytes we are trying to fit into the cache.
     */
    private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }
        if (VolleyLog.DEBUG) {
            VolleyLog.v("Pruning old cache entries.");
        }

        long before = mTotalSize;
        int prunedFiles = 0;
        long startTime = SystemClock.elapsedRealtime();

        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, CacheHeader> entry = iterator.next();
            CacheHeader e = entry.getValue();
            boolean deleted = getFileForKey(e.key).delete();
            if (deleted) {
                mTotalSize -= e.size;
            } else {
               VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                       e.key, getFilenameForKey(e.key));
            }
            iterator.remove();
            prunedFiles++;

            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }

        if (VolleyLog.DEBUG) {
            VolleyLog.v("pruned %d files, %d bytes, %d ms",
                    prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
        }
    }

pruneIfNeeded 方法是在 put 方法的第一行執(zhí)行的我磁,做的就是這件事孽文,Volley 并沒有使用 LRU。而是使用的 FIFO夺艰。DiskBasedCache 剩下的就是一些文件操作了芋哭,就不挨著看了。

DiskBasedCache 更多介紹在這里

網(wǎng)絡(luò)調(diào)度

同緩存調(diào)度郁副,NetworkDispatcher 也是一個(gè) Thread 子類减牺,主要看它的成員變量和 run 方法,說干就干

public class NetworkDispatcher extends Thread {
    /** The queue of requests to service. */
    private final BlockingQueue<Request<?>> mQueue;
    /** The network interface for processing requests. */
    private final Network mNetwork;
    /** The cache to write to. */
    private final Cache mCache;
    /** For posting responses and errors. */
    private final ResponseDelivery mDelivery;
    /** Used for telling us to die. */
    private volatile boolean mQuit = false;

NetworkDispatcher 類圖

NetworkDispatcher .png
  • 一個(gè)阻塞隊(duì)列
  • 一個(gè) NetWork 接口
  • 一個(gè) Cache 接口
  • 一個(gè)結(jié)果分發(fā)器

好了師徒四人湊齊了霞势,可以去取經(jīng)了烹植。開個(gè)玩笑,以上四種類型前面手撕Volley(一)介紹愕贡,忘記的可以去前面查草雕,這里就不再介紹了。再看 run 方法固以。

 @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Request<?> request;
        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            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);
            }
        }
    }

長的一匹墩虹,跟緩存調(diào)度很像嘱巾,流程圖如下:


networkdispatcher.png

Network的具體的實(shí)現(xiàn)之前已經(jīng)分析是BasicNetwork,先看成員變量

/**
 * A network performing Volley requests over an {@link HttpStack}.
 */
public class BasicNetwork implements Network {
    protected static final boolean DEBUG = VolleyLog.DEBUG;

    private static int SLOW_REQUEST_THRESHOLD_MS = 3000;

    private static int DEFAULT_POOL_SIZE = 4096;

    protected final HttpStack mHttpStack;

    protected final ByteArrayPool mPool;
  • 兩個(gè)常量诫钓,分別表示最長請(qǐng)求時(shí)間和線程池大小
  • 一個(gè)HttpStack 接口旬昭,真正執(zhí)行網(wǎng)絡(luò)請(qǐng)求的類
  • 二進(jìn)制數(shù)組池,一個(gè)工具類

類圖如下

network.png

再看實(shí)現(xiàn)的 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);
                }
                
                // Handle moved resources
                if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                    String newUrl = responseHeaders.get("Location");
                    request.setRedirectUrl(newUrl);
                }

                // 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 = 0;
                NetworkResponse networkResponse = null;
                if (httpResponse != null) {
                    statusCode = httpResponse.getStatusLine().getStatusCode();
                } else {
                    throw new NoConnectionError(e);
                }
                if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
                        statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                    VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
                } else {
                    VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
                }
                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 == HttpStatus.SC_MOVED_PERMANENTLY || 
                                statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                        attemptRetryOnException("redirect",
                                request, new RedirectError(networkResponse));
                    } else {
                        // TODO: Only throw ServerError for 5xx status codes.
                        throw new ServerError(networkResponse);
                    }
                } else {
                    throw new NetworkError(e);
                }
            }
        }
    }

流程圖如下:


network_performrequest

最終的網(wǎng)絡(luò)執(zhí)行還是在HTTPStack中,待續(xù)菌湃。问拘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惧所,隨后出現(xiàn)的幾起案子骤坐,更是在濱河造成了極大的恐慌,老刑警劉巖下愈,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纽绍,死亡現(xiàn)場離奇詭異,居然都是意外死亡势似,警方通過查閱死者的電腦和手機(jī)拌夏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來履因,“玉大人障簿,你說我怎么就攤上這事≌て” “怎么了卷谈?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長霞篡。 經(jīng)常有香客問我世蔗,道長,這世上最難降的妖魔是什么朗兵? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任污淋,我火速辦了婚禮,結(jié)果婚禮上余掖,老公的妹妹穿的比我還像新娘寸爆。我一直安慰自己,他們只是感情好盐欺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布赁豆。 她就那樣靜靜地躺著,像睡著了一般冗美。 火紅的嫁衣襯著肌膚如雪魔种。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天粉洼,我揣著相機(jī)與錄音节预,去河邊找鬼叶摄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛安拟,可吹牛的內(nèi)容都是我干的蛤吓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼糠赦,長吁一口氣:“原來是場噩夢啊……” “哼会傲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拙泽,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤唆铐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奔滑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顺少,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年朋其,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脆炎。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梅猿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秒裕,到底是詐尸還是另有隱情袱蚓,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布几蜻,位于F島的核電站喇潘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏梭稚。R本人自食惡果不足惜颖低,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弧烤。 院中可真熱鬧忱屑,春花似錦、人聲如沸暇昂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽急波。三九已至从铲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澄暮,已是汗流浹背食店。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工渣淤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吉嫩。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓价认,卻偏偏與公主長得像,于是被迫代替她去往敵國和親自娩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子用踩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Volley源碼分析之流程和緩存 前言 Android一開始提供了HttpURLConnection和HttpCl...
    大寫ls閱讀 619評(píng)論 0 6
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)忙迁,斷路器脐彩,智...
    卡卡羅2017閱讀 134,652評(píng)論 18 139
  • 前言 在現(xiàn)在的Android開發(fā)之中,已經(jīng)比較少人使用volley進(jìn)行網(wǎng)絡(luò)請(qǐng)求了姊扔,之所以現(xiàn)在還寫這篇關(guān)于Volle...
    Linear_Li閱讀 4,096評(píng)論 0 6
  • Android 淺析 Volley (二) 原理 前言 Linus Benedict Torvalds : RTF...
    CodePlayer_Jz閱讀 1,985評(píng)論 0 4
  • 亦舒曾在她的作品《紅到幾時(shí)》中說“一個(gè)女子惠奸,必須先憑雙手爭取生活,才有資格追求快樂恰梢,幸福和理想”佛南。這個(gè)...
    易小喜96閱讀 563評(píng)論 0 4