文档章节

Volley设计与实现分析

WolfCS
 WolfCS
发布于 2016/05/30 23:57
字数 3433
阅读 132
收藏 2

Volley设计与实现分析


我们平时在开发Android应用的时候,不可避免地经常要通过网络来进行数据的收发,而多数情况下都是会用HTTP协议来做这些事情。Android系统主要提供了HttpURLConnection和Apache HttpClient这两种方式来帮我们进行HTTP通信。对于这两种方式,Google官方的一份文档 Android’s HTTP Clients 有做一个对比说明。是说,Apache HttpClient提供的API非常多,实现稳定,bug也比较少,正因如此,为了保持API兼容性而非常难以做优化。HttpURLConnection的API比较少,故而比较容易做优化。但在Android 2.3之前,HttpURLConnection的实现又有一些比较严重的问题。Google官方建议在2.2及之前的Android上,用Apache HttpClient来执行HTTP请求,在2.3及之后的Android上,则用HttpURLConnection接口。

另外,HttpURLConnection和HttpClient的用法还是有些复杂的,提供的功能也比较基础,如果不进行适当封装的话,很容易写出大量重复代码。于是乎,一些Android网络通信框架也就应运而生,比如说AsyncHttpClient等,它把HTTP所有的通信细节全部封装在内部,同时提供更为强大的API,我们只需简单调用几行代码就可以完成通信操作。

Volley是Google提供的一个HTTP网络库,其功能大体是提供对通信细节的封装,以方便网络操作的调用,volley在内部实现中,会根据运行的android的版本,来决定是使用HttpURLConnection和Apache HttpClient接口;提供缓存机制,以加速网络访问;提供HTTP请求异步执行的能力。这里我们就来看一下Volley的设计和实现。

Volley的获取

我们先来了解一些怎么下载到volley的代码。我们可以通过如下的命令,下载的volley的代码:

git clone https://android.googlesource.com/platform/frameworks/volley

下载了volley之后,将代码导入到Android Studio中,根据volley工程的配置对于工具版本的要求,下载必要的工具,比如Android SDK platform,SDK Build tools,Gradle插件,或者根据本地工具链的版本,适当修改volley工程的设置,随后就可以对volley进行编译,产生aar包了。

Volley的使用

这里我们通过一个简单的例子来看一下volley的使用。比如,我们利用淘宝的接口抓取某一个IP地址的相关信息:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "myapplication";
    private TextView mWeatherDataText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWeatherDataText = (TextView)findViewById(R.id.weather_data);
        getIpData();
    }

    private void getIpData() {
        String RegionServiceUrl = "http://ip.taobao.com/service/getIpInfo.php?ip=112.65.189.212";
        RequestQueue requestQueue = Volley.newRequestQueue(this);
        StringRequest request = new StringRequest(RegionServiceUrl, new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.i(TAG, "response = " + response);
                mWeatherDataText.setText(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.i(TAG, "error = " + error.getMessage());
                mWeatherDataText.setText(error.getMessage());
            }
        });
        requestQueue.add(request);
    }
}

主要关注getIpData(),实际是在这个方法中利用volley,执行了网络请求。可以看到,使用volley执行网络请求,大概分为如下的几个步骤:

  1. 通过Volley类获取一个RequestQueue对象。
  2. 创建Listener来处理网络操作的返回值。Response.Listener和Response.ErrorListener分别用于处理正常的和异常的返回值。
  3. 传入Url,HTTP Method,Listener等参数创建Request。
  4. 将前面创建的Request添加到RequestQueue。

通过volley执行基本的网络请求就是这么简单。要执行更复杂的网络请求的话,可以自行探索。

Volley项目的结构

这里我们以2016.5.27 clone下来的代码为基础进行volley整个设计与实现的分析。我们先来看一下Volley的代码结构:

com/android/volley/AuthFailureError.java
com/android/volley/Cache.java
com/android/volley/CacheDispatcher.java
com/android/volley/ClientError.java
com/android/volley/DefaultRetryPolicy.java
com/android/volley/ExecutorDelivery.java
com/android/volley/Network.java
com/android/volley/NetworkDispatcher.java
com/android/volley/NetworkError.java
com/android/volley/NetworkResponse.java
com/android/volley/NoConnectionError.java
com/android/volley/ParseError.java
com/android/volley/Request.java
com/android/volley/RequestQueue.java
com/android/volley/Response.java
com/android/volley/ResponseDelivery.java
com/android/volley/RetryPolicy.java
com/android/volley/ServerError.java
com/android/volley/TimeoutError.java
com/android/volley/VolleyError.java
com/android/volley/VolleyLog.java
com/android/volley/toolbox/AndroidAuthenticator.java
com/android/volley/toolbox/Authenticator.java
com/android/volley/toolbox/BasicNetwork.java
com/android/volley/toolbox/ByteArrayPool.java
com/android/volley/toolbox/ClearCacheRequest.java
com/android/volley/toolbox/DiskBasedCache.java
com/android/volley/toolbox/HttpClientStack.java
com/android/volley/toolbox/HttpHeaderParser.java
com/android/volley/toolbox/HttpStack.java
com/android/volley/toolbox/HurlStack.java
com/android/volley/toolbox/ImageLoader.java
com/android/volley/toolbox/ImageRequest.java
com/android/volley/toolbox/JsonArrayRequest.java
com/android/volley/toolbox/JsonObjectRequest.java
com/android/volley/toolbox/JsonRequest.java
com/android/volley/toolbox/NetworkImageView.java
com/android/volley/toolbox/NoCache.java
com/android/volley/toolbox/PoolingByteArrayOutputStream.java
com/android/volley/toolbox/RequestFuture.java
com/android/volley/toolbox/StringRequest.java
com/android/volley/toolbox/Volley.java

可以看到volley的所有代码都在两个package中,一个是com.android.volley,另一个是com.android.volley.toolbox,前者可以认为是定义了volley的框架架构及接口,而后者则是相关接口的实现,提供实际的诸如HTTP网络访问、缓存等功能。

Volley设计

这里先分析com.android.volley包,来看一下volley整体的框架架构。com.android.volley包中,类名以Error结尾的所有类都是Exception,用来指示某种异常。所有这些类的层次结构如下图: 输入图片说明

对于这些Exception类,没有需要过多说明的地方。接下来,我们从网络请求的执行及执行结果的发布的角度来看一下com.android.volley包中各个类之间的关系,如下图: 输入图片说明

如我们在上面 Volley的使用 一节中看到的,应用程序在创建了Request之后,会将这个Request丢给RequestQueue,RequestQueue负责这个Request的处理及结果的Post。

RequestQueue在拿到Request之后,会根据这个Request是否应该缓存而将这个Request丢进NetworkQueue或CacheQueue,若Request应该缓存它会被放进CacheQueue中,若不需要则会被直接放进NetworkQueue中。NetworkQueue和CacheQueue都是类型为PriorityBlockingQueue<Request<?>>的容器。

NetworkDispatcher和CacheDispatcher都是Thread。NetworkDispatcher主要的职责是通过Network执行HTTP请求并抛出执行结果。NetworkDispatcher线程在被启动之后,会不断地从NetworkQueue中取出Request来执行,执行之后得到NetworkReponse,NetworkReponse会得到解析并被重新构造为Response,构造后的Response会被丢给ResponseDelivery,并由后者发布给volley的调用者,同时在Request应该被缓存时,获得的Response数据还会被放进Cache中。在Volley中,会创建NetworkDispatcher线程的线程池,其中包含固定的4个线程。

CacheDispatcher的主要职责则是访问缓存,找到之前缓存的下载的数据,并通过ResponseDelivery发布给volley的调用者,在没找到时,则将Request丢进NetworkQueue中,以便于从网络中获取。在Volley中,只有一个CacheDispatcher线程。

Cache主要定义了缓存接口。RetryPolicy定义了缓存策略的接口,每个Request都会有自己的RetryPolicy,用于帮助Network确定重试的策略。ResponseDelivery定义了Request的发布者的行为,ExecutorDelivery是ResponseDelivery的一个实现,它主要是将结果post到一个Executor中。

Volley的实现

接下来通过代码来看一下Volley的实现。

RequestQueue对象的创建

在Volley中,主要通过Volley类的newRequest来创建RequestQueue对象。Volley类就像胶水一样,把Network的实现BasicNetwork/HurlStack/HttpClientStack和Cache的实现DiskBasedCache粘到一起,创建出可用的RequestQueue。其代码如下:

public class Volley {

    /** Default on-disk cache directory. */
    private static final String DEFAULT_CACHE_DIR = "volley";

    /**
     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
     *
     * @param context A {@link Context} to use for creating the cache dir.
     * @param stack An {@link HttpStack} to use for the network, or null for default.
     * @return A started {@link RequestQueue} instance.
     */
    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

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

        if (stack == null) {
            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 queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();

        return queue;
    }

    /**
     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
     *
     * @param context A {@link Context} to use for creating the cache dir.
     * @return A started {@link RequestQueue} instance.
     */
    public static RequestQueue newRequestQueue(Context context) {
        return newRequestQueue(context, null);
    }
}

不带HttpStack参数的newRequestQueue()方法就是我们前面用到的那个,它会直接传入null HttpStack调用带HttpStack参数的newRequestQueue()方法。带参数的newRequestQueue()方法的实现,感觉改为下面这样似乎更加清晰一点:

    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                String userAgent = "volley/0";
                try {
                    String packageName = context.getPackageName();
                    PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
                    userAgent = packageName + "/" + info.versionCode;
                } catch (NameNotFoundException e) {
                }

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

        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();

        return queue;
    }

以上面的这段代码为基础,来分析newRequestQueue()方法做的事情。可以看到它主要做了如下这样几件事情:

  1. 在HttpStack参数为空时,创建HttpStack。HttpStack的职责主要是直接的执行网络请求,并返回HttpResponse。BasicNetwork通过HttpStack执行网络请求,对返回的HttpResponse做一些处理,然后构造NetworkResponse返回给调用者。这里会根据系统当前的版本,来选择是使用HttpClient接口还是HttpURLConnection接口,也就是使用HttpClientStack还是HurlStack,这两个class都是实现了HttpStack接口。这里可以来看一下HttpStack接口的定义:
public interface HttpStack {
    /**
     * Performs an HTTP request with the given parameters.
     *
     * A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
     * and the Content-Type header is set to request.getPostBodyContentType().

     *
     * @param request the request to perform
     * @param additionalHeaders additional headers to be sent together with
     *         {@link Request#getHeaders()}
     * @return the HTTP response
     */
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;

}

HurlStack的对象创建过程:

    /**
     * An interface for transforming URLs before use.
     */
    public interface UrlRewriter {
        /**
         * Returns a URL to use instead of the provided one, or null to indicate
         * this URL should not be used at all.
         */
        public String rewriteUrl(String originalUrl);
    }

    private final UrlRewriter mUrlRewriter;
    private final SSLSocketFactory mSslSocketFactory;

    public HurlStack() {
        this(null);
    }

    /**
     * @param urlRewriter Rewriter to use for request URLs
     */
    public HurlStack(UrlRewriter urlRewriter) {
        this(urlRewriter, null);
    }

    /**
     * @param urlRewriter Rewriter to use for request URLs
     * @param sslSocketFactory SSL factory to use for HTTPS connections
     */
    public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
        mUrlRewriter = urlRewriter;
        mSslSocketFactory = sslSocketFactory;
    }

然后是HttpClientStack对象的创建过程:

    protected final HttpClient mClient;

    private final static String HEADER_CONTENT_TYPE = "Content-Type";

    public HttpClientStack(HttpClient client) {
        mClient = client;
    }
  1. 利用HttpStack创建BasicNetwork对象,其过程为:
    protected final HttpStack mHttpStack;

    protected final ByteArrayPool mPool;

    /**
     * @param httpStack HTTP stack to be used
     */
    public BasicNetwork(HttpStack httpStack) {
        // If a pool isn't passed in, then build a small default pool that will give us a lot of
        // benefit and not use too much memory.
        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
    }

    /**
     * @param httpStack HTTP stack to be used
     * @param pool a buffer pool that improves GC performance in copy operations
     */
    public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
        mHttpStack = httpStack;
        mPool = pool;
    }
  1. 创建DiskBasedCache对象。
    /** Default maximum disk usage in bytes. */
    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

    /** High water mark percentage for the cache */
    private static final float HYSTERESIS_FACTOR = 0.9f;

    /** Magic number for current version of cache file format. */
    private static final int CACHE_MAGIC = 0x20150306;

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory.
     * @param rootDirectory The root directory of the cache.
     * @param maxCacheSizeInBytes The maximum size of the cache in bytes.
     */
    public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
        mRootDirectory = rootDirectory;
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory using
     * the default maximum cache size of 5MB.
     * @param rootDirectory The root directory of the cache.
     */
    public DiskBasedCache(File rootDirectory) {
        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

可以看到,volley创建了一个最大大小为5MB的一个基于磁盘的缓存,缓存目录的位置为application的缓存目录。 4. 传递BasicNetwork对象和DiskBasedCache对象,构造RequestQueue对象。 5. 执行RequestQueue的start()方法,启动Request内部的线程。 整体地来看一下RequestQueue对象的构造,和start()初始化过程:

    /** Number of network request dispatcher threads to start. */
    private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

    /** Cache interface for retrieving and storing responses. */
    private final Cache mCache;

    /** Network interface for performing requests. */
    private final Network mNetwork;

    /** Response delivery mechanism. */
    private final ResponseDelivery mDelivery;

    /** The network dispatchers. */
    private NetworkDispatcher[] mDispatchers;

    /** The cache dispatcher. */
    private CacheDispatcher mCacheDispatcher;

    private List<RequestFinishedListener> mFinishedListeners =
            new ArrayList<RequestFinishedListener>();

    /**
     * Creates the worker pool. Processing will not begin until {@link #start()} is called.
     *
     * @param cache A Cache to use for persisting responses to disk
     * @param network A Network interface for performing HTTP requests
     * @param threadPoolSize Number of network dispatcher threads to create
     * @param delivery A ResponseDelivery interface for posting responses and errors
     */
    public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }

    /**
     * Creates the worker pool. Processing will not begin until {@link #start()} is called.
     *
     * @param cache A Cache to use for persisting responses to disk
     * @param network A Network interface for performing HTTP requests
     * @param threadPoolSize Number of network dispatcher threads to create
     */
    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

    /**
     * Creates the worker pool. Processing will not begin until {@link #start()} is called.
     *
     * @param cache A Cache to use for persisting responses to disk
     * @param network A Network interface for performing HTTP requests
     */
    public RequestQueue(Cache cache, Network network) {
        this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
    }

    /**
     * Starts the dispatchers in this queue.
     */
    public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // Create network dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

    /**
     * Stops the cache and network dispatchers.
     */
    public void stop() {
        if (mCacheDispatcher != null) {
            mCacheDispatcher.quit();
        }
        for (int i = 0; i < mDispatchers.length; i++) {
            if (mDispatchers[i] != null) {
                mDispatchers[i].quit();
            }
        }
    }

在RequestQueue对象的构造过程中,会创建ExecutorDelivery对象,该对象被用于发布网络请求的执行结果,向application的主UI线程中发布,后面我们分析结果发布时,会更详细地来分析这个类。还会创建一个NetworkDispatcher的数组,其中包含了4个元素,也即是说,volley的网络请求是通过后台一个含有4个线程的固定线程池来执行的。 在RequestQueue的start()方法中,则主要是清理掉老的CacheDispatcher和NetworkDispatcher线程,创建新的并启动他们。

Request对象的添加

这里通过RequestQueue.add()的代码,来具体看一下,向RequestQueue中添加一个Request的执行过程:

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

可以看到RequestQueue.add()为Request设置了RquestQueue。

© 著作权归作者所有

共有 人打赏支持
WolfCS
粉丝 80
博文 147
码字总数 505184
作品 4
杭州
高级程序员
私信 提问
volley源码学习

volley源码学习 之前一直对于源码学习抱着一种又爱又恨的心情。爱的是因为知道源码有一些特别好的设计思路,可以让自己借鉴,而且对于设计模式来说是最好的实战场。那为啥还会恨呢,曾经很多...

xuelang
2017/11/26
0
0
Volley为何不用ThreadPoolExecutor

很多人看过Volley的源码,会有一个困惑,执行网络通信操作的4个线程是用数组管理的,没有用到ThreadPoolExecutor。 贴代码RequestQueue.start(),这是网络请求的起始点: public void start(...

指尖上的开源
2015/08/17
0
0
简要分析Volley响应3次的原理

Volley是个轻量级的通信框架,对Android开发来说确实比较好用,扩展性也强。不过今天谈的不是它的应用,而是剖析它的一个问题,就是Volley使用缓存,则会响应3次。这篇文章的思路只讲最核心的...

指尖上的开源
2015/08/17
0
0
Android面试经验总结

本文在我的微信公众号:杨守乐 首发。 转载请标明出处: http://blog.csdn.net/xiaole0313/article/details/51778103; 出处:张涛 本文由:【吧主的博客】分享 摘要 “基础 Android 知识掌握...

xiaole0313
2016/06/28
0
0
我的Android面试经验总结

原文出处: 张涛 摘要 “基础 Android 知识掌握的不错,学习能力也不错。但是基础知识部分比较薄弱,有些概念和逻辑掌握不清。” 感谢春林的这句话。 MVC,MVP 和 MVVM 架构的定义 Volley相关...

刘星石
2016/03/14
31
0

没有更多内容

加载失败,请刷新页面

加载更多

CentOS5.11配置Let's Encrypt免费证书

安装环境: [root@WQ02 opt]# lsb_release -aLSB Version::core-4.0-amd64:core-4.0-ia32:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-ia32:graphics-4.0-noarch:printing-4.0-amd6......

m_lm
8分钟前
0
0
看看Canonical分享的2018年的十大Linux Snap

导读 Linux在2018年最令人耳目一新的一个方面是Snaps的普及。 Canonical透露,集装箱化的包装已经取得了巨大的成功。今天,Ubuntu制造商分享了2018年的十大Snap。 随着2018年即将结束,我发现...

问题终结者
21分钟前
1
0
天啦噜!在家和爱豆玩"剪刀石头布",阿里工程师如何办到?

阿里妹导读:如今,90、00后一代成为消费主力,补贴、打折、优惠等“价格战”已很难建立起忠诚度,如何与年轻人建立更深层次的情感共鸣?互动就是一种很好的方式,它能让用户更深度的参与品牌...

阿里云官方博客
48分钟前
1
0
聊聊flink的Table API及SQL Programs

序 本文主要研究一下flink的Table API及SQL Programs 实例 // for batch programs use ExecutionEnvironment instead of StreamExecutionEnvironmentStreamExecutionEnvironment env = Stre......

go4it
57分钟前
2
0
mysqldump应用

备份单个库/表数据或库/表结构 命令行下具体用法如下: mysqldump -u用戶名 -p密码 -d 数据库名 表名 > 备份文件名 1、导出数据库为dbname的表结构(其中用戶名為root,密码为dbpasswd,生成的...

阿dai
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部