Android OkHttp 全面详解

  • 包的导入
  • 基本使用
    • 异步请求
    • 同步请求
    • build创建
  • 源码跟踪
    • newCall
    • RealCall.enqueue
    • Dispatcher.enqueue
    • executorService().execute(call)
    • AsyncCall.execute()
    • 拦截链
      • RealInterceptorChain.proceed()
      • RetryAndFollowUpInterceptor
      • BridgeInterceptor
      • CacheInerceptor
      • ConnectInterceptor
      • CallServerInterceptor
  • 总结

目前来说OkHttp已经是对于android开发人员实现网络编程的重要途径之一了。
github地址
这里以3.10.0的源码分析,梳理整个网络请求的流程。

包的导入

引入包

 api 'com.squareup.okhttp3:okhttp:3.10.0'

日志库:搭配更佳

api 'com.squareup.okhttp3:logging-interceptor:3.10.0'

上面的不够好用?来个客户端的日志更清晰的:chuck库

   debugCompile 'com.readystatesoftware.chuck:library:1.1.0'releaseCompile 'com.readystatesoftware.chuck:library-no-op:1.1.0'

基本使用

注意网络权限的申请

异步请求

         OkHttpClient okHttpClient = new OkHttpClient();Request request = new Request.Builder().url(url)//默认get请求.get().build();okHttpClient.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {}@Overridepublic void onResponse(Call call, Response response) throws IOException {}});

同步请求

在android3.0 之后 不允许在主线程里请求网络。
这里可以开启一个子线程去操作同步请求(ps:那还是异步了),或者设置android严苟模式设置。

 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().penaltyDialog().build());OkHttpClient okHttpClient = new OkHttpClient();Request request = new Request.Builder().url(url).build();try {Response response =  okHttpClient.newCall(request).execute();} catch (IOException e) {e.printStackTrace();}

build创建

有很多功能可以选择,并不是每个都需要用到的。

     //创建者模式OkHttpClient.Builder builder = new OkHttpClient.Builder();builder.connectTimeout(60, TimeUnit.SECONDS)//设置连接超时//设置读超时.readTimeout(60,TimeUnit.SECONDS)//设置写超时.writeTimeout(60,TimeUnit.SECONDS)//添加拦截器 日志库的拦截一般在这里.addInterceptor(interceptor)//设置网络拦截器.addNetworkInterceptor(netInterceptor)// 设置dns 解析节点 加快网络访问.dns(dns)//设置Cookie持久化.cookieJar(cookieJar)//设置本地缓存策略.cache(cache)//设置代理.proxy(proxy)//创建.build();

这里说说dns这个,这个是可以设置第三方的dns服务器运营商,加快域名的解析速度。大家知道的dns的解析是一层一层向上传递的查找过程,这个过程可能会耗时。
阿里云有提供第三方的dns服务的,也可以只直接配合OkHttp使用,产品文档

源码跟踪

看了上面的简单使用介绍后,OkHttp的请求操作起点从OkHttpClient.newCall().enqueue() 开始的。

newCall

 @Override public Call newCall(Request request) {return RealCall.newRealCall(this, request, false /* for web socket */);}
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {// Safely publish the Call instance to the EventListener.RealCall call = new RealCall(client, originalRequest, forWebSocket);//注册到工厂监听类call.eventListener = client.eventListenerFactory().create(call);return call;}

newRealCall方法返回一个RealCall对象,然后会走RealCall.enqueue方法

RealCall.enqueue

 @Override public void enqueue(Callback responseCallback) {synchronized (this) {if (executed) throw new IllegalStateException("Already Executed");executed = true;}captureCallStackTrace();eventListener.callStart(this);//okhttp  两大核心之一:分发器client.dispatcher().enqueue(new AsyncCall(responseCallback));}

可以看出来,最后的请求是有Dispatcherenqueue方法来实现的,Dispatcher是整个OkHttpCient的分发器(调度器),是一种门户模式的实现(不需要关系内部构造,通过门户就可以获取想要的)。主要用来实现执行、取消异步请求、执行请求等操作。

Dispatcher.enqueue

synchronized void enqueue(AsyncCall call) {//异步请求队列中数量小于最大请求数 并且 同一主机正运行数量小于最大运行数if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {//加入队列runningAsyncCalls.add(call);//执行请求callexecutorService().execute(call);} else {readyAsyncCalls.add(call);}}

在Dispatcher的内部维护了一个线程池去执行异步任务,并且会根据一定的策略,保证最大并发个数、同一host主机允许执行请求的线程个数等。
具体看下当前版本的维护情况:

 //private int maxRequests = 64;private int maxRequestsPerHost = 5;

还维护了三个队列来维护每个请求call,如下:

  /** Ready async calls in the order they'll be run. */  异步等待private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */ 异步请求中private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();/** Running synchronous calls. Includes canceled calls that haven't finished yet. */  同步请求中private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

知道了部分参数含义,再来看上面逻辑
如果–》异步请求队列中数量小于最大请求数 并且 同一主机正运行数量小于最大运行线程数
则执行–》 加入异步请求队列中,并执行AsyncCall(AsyncCall实现了Runnable接口,因此整个操作会在子线程中执行)
否则–》
加入异步等待队列

executorService().execute(call)

executorService()是OkHttp线程的创建

 public synchronized ExecutorService executorService() {if (executorService == null) {executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));}return executorService;}

execute(call)方法其实就是执行AsyncCallrun方法。

AsyncCall.execute()

首页自己并没有实现run方法,而是交给NamedRunnable(给线程设置名字)来实现的,会走会自己的execute()方法。

 @Override protected void execute() {//回调标示boolean signalledCallback = false;try {//okhttp  两大核心之一:拦截链Response response = getResponseWithInterceptorChain();//该拦截器从故障中恢复,并根据需要进行重定向。如果呼叫被取消,则可能抛出IOException。//判断重定向拦截器是否取消链的下发if (retryAndFollowUpInterceptor.isCanceled()) {signalledCallback = true;//失败回调responseCallback.onFailure(RealCall.this, new IOException("Canceled"));} else {signalledCallback = true;responseCallback.onResponse(RealCall.this, response);}} catch (IOException e) {if (signalledCallback) {// Do not signal the callback twice!Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);} else {eventListener.callFailed(RealCall.this, e);responseCallback.onFailure(RealCall.this, e);}} finally {client.dispatcher().finished(this);}}

这里有个非常关键的拦截链。会返回拦截链处理完返回的请求结果Response,并把结果回调回去。所以要看看拦截链到底干啥了。

拦截链

 Response getResponseWithInterceptorChain() throws IOException {// Build a full stack of interceptors.List<Interceptor> interceptors = new ArrayList<>();//client创建加入的自定义拦截器interceptors.addAll(client.interceptors());//重定向拦截器interceptors.add(retryAndFollowUpInterceptor);//桥链接拦截器interceptors.add(new BridgeInterceptor(client.cookieJar()));//缓存拦截器interceptors.add(new CacheInterceptor(client.internalCache()));//连接拦截器interceptors.add(new ConnectInterceptor(client));if (!forWebSocket) {//client创建加入的自定义网络拦截器interceptors.addAll(client.networkInterceptors());}//请求拦截器interceptors.add(new CallServerInterceptor(forWebSocket));Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,originalRequest, this, eventListener, client.connectTimeoutMillis(),client.readTimeoutMillis(), client.writeTimeoutMillis());return chain.proceed(originalRequest);}

这里是典型的责任链模式。为啥了?
这里把所以的拦截器放入interceptors中,最后由RealInterceptorChain来执行proceed方法。来看看内部如何抽象的运行了,因为真正的执行都是各个拦截器处理的。具体看注释。

RealInterceptorChain.proceed()

 public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection) throws IOException {if (index >= interceptors.size()) throw new AssertionError();calls++;// If we already have a stream, confirm that the incoming request will use it.//如果我们已经有了一个流,请确认传入的请求将使用它。if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)+ " must retain the same host and port");}// If we already have a stream, confirm that this is the only call to chain.proceed().//如果我们已经有了一个流,请确认这是对chain.proceed()的唯一调用。if (this.httpCodec != null && calls > 1) {throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)+ " must call proceed() exactly once");}// Call the next interceptor in the chain.//创建调用链中的下一个拦截器。 index + 1下一个RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,writeTimeout);//当前具体拦截器 Interceptor interceptor = interceptors.get(index);//执行当前拦截器的逻辑,并传入下一个的RealInterceptorChain ,这里会递归执行并返回Response response = interceptor.intercept(next);//这里返回的已经是 被所有拦截器处理完的结果 // Confirm that the next interceptor made its required call to chain.proceed().if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {throw new IllegalStateException("network interceptor " + interceptor+ " must call proceed() exactly once");}// Confirm that the intercepted response isn't null.if (response == null) {throw new NullPointerException("interceptor " + interceptor + " returned null");}if (response.body() == null) {throw new IllegalStateException("interceptor " + interceptor + " returned a response with no body");}return response;}

简要讲讲各个拦截器的作用:

RetryAndFollowUpInterceptor

这个拦截器它的作用主要是负责请求的重定向操作,用于处理网络请求中,请求失败后的重试机制。

  • 会实例化一个StreamAllocation,会创建一个Socket连接对象。
  • 会执行下一个拦截器的实现,并等待返回一个Response对象
  • 协调Connections、Streams、Calls之间的关系
  • 会循环判断是否取消请求,取消则释放相应资源

BridgeInterceptor

这个拦截器它的作用主要负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。并对Request中的Head设置默认值,比如Content-Type、Keep-Alive、Cookie等。
执行代码如下:

 @Override public Response intercept(Chain chain) throws IOException {Request userRequest = chain.request();Request.Builder requestBuilder = userRequest.newBuilder();RequestBody body = userRequest.body();if (body != null) {MediaType contentType = body.contentType();if (contentType != null) {requestBuilder.header("Content-Type", contentType.toString());}long contentLength = body.contentLength();if (contentLength != -1) {requestBuilder.header("Content-Length", Long.toString(contentLength));requestBuilder.removeHeader("Transfer-Encoding");} else {requestBuilder.header("Transfer-Encoding", "chunked");requestBuilder.removeHeader("Content-Length");}}//默认hostif (userRequest.header("Host") == null) {requestBuilder.header("Host", hostHeader(userRequest.url(), false));}//添加Keep-Alive默认if (userRequest.header("Connection") == null) {requestBuilder.header("Connection", "Keep-Alive");}// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing// the transfer stream.//如果我们添加“Accept-Encoding: gzip”头字段,我们也负责解压传输流boolean transparentGzip = false;if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {transparentGzip = true;requestBuilder.header("Accept-Encoding", "gzip");}// 创建OkhttpClient配置的cookieJarList<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());if (!cookies.isEmpty()) {requestBuilder.header("Cookie", cookieHeader(cookies));}if (userRequest.header("User-Agent") == null) {requestBuilder.header("User-Agent", Version.userAgent());}// 拼接完后执行下一个Response networkResponse = chain.proceed(requestBuilder.build());//解析服务器返回的Header,如果没有这个cookie,则不进行解析HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);if (transparentGzip&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))&& HttpHeaders.hasBody(networkResponse)) {GzipSource responseBody = new GzipSource(networkResponse.body().source());Headers strippedHeaders = networkResponse.headers().newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build();responseBuilder.headers(strippedHeaders);String contentType = networkResponse.header("Content-Type");responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));}return responseBuilder.build();}

CacheInerceptor

负责HTTP请求的缓存处理。

  • 根据Request获取当前已有缓存的Response(可能为null),并根据获取到的缓存Response,创建CacheStrategy对象。

CacheInterceptor.intercept()中

//判断是否为空Response cacheCandidate = cache != null? cache.get(chain.request()): null;long now = System.currentTimeMillis();
//创建CacheStrategy缓存策略CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();Request networkRequest = strategy.networkRequest;Response cacheResponse = strategy.cacheResponse;
  • 通过CacheStrategy判断当前缓存中的Response是否有效(判断是否过期,过期策略自行商量),如果缓存Response可用则直接返回(拦截链终止),否则调用chain.proceed()继续执行下一个拦截器,也就是发送网络请求从服务器获取远端Response。
  //如果缓存无效,且禁止使用网络请求,则直接返回空的Responseif (networkRequest == null && cacheResponse == null) {//提前返回了return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(Util.EMPTY_RESPONSE)//空的.sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();}// 缓存在有效期内,则将缓存中的Response返回if (networkRequest == null) {return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();}//最后如果没有缓存,或者缓存失效,则发送网络请求获取服务器ResponseResponse networkResponse = null;try {//执行下一个拦截器了networkResponse = chain.proceed(networkRequest);} finally {// If we're crashing on I/O or otherwise, don't leak the cache body.if (networkResponse == null && cacheCandidate != null) {closeQuietly(cacheCandidate.body());}}
  • 如果从服务器端成功的获取Response,再判断是否将Response进行缓存处理。
//通过网络请求返回的ResponseResponse response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();//开发人员有设置自定义cache,则将最新的数据缓存起来if (cache != null) {if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {// Offer this request to the cache.CacheRequest cacheRequest = cache.put(response);return cacheWritingResponse(cacheRequest, response);}if (HttpMethod.invalidatesCache(networkRequest.method())) {try {cache.remove(networkRequest);} catch (IOException ignored) {// The cache cannot be written.}}}return response;

ConnectInterceptor

负责建立与服务器地址之间的连接,也就是TCP链接。链接后会进入下一个拦截器

@Override public Response intercept(Chain chain) throws IOException {RealInterceptorChain realChain = (RealInterceptorChain) chain;Request request = realChain.request();StreamAllocation streamAllocation = realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.boolean doExtensiveHealthChecks = !request.method().equals("GET");HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);RealConnection connection = streamAllocation.connection();return realChain.proceed(request, streamAllocation, httpCodec, connection);}

CallServerInterceptor

负责向服务器发送请求,并从服务器拿到远端数据结果。是OkHttp中最核心的网络请求部分。主要是向服务端发送请求数据和获取到数据后构建Response对象。

@Override public Response intercept(Chain chain) throws IOException {RealInterceptorChain realChain = (RealInterceptorChain) chain;//获取HttpCodecHttpCodec httpCodec = realChain.httpStream();//RetryAndFollowUpInterceptor创建的StreamAllocationStreamAllocation streamAllocation = realChain.streamAllocation();RealConnection connection = (RealConnection) realChain.connection();Request request = realChain.request();long sentRequestMillis = System.currentTimeMillis();//将请求头发送到服务端realChain.eventListener().requestHeadersStart(realChain.call());httpCodec.writeRequestHeaders(request);realChain.eventListener().requestHeadersEnd(realChain.call(), request);//如果请求有body,则也发送到服务端Response.Builder responseBuilder = null;if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100// Continue" response before transmitting the request body. If we don't get that, return// what we did get (such as a 4xx response) without ever transmitting the request body.if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {httpCodec.flushRequest();realChain.eventListener().responseHeadersStart(realChain.call());responseBuilder = httpCodec.readResponseHeaders(true);}if (responseBuilder == null) {// Write the request body if the "Expect: 100-continue" expectation was met.realChain.eventListener().requestBodyStart(realChain.call());long contentLength = request.body().contentLength();CountingSink requestBodyOut =new CountingSink(httpCodec.createRequestBody(request, contentLength));//请求数据在Okio里面BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);request.body().writeTo(bufferedRequestBody);bufferedRequestBody.close();realChain.eventListener().requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);} else if (!connection.isMultiplexed()) {// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection// from being reused. Otherwise we're still obligated to transmit the request body to// leave the connection in a consistent state.streamAllocation.noNewStreams();}}//结束网络请求httpCodec.finishRequest();//从服务端获取相应的数据并构建Response对象if (responseBuilder == null) {realChain.eventListener().responseHeadersStart(realChain.call());responseBuilder = httpCodec.readResponseHeaders(false);}//构建响应bodyResponse response = responseBuilder.request(request).handshake(streamAllocation.connection().handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build();int code = response.code();if (code == 100) {// server sent a 100-continue even though we did not request one.// try again to read the actual responseresponseBuilder = httpCodec.readResponseHeaders(false);response = responseBuilder.request(request).handshake(streamAllocation.connection().handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build();code = response.code();}realChain.eventListener().responseHeadersEnd(realChain.call(), response);//下面是对响应码的处理if (forWebSocket && code == 101) {// Connection is upgrading, but we need to ensure interceptors see a non-null response body.response = response.newBuilder().body(Util.EMPTY_RESPONSE).build();} else {response = response.newBuilder().body(httpCodec.openResponseBody(response)).build();}if ("close".equalsIgnoreCase(response.request().header("Connection"))|| "close".equalsIgnoreCase(response.header("Connection"))) {streamAllocation.noNewStreams();}if ((code == 204 || code == 205) && response.body().contentLength() > 0) {throw new ProtocolException("HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());}return response;}

在CallServerInterceptor中,向服务器发送数据已经获取数据都是一个Okio的框架完成的,这是OkHttp框架的基石。

总结

五个拦截器已经讲完了,拦截器链的执行完毕也是整个请求流程的结束。不必太在意Okio到底怎么实现了,其实更关注中间层的扩展怎么实现就好了。比如cache、cookie、请求进度、失败重请求等中间层功能的扩展。

Android OkHttp 全面详解相关推荐

  1. 《Android游戏开发详解》——第1章,第1.6节函数(在Java中称为“方法”更好)...

    本节书摘来自异步社区<Android游戏开发详解>一书中的第1章,第1.6节函数(在Java中称为"方法"更好),作者 [美]Jonathan S. Harbour,更 ...

  2. JMessage Android 端开发详解

    JMessage Android 端开发详解 目前越来越多的应用会需要集成即时通讯功能,这里就为大家详细讲一下如何通过集成 JMessage 来为你的 App 增加即时通讯功能. 首先,一个最基础的 ...

  3. 《Java和Android开发实战详解》——2.5节良好的Java程序代码编写风格

    本节书摘来自异步社区<Java和Android开发实战详解>一书中的第2章,第2.5节良好的Java程序代码编写风格,作者 陈会安,更多章节内容可以访问云栖社区"异步社区&quo ...

  4. Android事件流程详解

    Android事件流程详解 网络上有不少博客讲述了android的事件分发机制和处理流程机制,但是看过千遍,总还是觉得有些迷迷糊糊,因此特地抽出一天事件来亲测下,向像我一样的广大入门程序员详细讲述an ...

  5. Android Studio 插件开发详解二:工具类

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78112856 本文出自[赵彦军的博客] 在插件开发过程中,我们按照开发一个正式的项 ...

  6. 《Android游戏开发详解》一2.16 区分类和对象

    本节书摘来异步社区<Android游戏开发详解>一书中的第2章,第2.16节,作者: [美]Jonathan S. Harbour 译者: 李强 责编: 陈冀康,更多章节内容可以访问云栖社 ...

  7. Android Framework系统服务详解

    Android Framework系统服务详解 操作环境 系统:Linux (Ubuntu 12.04) 平台:高通 Android版本:5.1 PS: 符号...为省略N条代码 一.大致原理分析 A ...

  8. android屏幕适配详解

    android屏幕适配详解 官方地址:http://developer.android.com/guide/practices/screens_support.html 一.关于布局适配建议 1.不要 ...

  9. Android LiveData组件详解以及LiveDataBus

    转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/99749323 本文出自[赵彦军的博客] 一.LiveData简介 LiveDat ...

最新文章

  1. 一站式学习Wireshark
  2. 不吹不黑,中美程序员的区别对比!
  3. python IO编程-StringIO和BytesIO
  4. G-华华对月月的忠诚
  5. 软件架构设计的六大原则
  6. spring cloud报错解决:java.lang.ClassNotFoundException: com.netflix.servo.monitor.Monitors
  7. 数据库 索引超出了数组界限
  8. 25%的CPU利用率也能够让一台笔记本如此狼狈 (小红伞)
  9. 怎样让网站显示在 Google 搜索结果中?
  10. webpack5 入门学习笔记(四)性能优化
  11. 2022年全球与中国石油和天然气固井服务行业发展趋势及投资战略分析报告
  12. EMI辐射发射超标案例
  13. HikariDataSource 配置详解
  14. 基于Hardhat和Openzeppelin开发可升级合约(二)
  15. java 创建manifest文件_jar Manifest例子如何将Manifest文件添加到jar文件中
  16. 数字标签转化为one-hot形式的tensor
  17. Prescan 8.5.0 许可证过期(Could not checkout a valid license)
  18. python视频教程全集-Python视频教程全集带你入门
  19. mac移动硬盘初始化
  20. 仿网易云音乐源码html5

热门文章

  1. 评价RA滑膜炎的综合评分系统的计量学特点: 来自一项随机、前瞻、多中心研究的结果...
  2. 3.Open3D教程——点云数据操作
  3. 阿里云现代农业园区解决方案,智慧园区大数据、产品管理、物联网、企业管理平台解决方案
  4. 《Vue.js技术内幕》读后感
  5. 为什么社交APP已经这么多了,他们还要耗巨资做
  6. C语言int型数据范围
  7. php手动起事物和自动的区别,手动挡和自动挡哪个好 有什么区别
  8. 独家 | 揭底!BiYong被爆严重用户隐私安全漏洞!
  9. 《哈利·波特:霍格沃茨之谜》游戏特推出万圣节内容致敬黑魔法
  10. 化工厂人员定位应考虑哪些因素?