Android 学习笔记之Volley开源框架解析(四)

学习内容:

1.NetWorkDispatcher网络请求线程调度...

2.NetWork网络请求抽象类...

3.BasicNetWork网络请求抽象类的具体实现...

4.NetWorkResponse接收网络请求返回的响应...

5.ResponseDelivery请求分配抽象类...

6.ExecutorDelivery请求分配的具体实现类...

  上一章说完了缓存请求线程调度,那么现在就说一下网络请求线程调度是如何来完成的,网络请求线程调度也是有一个核心过程的,从网络上获取数据信息的过程, 首先从网络请求队列取出请求,如果请求存在,那么对请求进行相关的处理,如果没有请求,那么线程进入等待状态,取出请求之后需要先对请求是否已经被中途取 消进行相关的判断,如果已经请求已经被中途中断,那么结束这次的处理过程,如果没有取消,那么执行请求,获取服务器的返回数据,然后对返回的响应是够是 304响应进行相关的判断,如果是304响应,那么直接也结束对请求的处理。

  304请求表示的是相同请求已经被服务器响应,并且返回了已经返回了相关的数据,由于服务器的状态是高并发的执行状态,有可能在同一时间段对两种或几种相同的请求进行相关的处理,那么对于这样多种相同的请求,服务器只需要响应一次数据就可以了,剩下的由ResponseDelivery去分发给所有与之相同的请求就可以了...也就是说服务器对于同一时间段的多个相同的请求只需要响应一次...

  如果不是304响应,那么表示这次请求是一个新的请求,那么我们需要向服务器发送请求来获取服务器的响应,最后通过是否进行缓存进行判断之后,一个请求就可以被分发出去了...

1.NetWorkDispatcher.java

  1.1 变量的定义

 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; // 用于关闭线程...

  1.2 NetWorkDispatcher构造函数...

  网络请求线程调度对象的初始化过程...构造函数比较简单,没什么说的...

 public NetworkDispatcher(BlockingQueue<Request> queue,
Network network, Cache cache,
ResponseDelivery delivery) {
mQueue = queue;
mNetwork = network;
mCache = cache;
mDelivery = delivery;
}

  1.3 public void run(){}

  run()方法,线程中最重要的方法,必须要继承的...整个方式就如同刚开始介绍的一样,从请求队列中取出请求,然后判断请求是否被中断,中断就结束处理,没有中断就发送请求,然后对服务器是否已经对这次请求返回了响应数据,如果服务器已经响应了与之相同的请求,那么就停止对这次请求的处理,由分发响应函数去处理,如果是一个新的请求,那么需要定义一个请求对象,然后发送...最后获取到服务器响应后,对响应进行分发...

 @Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //设置线程优先级...
Request request;
while (true) {
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;
} // Tag the request (if API >= 14)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { //如果API>=10,那么为请求设置标签...
TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
} // 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()) { //如果这个请求是没有改变..说白了就是请求如果是相同的...那么服务器就返回一次响应...其他的通过Delivery去分发就行了...
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) {
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) { //如果出现了错误,那么把错误发送...
VolleyLog.e(e, "Unhandled exception %s", e.toString());
mDelivery.postError(request, new VolleyError(e));
}
}
}
//一个错误处理函数,当发生解析请求或者是分发请求时出现错误时进行调用...
private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
error = request.parseNetworkError(error);
mDelivery.postError(request, error);
}

上面涉及到了执行请求...以及分发服务器响应,但是是如何实现的呢?我们先看如何去执行一个请求...

2.NetWork.java

  NetWork.java是一个抽象的接口...对外提供一个执行请求方法的接口,方便其他类去实现...

package com.android.volley;

/**
* An interface for performing requests.
*/
public interface Network {
/**
* Performs the specified request.
* @param request Request to process
* @return A {@link NetworkResponse} with data and caching metadata; will never be null
* @throws VolleyError on errors
*/
public NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

3.BasicNetWork.java

  BasicNetWork是实现NetWork的具体抽象类,是如何执行请求的一个具体过程,其中内部也封装了一些其他方法...

  3.1 变量的定义

  这几个变量的定义相对比较抽象,但是会在下面细说...

   protected static final boolean DEBUG = VolleyLog.DEBUG; //用于Volley内部调试..

    private static int SLOW_REQUEST_THRESHOLD_MS = 3000;//对于缓慢的请求定义了一个请求时间...

    private static int DEFAULT_POOL_SIZE = 4096; //Int值,用于以后的获取网络数据...

    protected final HttpStack mHttpStack;  //Http请求栈...

    protected final ByteArrayPool mPool; //ByteArrayPool对象...

  3.2 public BasicNetWork(){}

  构造函数...构造函数构造了一个保存Http请求栈,以及一个获取网络数据对象...

 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
*/
//建立一个BasicNetWork对象,这个对象保存了一个请求栈区,另一个参数用于获取数据而建立的对象...
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
mHttpStack = httpStack;
mPool = pool;
}

  3.3  public 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; //请求内容对象...
Map<String, String> responseHeaders = new HashMap<String, String>(); //map集合,用于保存数据报的Header中的数据..
try {
// Gather headers.
Map<String, String> headers = new HashMap<String, String>(); //用于保存缓存下来的Header...缓存的Header一般包含服务响应的整体时间,缓存新鲜度验证等属性值...
addCacheHeaders(headers, request.getCacheEntry()); //添加请求头部的过程...
httpResponse = mHttpStack.performRequest(request, headers);//执行请求,获取响应..
StatusLine statusLine = httpResponse.getStatusLine();//获取响应状态...
int statusCode = statusLine.getStatusCode();//获取状态码.. responseHeaders = convertHeaders(httpResponse.getAllHeaders());//获取响应后的Header中的所有数据...
// Handle cache validation.
//对304响应的一个判断过程,如果是304响应,那么直接走缓存,从缓存获取数据...
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
request.getCacheEntry().data, responseHeaders, true);
} // Some responses such as 204s do not have content. We must check.
if (httpResponse.getEntity() != null) {//判断响应是否是204判断,由于204响应时不返回数据信息的...因此需要判断...
responseContents = entityToBytes(httpResponse.getEntity()); //如果存在内容,那么通过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 != HttpStatus.SC_OK && statusCode != HttpStatus.SC_NO_CONTENT) {
throw new IOException(); //如果请求状态出现错误,那么抛出异常...
}
return new NetworkResponse(statusCode, responseContents, responseHeaders, false); //如果请求不是304请求,并且上面异常情况也没有发生,那么返回新的请求中的内容+头部以及状态码...
} 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);//url异常...
} catch (IOException e) {//IO异常的处理...
int statusCode = 0;
NetworkResponse networkResponse = null;
if (httpResponse != null) { //如果响应存在,但是出现异常..
statusCode = httpResponse.getStatusLine().getStatusCode(); //返回异常状态码..可以使客户端知道异常情况...
} else {
throw new NoConnectionError(e);
}
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
if (responseContents != null) {//如果响应内容不是空...
networkResponse = new NetworkResponse(statusCode, responseContents,
responseHeaders, false);//获取响应...
//请求需要进行验证,或者是需要授权异常处理...
if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
statusCode == HttpStatus.SC_FORBIDDEN) {
attemptRetryOnException("auth",
request, new AuthFailureError(networkResponse));
} else {
// TODO: Only throw ServerError for 5xx status codes.
throw new ServerError(networkResponse);
}
} else {
throw new NetworkError(networkResponse);
}
}
}
}

  这个函数涉及了其他几个函数...

  3.4 private void logSlowRequests(long requestLifetime, Request<?> request,byte[] responseContents, StatusLine statusLine) {}

  记录一个请求——响应的时间超出了预先设置的缓慢请求时间,那么需要进行记录...记录响应码,响应内容等等函数比较的简单...

 private void logSlowRequests(long requestLifetime, Request<?> request,
byte[] responseContents, StatusLine statusLine) {
if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
"[rc=%d], [retryCount=%s]", request, requestLifetime,
responseContents != null ? responseContents.length : "null",
statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
}

  3.5  private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {}

  添加请求的请求头部,从缓存当中获取头部信息添加到这次请求...

 private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
// If there's no cache entry, we're done.
if (entry == null) { //缓存数据为空...直接return
return;
} if (entry.etag != null) {
headers.put("If-None-Match", entry.etag); //返回新鲜度验证标志..
} if (entry.serverDate > 0) {
Date refTime = new Date(entry.serverDate); //返回请求——响应的时间...
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}

   3.6  private static void attemptRetryOnException(String logPrefix, Request<?> request,VolleyError exception) throws VolleyError {}

   尝试重试策略方法...如果请求发生了异常...

private static void attemptRetryOnException(String logPrefix, Request<?> request,
VolleyError exception) throws VolleyError {
RetryPolicy retryPolicy = request.getRetryPolicy(); //获取请求的重试策略...
int oldTimeout = request.getTimeoutMs(); try {
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));
}

3.7  private static Map<String, String> convertHeaders(Header[] headers) {}

 上面涉及到了当响应返回的时候需要获取响应数据报的Header,将所有的Header数据获取并保存...以键值对的形式保存在map集合当中...最后传递给responseHeaders集合...

private static Map<String, String> convertHeaders(Header[] headers) {
Map<String, String> result = new HashMap<String, String>();
for (int i = 0; i < headers.length; i++) {
result.put(headers[i].getName(), headers[i].getValue());
}
return result;
}

 3.8   private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {}

 当Http请求响应成功时,我们需要从HttpEntity中获取返回的内容数据...数据的获取调用此函数..这个函数采用了PoolingArrayByteOutputStream()流..这个流采用了字节回收机制,可以减少内存的分配和回收...我们先知道就行...后面还会具体说明...

 private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
PoolingByteArrayOutputStream bytes =
new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); //新建一个流对象..
byte[] buffer = null; //缓冲字节..
try {
InputStream in = entity.getContent();//将Entity中保存的内容封装成流...
if (in == null) {
throw new ServerError();
}
buffer = mPool.getBuf(1024); //缓冲流分配...首先new一个缓冲字节数组...
int count;
while ((count = in.read(buffer)) != -1) {
bytes.write(buffer, 0, count); //写入数据...
}
return bytes.toByteArray();//返回数据..
} finally {
try {
// Close the InputStream and release the resources by "consuming the content".
entity.consumeContent();
} catch (IOException e) {
// This can happen if there was an exception above that left the entity in
// an invalid state.
VolleyLog.v("Error occured when calling consumingContent");
}
mPool.returnBuf(buffer);
bytes.close();
}
}

  这样总体就完成了请求之后响应数据的获取,也就是数据报的Header+Body的数据被保存了起来...那么完成了数据的获取,就需要响应数据的分发了...分发到请求才是请求——响应的一个最终完成过程...

4.NetWorkResponse.java

  那么响应的传递就需要通过NetWorkResponse来进行传递,无论是从网络上获取的请求数据,还是从缓存当中获取的请求数据,都会被封装成NetWorkResponse,然后传递给相应请求中的parseNetWorkResponse方法,在进行下一步的处理...

package com.android.volley;

import org.apache.http.HttpStatus;

import java.util.Collections;
import java.util.Map; /**
* Data and headers returned from {@link Network#performRequest(Request)}.
*/
public class NetworkResponse {
//对服务器的响应进行一个彻底封装...
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
boolean notModified) {
this.statusCode = statusCode;
this.data = data;
this.headers = headers;
this.notModified = notModified;
}
//构造函数..用来调用第一个构造函数,传递过来的响应只包含数据部分...
public NetworkResponse(byte[] data) {
this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false);
}
//第二个构造函数,通过响应的数据和相关头部来调用第一个构造函数...
public NetworkResponse(byte[] data, Map<String, String> headers) {
this(HttpStatus.SC_OK, data, headers, false);
} /** The HTTP status code. */
public final int statusCode; //响应状态码... /** Raw data from this response. */
public final byte[] data; //响应数据... /** Response headers. */
public final Map<String, String> headers; //以键值对的形式保存首部.. /** True if the server returned a 304 (Not Modified). */
public final boolean notModified; //304响应的判断...
}

  NetWorkResponse只是对服务器的响应的一个进一步封装,以参数的形式传递到Request.parseNetWorkResponse()方法...如果中间并么有出现什么异常情况,那么最后相应实现了Request.parseNetWorkResponse类会调用Response.success方法,将这次请求进行最后的封装,封装成Response<T>的形式,请求是什么类型,T就是什么类型...

5.Response.java

package com.android.volley;

public class Response<T> {

    /** Callback interface for delivering parsed responses. */
public interface Listener<T> {
/** Called when a response is received. */
public void onResponse(T response); //当一个请求——相应成功后的监听...
} /** Callback interface for delivering error responses. */
public interface ErrorListener {
/**
* Callback method that an error has been occurred with the
* provided error code and optional user-readable message.
*/
public void onErrorResponse(VolleyError error); //当请求出现了错误时,需要监听错误..
} /** Returns a successful response containing the parsed result. */
public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
return new Response<T>(result, cacheEntry); //success方法,当请求被解析成功时调用的方法...
} /**
* Returns a failed response containing the given error code and an optional
* localized message displayed to the user.
*/
public static <T> Response<T> error(VolleyError error) {
return new Response<T>(error); //如果解析请求时失败需要封装错误...
} /** Parsed response, or null in the case of error. */
public final T result; //响应返回的结果.. /** Cache metadata for this response, or null in the case of error. */
public final Cache.Entry cacheEntry; //缓存... /** Detailed error information if <code>errorCode != OK</code>. */
public final VolleyError error; //一个错误对象...记录错误的生成... /** True if this response was a soft-expired one and a second one MAY be coming. */
public boolean intermediate = false; //判断一个请求是否失效... /**
* Returns whether this response is considered successful.
*/
public boolean isSuccess() {
return error == null; //如果成功就没有错误传递...
} private Response(T result, Cache.Entry cacheEntry) { //对成功时封装的构造函数...
this.result = result;
this.cacheEntry = cacheEntry;
this.error = null;
} private Response(VolleyError error) {/失败时封装的构造函数...
this.result = null;
this.cacheEntry = null;
this.error = error;
}
}

  当响应被封装成Response之后,就需要向客户端发送响应了,通过postResponse方法进行发送,如果一个响应成功,我们需要发送响应,但是如果中途出现了失败,那么我们需要把错误发送,需要让客户端清楚到底是发生了什么错误,这样在错误发生时提示用户到底应该怎样进行操作...

6.ResponseDelivery.java

  解析响应之后的发送类,只是一个抽象的接口,我们可以人为去重写如何发送响应...而系统默认则采用线程池的方式对响应进行发送...

package com.android.volley;

public interface ResponseDelivery {
/**
* Parses a response from the network or cache and delivers it.
*/
public void postResponse(Request<?> request, Response<?> response); //对响应进行发送... /**
* Parses a response from the network or cache and delivers it. The provided
* Runnable will be executed after delivery.
*/
public void postResponse(Request<?> request, Response<?> response, Runnable runnable); //对响应发送,同时开启一个线程去执行其他事情...线程在这个方法结束后执行... /**
* Posts an error for the given request.
*/
public void postError(Request<?> request, VolleyError error); //发送错误信息...
}

7.ExecutorDelivery.java

  发送请求或者是错误的具体实现类,采用线程池的方式对响应进行发送...无论是服务器响应正确还是错误,我们都需要对其进行封装发送给客户端,让客户端去清楚服务器到底返回了什么东西...

package com.android.volley;

import android.os.Handler;

import java.util.concurrent.Executor;

public class ExecutorDelivery implements ResponseDelivery {
/** Used for posting responses, typically to the main thread. */
private final Executor mResponsePoster; //定义一个线程池... //定义了一个Response的传输接口...
public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster = new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
} //构造函数,定义了一个线程池...
public ExecutorDelivery(Executor executor) {
mResponsePoster = executor;
}
//发送请求的抽象方法的实现...
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
/*最后都是通过调用此方法来发送请求,因为poseResponse的方法有两种
* 一种是public void postResponse(Request<?> request, Response<?> response);
* 另一种是 public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
*这两个方法上面已经说过,就是一个带有一个附加线程,一个没有而已...
*但最终都需要调用这个方法...
*/
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered(); //表示可以发送请求...
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); //用线程封装好Response..通过线程池的方式去管理这些线程...从这一步开始run()方法已经被调用了...
} //如果出现了错误,那么将错误封装,同时也要发送给请求的客户端...
@Override
public void postError(Request<?> request, VolleyError error) {
request.addMarker("post-error");
Response<?> response = Response.error(error); //封装错误..
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null)); //发送错误...
} @SuppressWarnings("rawtypes")
private class ResponseDeliveryRunnable implements Runnable {
private final Request mRequest; //请求
private final Response mResponse; //响应
private final Runnable mRunnable; //其他线程...
//构造函数非常的简单..对三者的一个封装过程...
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
//run方法...
@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;
} // Deliver a normal response or error, depending.
if (mResponse.isSuccess()) { //如果服务器响应成功,中途没有错误的发生,,,
mRequest.deliverResponse(mResponse.result);//将服务器返回的结果发送给客户端...这是最后的关键地方...
} else {
mRequest.deliverError(mResponse.error);//如果其中出现了失败,需要把错误发送...
} // If this is an intermediate response, add a marker, otherwise we're done
// and the request can be finished.
//这里不知道该如何理解...翻译成中间响应...
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response"); //如果是需要进行调试过程...
} else {
mRequest.finish("done");//如果不是表示这次请求结束...
} // If we have been provided a post-delivery runnable, run it.
if (mRunnable != null) { //如果附加线程不是空,那么就启动附加线程..
mRunnable.run();
}
}
}
}

  我们可以看到,最后服务器的响应被封装之后,通过mRequest.deliveryResponse或者是mRequest.deliveryerror进行发送...而这两个方法就会在相应类型的请其中得到重写...因为所有的其他请求都是继承Request类的...Request类中只是一个抽象的方法,具体的实现在那些实际实现了Request的类中...而每一个实现类中都会去调用mListener.onResponse(response)方法,这里只表示请求成功时调用的方法...

abstract protected void deliverResponse(T response);
public interface Listener<T> {
/** Called when a response is received. */
public void onResponse(T response);
}

  这样在客户端重写OnResponse方法之后,就彻底的完成了请求——响应的结束过程...数据也就成功的从服务器通过网络成功的发送给了客户端...



 

上一篇:Python中的相对文件路径的调用


下一篇:C# 一行代码随意类型转换