開源項目學習之Volley(一)

一. 重要的類

1.1 RequestQueue

重要變量

    
    //請求等待集合,隊列中的請求是重復的娜汁,之前已經有一個相同的請求正在執(zhí)行
    private final Map<String, Queue<Request<?>>> mWaitingRequests =
            new HashMap<>();

    private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

    //緩存隊列
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
            new PriorityBlockingQueue<>();

    //網絡請求隊列
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
            new PriorityBlockingQueue<>();
            
    //默認使用的是DiskBasedCache
    private final Cache mCache;

    /** 默認使用BasicNetwork */
    private final Network mNetwork;

    /** 默認使用ExecutorDelivery */
    private final ResponseDelivery mDelivery;

    //網絡請求線程數組
    private final NetworkDispatcher[] mDispatchers;

    //緩存線程
    private CacheDispatcher mCacheDispatcher;

重要方法

  • add(), 可以將請求放入隊列中
  • start(), RequestQueue還負責啟動cache線程和network線程
  • stop(), 停止緩存線程和網絡線程
  • finish(), 請求結束后的回調

1.2 Request

抽象了Http請求饿自, Request本身是一個抽象類撩银,繼承它的子類必須要實現兩個抽象方法:

  • abstract protected Response<T> parseNetworkResponse(NetworkResponse response) - 將http請求返回的響應解析成合適的類型
  • abstract protected void deliverResponse(T response) - 將解析好的響應傳遞給監(jiān)聽器

每一個Request的緩存key就是它的Url, 如果要創(chuàng)建POST或者PUT請求滥沫, 也可以重寫以下兩個方法:

  • public byte[] getBody() throws AuthFailureError {}
  • public Map<String, String> getParams() throws AuthFailureError

getBody()方法默認也是通過getParams方法來創(chuàng)建Body內容尺棋,而getParams方法默認實現是直接返回null缸夹,所以一般構建POSTPUT請求應當重寫這兩個方法中的一個, 一般直接重寫getParams即可

子類

  • StringRequest, 返回值為String類型
  • JsonRequest, 代表了Body為json的請求, 本身也是一個抽象類
  • JsonObjectRequest, 繼承了JsonRequest匀油, 將返回值解析為JSONObject
  • JsonArrayRequest, 繼承了JsonRequest, 將返回值解析為JsonArrayRequest
  • ImageRequest, 圖片請求,將返回值解析成為Bitmap

1.3 HttpStack

HttpStack是一個接口腥椒,就定義了一個執(zhí)行方網絡請求的方法performRequest, 它有兩個實現類HurlStackHttpClientStack

HurlStack

HurlStackperformRequest主要是用HttpUrlConnection來實現網絡請求阿宅,這也是Android官方目前推薦使用的方式

HttpClientStack

HttpClientStack是使用Apache的HttpClient來進行網絡請求,這一方式目前目前不被Android官方推薦笼蛛,HttpClient也已從Android源碼中移除

1.4 BasicNetwork

BasicNetwork實現了Network接口洒放,其內部含有HttpStack
引用,其performRequest主要工作是解析請求的Header滨砍,調用HttpStack.performRequest來真正執(zhí)行網絡請求往湿, 之后判斷http響應碼(主要是判斷304,跟緩存相關)惋戏,生成NetworkResponse

1.5 DiskBasedCache

磁盤緩存類领追,實現了Cache接口

重要方法

  • public synchronized void initialize(),初始化緩存,掃面緩存目錄(默認為/data/data/pkg_name/files/cache/volley)得到緩存數據生成緩存header并放入內存
  • public synchronized Entry get(String key), 從緩存中獲取緩存header,然后讀取緩存文件
  • public synchronized void put(String key, Entry entry), 先檢查如果給當前Entry分配空間以后响逢, 緩存是否會滿绒窑,如果會滿則遍歷并逐個刪除緩存,直到如果為當前Entry分配空間以后舔亭,緩存量小于最大緩存量的0.9些膨, 然后再新建緩存文件

二. 基本流程

2.1 Volley.newRequestQueue

    
    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        //創(chuàng)建緩存目錄蟀俊,默認在/data/data/pkg_name/files/cache/volley
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                //使用HttpUrlConnection的方式進行網絡請求
                stack = new HurlStack();
            } else {
                //使用HttpClient進行http請求
                ...
            }
        }
        //使用默認的BasicNetwork
        Network network = new BasicNetwork(stack);
        //創(chuàng)建請求隊列, 默認使用ExecutorDelivery
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        //啟動CacheDispatcher和NetworkDispatcher線程,開始接收請求,[2]
        queue.start();

        return queue;
    }

  1. 創(chuàng)建緩存目錄, 默認為/data/data/pkg_name/files/cache/volley
  2. 創(chuàng)建對應的HttpStack
  3. 創(chuàng)建BasciNetwork
  4. 創(chuàng)建DiskBasedCache
  5. 創(chuàng)建RequestQueue, 并啟動

2.2 RequestQueue.start


    public void start() {
        //停止所有緩存線程和網絡請求線程, 
        stop();
        //創(chuàng)建緩存線程订雾,并啟動線程. [3]
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        //創(chuàng)建網絡請求線程數組中所有的網絡請求線程肢预,并啟動, [4]
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

  1. mCacheQueuePriorityBlockingQueue<Request<?>>類型
  2. mNetworkQueue也是PriorityBlockingQueue<Request<?>>類型
  3. CacheDispatcherNetworkDispatcher都是繼承自Thread即一個線程類

2.3 CacheDispatcher.run

    
    @Override
    public void run() {
        //設置緩存線程的優(yōu)先級
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // 初始化DiskBasedCache, [2.3.1]
        mCache.initialize();

        while (true) {
            try {
                /* 從緩存隊列中讀取Request, 如果隊列中沒有請求,則線程阻塞*/
                final Request<?> request = mCacheQueue.take();
                request.addMarker("cache-queue-take");

                /* 如果請求已經被取消葬燎,則直接結束當前請求, 從當前請求集合以及等待請求列表中刪除*/
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                /* 判斷當前請求有沒有緩存误甚, request的cacheKey就是url*/
                Cache.Entry entry = mCache.get(request.getCacheKey());
                /* 沒有緩存結果缚甩,則直接交友網絡請求線程*/
                if (entry == null) {
                    request.addMarker("cache-miss");
                    mNetworkQueue.put(request);
                    continue;
                }

                /* 如果有緩存結果谱净,但緩存結果已經失效,同樣交由網絡線程*/
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                request.addMarker("cache-hit");
                /* 緩存命中擅威,則調用Request的parseNetworkResponse獲取相應的Response*/
                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;
                    
                     /* 向網絡請求隊列中添加請求*/
                    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;
                }
            }
        }
    }

  1. 初始化緩存李请,獲取緩存內容
  2. 開啟無限循環(huán),從PriorityBlockingQueue中取出請求厉熟,查看請求是否有緩存(請求的key就是url)导盅,如果隊列中沒有請求,則線程阻塞
  3. 如果沒有緩存命中揍瑟,則將請求放入網絡隊列白翻,去執(zhí)行網絡請求
  4. 如果有緩存命中,檢查緩存是否失效绢片,如果失效也放入網絡隊列滤馍,去執(zhí)行網絡請求
  5. 如果緩存命中且有效,調用Request.parseNetworkResponse獲取相應的Response
  6. 判斷緩存需不要刷新底循,如果不需要刷新巢株,則直接調用ExceutorDelivery.postResponse傳遞Response;如果緩存還需要刷新,則還是將請求放入網絡請求隊列熙涤,去執(zhí)行網絡請求獲取最新結果

2.3.1 DiskBasedCache.initialize

    @Override
    public synchronized void initialize() {
        /* 如果緩存目錄不存在阁苞,則創(chuàng)建緩存目錄 */
        if (!mRootDirectory.exists()) {
            if (!mRootDirectory.mkdirs()) {
                VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
            }
            return;
        }
        /* 獲取緩存目錄下所有文件*/
        File[] files = mRootDirectory.listFiles();
        if (files == null) {
            return;
        }
        /* 遍歷多有緩存文件,如果能夠正常讀取祠挫,則為每個緩存文件生成一個CacheHeader
        * 并放入集合中*/
        for (File file : files) {
            BufferedInputStream fis = null;
            try {
                fis = new BufferedInputStream(new FileInputStream(file));
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);
            } catch (IOException e) {
                /* 如果發(fā)生異常猬错,證明該文件不符合Volley的緩存格式
                * 刪除該文件*/
                if (file != null) {
                   file.delete();
                }
            } finally {
                try {
                    if (fis != null) {
                        fis.close();
                    }
                } catch (IOException ignored) { }
            }
        }
    }

2.4 NetworkDispatcher.run


    @Override
    public void run() {
        //設置該網絡線程的優(yōu)先級
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            Request<?> request;
            try {
                // 從網絡請求隊列中獲取請求
                request = mQueue.take();
            } catch (InterruptedException e) {
                ...
                continue;
            }

            try {
                ...
                /* 如果請求已經被取消,則直接結束當前請求, 從當前請求集合以及等待請求列表中刪除*/
                if (request.isCanceled()) {
                    request.finish("network-discard-cancelled");
                    continue;
                }
                /* 設置網絡流量標識*/
                addTrafficStatsTag(request);

                /* 執(zhí)行網絡請求, 并獲取對應的Response [2.4.1]*/ 
                NetworkResponse networkResponse = mNetwork.performRequest(request);
                request.addMarker("network-http-complete");

                /* 如果服務端返回304同時我們已經傳輸了Response, 則結束該Request避免重復傳輸Response*/
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                    request.finish("not-modified");
                    continue;
                }

                /* 調用Request的parseNetworkResponse解析結果*/
                Response<?> response = request.parseNetworkResponse(networkResponse);
                request.addMarker("network-parse-complete");

                /* 如果Request允許緩存且緩存實體不為空茸歧,則調用DiskBasedCache進行緩存 */
                if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

                /* 標記Request已傳輸結果 */
                request.markDelivered();
                /* ExecutorDelivery 傳輸結果 */
                mDelivery.postResponse(request, response);
            } catch (VolleyError volleyError) {
                ... //分發(fā)VolleyError
            } catch (Exception e) {
                ... //分發(fā)VolleyError
            }
        }
    }


  1. 從網絡請求隊列中取出請求倦炒,如果沒有請求則線程會一直阻塞
  2. 調用BasicNetwork.performRequest執(zhí)行網絡請求,并獲取相應的Response
  3. 如果Request允許緩存且緩存實體不為空软瞎,則將Request放入緩存
  4. 調用ExecutorDelivery.postResponse傳輸結果

2.4.1 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<>();
                //如果請求中包含緩存字段逢唤,存儲進headers
                addCacheHeaders(headers, request.getCacheEntry());
                //調用HttpStack.performRequest, 一般情況下拉讯,HttpStack都會是HurlStack
                //[2.4.2]
                httpResponse = mHttpStack.performRequest(request, headers);

                StatusLine statusLine = httpResponse.getStatusLine();
                int statusCode = statusLine.getStatusCode();
                //解析Header
                responseHeaders = convertHeaders(httpResponse.getAllHeaders());
                // 返回碼是304
                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);
                    }

                    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 (statusCode < 200 || statusCode > 299) {
                    throw new IOException();
                }
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false,SystemClock.elapsedRealtime() - requestStart);
            } catch (SocketTimeoutException e) {
                //[2.4.3]
                attempRetryonExcption("socket", request, new TimeoutError());
            } catch .... {//其他excption
                .... //基本都是attempRetryonExcption
            }
        }
    }

2.4.2 HurlStack.performRequest

由于目前Android平臺基本都是使用HttpUrlConnection, 所以這里就默認使用HurlStack, 不考慮HttpClientStack

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError {
        //設置Header
        String url = request.getUrl();
        HashMap<String, String> map = new HashMap<>();
        map.putAll(request.getHeaders());
        //additionalHeader一般都是用來緩存的Header
        map.putAll(additionalHeaders);
        if (mUrlRewriter != null) {
            String rewritten = mUrlRewriter.rewriteUrl(url);
            if (rewritten == null) {
                throw new IOException("URL blocked by rewriter: " + url);
            }
            url = rewritten;
        }
        URL parsedUrl = new URL(url);
        //建立Http連接[2.4.2.1]
        HttpURLConnection connection = openConnection(parsedUrl, request);
        //設置請求頭
        for (String headerName : map.keySet()) {
            connection.addRequestProperty(headerName, map.get(headerName));
        }
        //設置請求方法,請求參數等
        setConnectionParametersForRequest(connection, request);
        // Initialize HttpResponse with data from the HttpURLConnection.
        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if (responseCode == -1) {
           ...
        }
        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                connection.getResponseCode(), connection.getResponseMessage());
        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
        if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
            response.setEntity(entityFromConnection(connection));
        }
        for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
            if (header.getKey() != null) {
                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                response.addHeader(h);
            }
        }
        return response;
    }
2.4.2.1 HurlStack.openConnection

    private HttpURLConnection openConnection(URL url, Request<?> request) throws            IOException {
        HttpURLConnection connection = createConnection(url);

        int timeoutMs = request.getTimeoutMs();
        connection.setConnectTimeout(timeoutMs);
        connection.setReadTimeout(timeoutMs);
        connection.setUseCaches(false);
        connection.setDoInput(true);

        // use caller-provided custom SslSocketFactory, if any, for HTTPS
        // 如果scheme是https鳖藕,設置SSLSocketFactory
        if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
            ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
        }

        return connection;
    }

2.4.3 BasicNetwork.attemptRetryOnException


    private static void attemptRetryOnException(String logPrefix, Request<?> request,
            VolleyError exception) throws VolleyError {
        //默認是DefaultRetryPolicy
        RetryPolicy retryPolicy = request.getRetryPolicy();
        int oldTimeout = request.getTimeoutMs();

        try {
            //DefaultRetryPolicy中的retry主要就是判斷重試次數是否已經到達上限
            retryPolicy.retry(exception);
        } catch (VolleyError e) {
            request.addMarker(
                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
            throw e;
        }
        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
    }


2.5. ExecutorDelivery.postResponse

    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        //向線程池中提交任務
        //[2.5.1]
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }

2.5.1 ResponseDeliveryRunnable.run()


        @Override
        public void run() {
            if (mRequest.isCanceled()) {
                mRequest.finish("canceled-at-delivery");
                return;
            }

            // Deliver a normal response or error, depending.
            if (mResponse.isSuccess()) {
                //如果成功會回調Response.Listener的onResponse
                mRequest.deliverResponse(mResponse.result);
            } else {
                //如果失敗會回調Response.ErrorListener的onErrorResponse
                mRequest.deliverError(mResponse.error);
            }

            if (mResponse.intermediate) {
                //如果這是一個intermediate Response魔慷,添加Marker
                mRequest.addMarker("intermediate-response");
            } else {
                //結束當前請求
                mRequest.finish("done");
            }

            // If we have been provided a post-delivery runnable, run it.
            if (mRunnable != null) {
                mRunnable.run();
            }
       }

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市著恩,隨后出現的幾起案子院尔,更是在濱河造成了極大的恐慌,老刑警劉巖喉誊,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邀摆,死亡現場離奇詭異,居然都是意外死亡伍茄,警方通過查閱死者的電腦和手機栋盹,發(fā)現死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敷矫,“玉大人例获,你說我怎么就攤上這事〔苷蹋” “怎么了榨汤?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長怎茫。 經常有香客問我收壕,道長,這世上最難降的妖魔是什么遭居? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任啼器,我火速辦了婚禮,結果婚禮上俱萍,老公的妹妹穿的比我還像新娘端壳。我一直安慰自己,他們只是感情好枪蘑,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布损谦。 她就那樣靜靜地躺著,像睡著了一般岳颇。 火紅的嫁衣襯著肌膚如雪照捡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天话侧,我揣著相機與錄音栗精,去河邊找鬼。 笑死,一個胖子當著我的面吹牛悲立,可吹牛的內容都是我干的鹿寨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼薪夕,長吁一口氣:“原來是場噩夢啊……” “哼脚草!你這毒婦竟也來了?” 一聲冷哼從身側響起原献,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤馏慨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后姑隅,有當地人在樹林里發(fā)現了一具尸體写隶,經...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年粤策,在試婚紗的時候發(fā)現自己被綠了樟澜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片误窖。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡叮盘,死狀恐怖,靈堂內的尸體忽然破棺而出霹俺,到底是詐尸還是另有隱情柔吼,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布丙唧,位于F島的核電站愈魏,受9級特大地震影響,放射性物質發(fā)生泄漏想际。R本人自食惡果不足惜培漏,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胡本。 院中可真熱鬧牌柄,春花似錦、人聲如沸侧甫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽披粟。三九已至咒锻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間守屉,已是汗流浹背惑艇。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拇泛,地道東北人滨巴。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓须板,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兢卵。 傳聞我的和親對象是個殘疾皇子习瑰,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內容