1. 程式人生 > >Volley原始碼解析(一)——傳送請求與結束請求

Volley原始碼解析(一)——傳送請求與結束請求

Volley是一個Android HTTP庫,只支援非同步方式。

傳送請求樣例

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>() { @Override public void onResponse(String response) { // Display the first 500 characters of the response string. mTextView.setText("Response is: "+ response.substring(0,500)); } }, new Response.ErrorListener() { @Override
public void onErrorResponse(VolleyError error) { mTextView.setText("That didn't work!"); } }); // Add the request to the RequestQueue. queue.add(stringRequest);

傳送請求原始碼分析

一個請求的生命週期如下圖:
Request生命週期
從上圖可以看出,在Volley中會有三個執行緒:UI執行緒負責發請求和收響應;快取分發器;網路分發器。
Volley類中只負責一件事情,就是建立一個RequestQueue物件,用於存放請求,該物件最好是單例的,供整個APP使用。
其具體實現如下:

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        //建立快取目錄
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

        String userAgent = "volley/0";
        //更新userAgent欄位
        try {
            String packageName = context.getPackageName();
            PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
            userAgent = packageName + "/" + info.versionCode;
        } catch (NameNotFoundException e) {
        }

        //對HttpStack賦值
        if (stack == null) {
            //如果SDK大於8,使用HurlStack,否則使用HttpClientStack
            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物件
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();

        return queue;
    }

RequestQueue的構造方法中有兩個引數,一個是Cache,負責將響應儲存到磁碟,一個是Network,負責執行HTTP操作。
下面先看一下RequestQueue的內部定義,RequestQueue內部主要有四個欄位是在初始化的時候指定的,分別是:

    private final Cache mCache;

    private final Network mNetwork;

    /** 分發HTTP響應 */
    private final ResponseDelivery mDelivery;

    /** 網路分發器 */
    private NetworkDispatcher[] mDispatchers;

RequestQueue的構造方法一共有三個,但最終都會呼叫下面的這個構造器,如下:

 public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }

RequesteQueue#start()

當建立好RequestQueue之後,呼叫了start()方法,start()方法如下:

    public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // 建立快取分發器,然後啟動
        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();
        }
    }

可以看到RequestQueue的start方法就是啟動其內部的分發器,主要包括一個快取分發器和多個網路分發器,網路分發器的數量實在構造方法中設定的,預設為4個。
前面的例子中,當建立好Request和RequestQueue之後,就將Request放進RequestQueue中就可以了。

RequestQueue#add()方法

add()方法實現如下:

public <T> Request<T> add(Request<T> request) {
        //Request和RequestQueue關聯
        request.setRequestQueue(this);
        //將請求加入到Set中
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }

        // Process requests in the order they are added.
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");

        // 如果這個請求不應該被快取,那麼直接新增進網路佇列中
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            //如果該請求已經在執行了
            if (mWaitingRequests.containsKey(cacheKey)) {
                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 {
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

這裡面涉及到RequestQueue中的多個個集合,定義分別如下:

    //如果當前請求已經在執行了,那麼將會加入到該集合中
    private final Map<String, Queue<Request<?>>> mWaitingRequests =
            new HashMap<String, Queue<Request<?>>>();

    //當前正在執行的請求
    private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

    //快取請求優先順序佇列
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
        new PriorityBlockingQueue<Request<?>>();

    //網路請求優先順序佇列
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
        new PriorityBlockingQueue<Request<?>>();

可以看到,add()方法主要做的就是根據不同情況將請求加入到不同的佇列中。由於快取請求佇列和網路請求佇列都是使用的優先順序佇列,所以可以給Request設定優先順序。
1. 如果請求不應該從快取中得到,那麼直接加入到網路請求佇列中;
2. 如果請求已經在執行了,那麼將其加入到正在執行的一個HashMap中,其中key是請求,值是一個請求的佇列;
3. 如果請求沒有在執行,那麼將其加入到快取佇列中。

在這裡,我們已經將一個請求提交了,下面看一下,Request的快取鍵值是如何得到的。

Request#getCacheKey()

public String getCacheKey() {
        return getUrl();
    }
public String getUrl() {
        return mUrl;
    }

可以看到一個Request的鍵值就是其URL。
現在考慮一個問題:建立了同一個相同URL的兩個Request,或者說同一個Request新增到RequestQueue兩次,情況是怎麼樣的呢?下面分別分析:

RequestQueue新增同一相同URL的兩個Request物件

設為Request1和Request2,新增時,由於Request沒有重寫equals方法和hashCode方法,所以mCurrentRequests會人為這是兩個不同的請求,都新增進Set,然後比如說Request1首先獲得了mWaitingRequests的鎖,由於mWaitingRequests中還沒有該URL,所以被新增進mWaitingRequests和放到了快取佇列中,然後當Request2再獲取到mWaitingRequests時候,由於已經有了URL,所以會在mWaitingRequests中建立一個連結串列並把該請求放入連結串列中,從而可以看出同一時刻相同URL的請求只會被執行一次。不過具體放在連結串列中的請求在Request1被處理之後是如何處理的,下面一篇部落格會分析到。

Request新增同一相同的Request兩次

經過前面的分析,可以知道,在mCurrentRequests中後一個Request會代替前一個Request,而後一個Request會被放入mWaitingRequests的連結串列中。可以發現同一個Request物件即在快取佇列中,又在待處理的佇列中。

下面就Request被執行完之後,看是怎樣操作的來解釋上面兩個問題。

結束請求原始碼分析

當想取消一個Request的執行時,可以呼叫RequestQueue的finish()來主要取消執行,也可以在Request被正常執行完之後自己呼叫finish()方法,下面先從Request的finish()方法看起,其實現如下:

Request#finish()

 void finish(final String tag) {
        if (mRequestQueue != null) {
            mRequestQueue.finish(this);
        }
        if (MarkerLog.ENABLED) {
            final long threadId = Thread.currentThread().getId();
            if (Looper.myLooper() != Looper.getMainLooper()) {
                // If we finish marking off of the main thread, we need to
                // actually do it on the main thread to ensure correct ordering.
                Handler mainThread = new Handler(Looper.getMainLooper());
                mainThread.post(new Runnable() {
                    @Override
                    public void run() {
                        mEventLog.add(tag, threadId);
                        mEventLog.finish(this.toString());
                    }
                });
                return;
            }

            mEventLog.add(tag, threadId);
            mEventLog.finish(this.toString());
        } else {
            long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
            if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
                VolleyLog.d("%d ms: %s", requestTime, this.toString());
            }
        }
    }

可以看到,首先也是呼叫了RequestQueue的finish()方法,下面再來分析RequestQueue的finish()方法。

RequestQueue#finish()

void finish(Request<?> request) {
        // 首先從當前執行集合中刪除
        synchronized (mCurrentRequests) {
            mCurrentRequests.remove(request);
        }

        //如果請求應該被快取
        if (request.shouldCache()) {
            synchronized (mWaitingRequests) {
                //得到請求的鍵值,即URL
                String cacheKey = request.getCacheKey();
                //得到與URL關聯的等待佇列
                Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
                //如果佇列不為null
                if (waitingRequests != null) {
                    //將所有請求都加入到快取佇列中
                    mCacheQueue.addAll(waitingRequests);
                }
            }
        }
    }

這兒,可以看到,首先是從Set集合中刪除當前請求,然後判斷請求是否應該被快取,這和add()方法裡面是相一致的,add()方法裡只有請求允許使用快取,才會被加入到URL關聯的等待佇列中。然後就是將與URL關聯的請求都加入到快取佇列中。下面就上面兩個問題再次給出解釋。
1. RequestQueue新增同一相同URL的兩個Request物件:對於這種情況,Request1被執行,Request2被存在等待佇列中,當Request1執行完成後,將會從URL關聯的佇列中得到Request2物件,然後將Request2加入到快取佇列,由於Request1之前已經有快取結果了,所以執行Request2時只需要經過CacheDispatcher就可以得到結果然後finish了,可以發現這種情況下,同一個URL的只會進行一次網路請求,其餘的都是走快取請求;不過這是針對於可以快取響應的情況,如果不能快取響應,那麼都會直接加入到網路請求中執行兩次網路操作。
2. Request新增同一相同的Request兩次:當執行了第一次之後,就從Set中成功移除了Request,然後再從等待佇列中取出,加入到快取佇列,可以當發現這一次的Request依然會走CacheDispatcher中一趟。

至此,我們將一個請求提交給了RequestQueue,那麼RequestQueue是如何執行請求,又是如何將響應交付給UI執行緒處理呢?這一部分,我們下一篇再講。下一篇文章Volley原始碼解析(二)——CacheDispatcher將會介紹CacheDispatcher是如何進行快取分發的。

相關推薦

Volley原始碼解析——傳送請求結束請求

Volley是一個Android HTTP庫,只支援非同步方式。 傳送請求樣例 final TextView mTextView = (TextView) findViewById(R.id.text); ... // Instantiate

Dubbo原始碼解析請求排程器 Dispatcher

排程器 Dispatcher 排程策略 all 所有訊息都派發到執行緒池,包括請求,響應,連線事件,斷開事件,心跳等。 direct 所有訊息都不派發到執行緒池,全部在 IO 執行緒上直接執行。 message 只有請求響應訊息派發到執行緒池,其它連線斷開

Android Volley解析之GET、POST請求

一、 Volley 的地位 自2013年Google I/O 大會上,Google 推出 Volley 之後,一直到至今,由於其使用簡單、程式碼輕量、通訊速度快、併發量大等特點,倍受開發者們的青睞。 先看兩張圖,讓圖片告訴我們 Volley 的用處; 第一

Spring原始碼解析——元件註冊1

一、@Configuration&@Bean給容器中註冊元件 public class Person { private String name; private Integer age; public Person() { } public

認真的 Netty 原始碼解析

本文又是一篇原始碼分析文章,其實除了 Doug Lea 的併發包原始碼,我是不太愛寫原始碼分析的。 本文將介紹 Netty,Java 平臺上使用最廣泛的 NIO 包,它是對 JDK 中的 NIO 實現的一層封裝,讓我們能更方便地開發 NIO 程式。其實,Netty 不僅僅是 NIO 吧,但是,基本上大家

EventBus原始碼解析—訂閱過程

1.EventBus原始碼解析(一)—訂閱過程 2.EventBus原始碼解析(二)—釋出事件和登出流程 前言 最近發現EventBus用起來是真的方便,本來對於EventBus我對於這個框架的原始碼的閱讀的優先順序是比較低的,因為這個框架不像OkHttp,Gli

【MapReduce詳解及原始碼解析】——分片輸入、Mapper及Map端Shuffle過程

title: 【MapReduce詳解及原始碼解析(一)】——分片輸入、Mapper及Map端Shuffle過程 date: 2018-12-03 21:12:42 tags: Hadoop categories: 大資料 toc: true 點選檢視我的部落格:Josonlee’

Redis5.0原始碼解析----------簡單動態字串SDS

基於Redis5.0 Redis 沒有直接使用 C 語言傳統的字串表示(以空字元結尾的字元陣列,以下簡稱 C 字串), 而是自己構建了一種名為簡單動態字串(simple dynamic string,SDS)的抽象型別, 並將 SDS 用作 Redis 的預設字串表示。

OkHttp原始碼解析

簡單使用 OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); Request request = new Request.Builder() .url("www.bai

OKHttp 3.10原始碼解析:執行緒池和任務佇列

OKhttp是Android端最火熱的網路請求框架之一,它以高效的優點贏得了廣大開發者的喜愛,下面是OKhttp的主要特點: 1.支援HTTPS/HTTP2/WebSocket 2.內部維護執行緒池佇列,提高併發訪問的效率 3.內部維護連線池,支援多路複用,減少連線建立開銷 4.

SLF4J原始碼解析

提出問題 閱讀原始碼之前,首先提幾個問題 SLF4J是如何整合不同的日誌框架的 Class Path中為什麼只能有且僅有一種日誌框架的binding 這段文字摘錄自官網:In your code, in addition to slf4j-api-1.8.0-beta2.jar, y

Java容器——HashMapJava8原始碼解析

一 概述 HashMap是最常用的Java資料結構之一,是一個具有常數級別的存取速率的高效容器。相對於List,Set等,結構相對複雜,本篇我們先對HashMap的做一個基本說明,對組成元素和構造方法進行介紹。 二 繼承關係 首先看HashMap的繼承關係,比較簡單,實現了Map和序列化

jQuery深入之原始碼解析

總體架構 可以看出來jQuery主要有三個模組: 入口模組、功能模組、底層支援模組。 - 入口模組 在構造jQuery物件模組中,如果在呼叫建構函式建立jQuery物件時,會呼叫選擇器

python 原始碼解析

為了看懂 python 原始碼 ,特地學了 c++ ,依然看不懂,看了個大概,先留個坑,慢慢填。 先從 python dict 物件開始看起。 python dict 物件  是鍵值對的 一種結構,類似於 java 的hashmap 物件。 dictd 物件 , 每個鍵值對 ,

ElasticSearch原始碼解析:轉篇介紹中文分詞的文章

轉自:http://www.cnblogs.com/flish/archive/2011/08/08/2131031.html  基於CRF(Conditional Random Field)分詞演算法 論文連結:http://nlp.stanford.edu/pubs/

ActiveMQ原始碼解析:聊聊broker

一、Broker     訊息佇列核心,相當於一個控制中心,負責路由訊息、儲存訂閱和連線、訊息確認和控制事務 1.Broker介面     定義了一些獲取broker本身相關資訊,新增connection、destination、session、訊息生產者、控制事務

StringBuffer和StringBuilder原始碼解析--構造方法

前幾天接到阿里巴巴的電話面試,被虐的一塌糊塗。當時問我列印字串可以用這種方式System.out.println(“a” + “b” + “c”),但是一般我們不用這種方式,而要用StringBuff

jdk原始碼解析

1、先說一下原始碼解析的過程:JDK-->JRE-->JVM(以openJDK代替)注意:這裡要了解jdk和jre和jvm他們分別是什麼?以及他們的關係才可以繼續。這裡先上一章從網上下載的關係圖方便理解 2、筆者本地的jdk是oraclejdk,jvm所

Spark原始碼解析

RDD之getNarrowAncestors內部方法分析 最近開始spark的原始碼攻關,其實看原始碼一直是我最怕的東西,因為太多、太雜、太深導致不能夠很好的把我脈絡導致每次最後都放棄。有人跟我說看原

Spring基於註解形式的 AOP的原理流程及原始碼解析

在Spring的配置類上添加註解@EnableAspectJAutoProxy: @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) public class MvcContextCo