OKhttp3源码阅读

 

网上关于Okhttp源码的文章很多,但是大多是人家自己的理解,而且不是很全面,结合很多文章以及自己阅读源码后的思考,也写一篇笔记

有大佬说如果读源码不去分析源码背后的设计模式或设计思想,那么读源码的意义不大。 同时,如果熟悉的设计模式越多,那么读某个框架的源码的时候就越容易,两者是相辅相成的。

要阅读一个框架的源码,首先要熟悉他的主流程,从主流程开始一层层剥开他的面纱。

那OKhtt3的主流程是什么?个人认为是这样的:

流程图翻译为文字就是

客户层的OkHttpClient ,在发送网络请求,执行层决定怎么处理请求,比如同步还是异步,同步请求的话直接在当前线程完成请求, 请求要经过多层拦截器处理; 如果是异步处理,需要 Dispatcher 执行分发策略, 线程池管理执行任务; 又比如,一个请求下来,要不要走缓存,如果不走缓存,进行网络请求。最后执行层将从连接层进行网络 IO 获取数据

我们先从主流程开始分析

OkHttpClient, 可以通过 new OkHttpClient() 或 new OkHttpClient.Builder() 来创建对象, 但是—特别注意, OkHttpClient() 对象最好是共享的, 建议使用单例模式创建。 因为每个 OkHttpClient 对象都管理自己独有的线程池和连接池。

 

我们在newCall的时候实际上调用的方法是Call的实现类RealCall

 /**调用了Call接口的实现类RealCall**/
 @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }


//下面是RealCall的源码

/**构造方法**/
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }
/**新建一个请求实例**/
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;
  }

RealCall里面通过构造方法封装了请求连接和拦截器(This interceptor recovers from failures and follows redirects as necessary)

就是说我们在发起请求的时候,会有重试机制和重定向机制

 

另外又得说说这个EventListener

eventListener就是一个抽象类,里面提供了一些回调方法,比如callStart(),callFailed(),好像没啥设计模式~~

/**EventListener 源码**/
public abstract class EventListener {
  public static final EventListener NONE = new EventListener() {
  };

  static EventListener.Factory factory(final EventListener listener) {
    return new EventListener.Factory() {
      public EventListener create(Call call) {
        return listener;
      }
    };
  }

  public void callStart(Call call) {
  }


  public void dnsStart(Call call, String domainName) {
  }

 
  public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
  }

 
  public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
  }

 
  public void secureConnectStart(Call call) {
  }

 
  public void secureConnectEnd(Call call, @Nullable Handshake handshake) {
  }

 
  public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
      @Nullable Protocol protocol) {
  }

 
  public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
      @Nullable Protocol protocol, IOException ioe) {
  }

 
  public void connectionAcquired(Call call, Connection connection) {
  }

  public void connectionReleased(Call call, Connection connection) {
  }

 
  public void requestHeadersStart(Call call) {
  }


  public void requestHeadersEnd(Call call, Request request) {
  }

 
  public void requestBodyStart(Call call) {
  }


  public void requestBodyEnd(Call call, long byteCount) {
  }

 
  public void responseHeadersStart(Call call) {
  }

  public void responseHeadersEnd(Call call, Response response) {
  }

 
  public void responseBodyStart(Call call) {
  }

 
  public void responseBodyEnd(Call call, long byteCount) {
  }

  public void callEnd(Call call) {
  }

  public void callFailed(Call call, IOException ioe) {
  }

  public interface Factory {
  
    EventListener create(Call call);
  }
}

再返回Call这个初始接口,提供了一些创建对象的接口和方法,但由子类决定实例化,明显的工厂模式

public interface Call extends Cloneable {
  Request request();

  Response execute() throws IOException;

  void enqueue(Callback responseCallback);

  void cancel();

  boolean isExecuted();

  boolean isCanceled();

  Call clone();

  interface Factory {
    Call newCall(Request request);
  }
}

从OkHttpClient的源码可以看出,其中使用到了建造者模式

public Builder newBuilder() {
    return new Builder(this);
  }

  public static final class Builder {
    Dispatcher dispatcher;
    @Nullable Proxy proxy;
    List<Protocol> protocols;
    List<ConnectionSpec> connectionSpecs;
    final List<Interceptor> interceptors = new ArrayList<>();
    final List<Interceptor> networkInterceptors = new ArrayList<>();
    EventListener.Factory eventListenerFactory;
    ProxySelector proxySelector;
    CookieJar cookieJar;
    @Nullable Cache cache;
    @Nullable InternalCache internalCache;
    SocketFactory socketFactory;
    @Nullable SSLSocketFactory sslSocketFactory;
    @Nullable CertificateChainCleaner certificateChainCleaner;
    HostnameVerifier hostnameVerifier;
    CertificatePinner certificatePinner;
    Authenticator proxyAuthenticator;
    Authenticator authenticator;
    ConnectionPool connectionPool;
    Dns dns;
    boolean followSslRedirects;
    boolean followRedirects;
    boolean retryOnConnectionFailure;
    int connectTimeout;
    int readTimeout;
    int writeTimeout;
    int pingInterval;

    public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
    }

    Builder(OkHttpClient okHttpClient) {
      this.dispatcher = okHttpClient.dispatcher;
      this.proxy = okHttpClient.proxy;
      this.protocols = okHttpClient.protocols;
      this.connectionSpecs = okHttpClient.connectionSpecs;
      this.interceptors.addAll(okHttpClient.interceptors);
      this.networkInterceptors.addAll(okHttpClient.networkInterceptors);
      this.eventListenerFactory = okHttpClient.eventListenerFactory;
      this.proxySelector = okHttpClient.proxySelector;
      this.cookieJar = okHttpClient.cookieJar;
      this.internalCache = okHttpClient.internalCache;
      this.cache = okHttpClient.cache;
      this.socketFactory = okHttpClient.socketFactory;
      this.sslSocketFactory = okHttpClient.sslSocketFactory;
      this.certificateChainCleaner = okHttpClient.certificateChainCleaner;
      this.hostnameVerifier = okHttpClient.hostnameVerifier;
      this.certificatePinner = okHttpClient.certificatePinner;
      this.proxyAuthenticator = okHttpClient.proxyAuthenticator;
      this.authenticator = okHttpClient.authenticator;
      this.connectionPool = okHttpClient.connectionPool;
      this.dns = okHttpClient.dns;
      this.followSslRedirects = okHttpClient.followSslRedirects;
      this.followRedirects = okHttpClient.followRedirects;
      this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
      this.connectTimeout = okHttpClient.connectTimeout;
      this.readTimeout = okHttpClient.readTimeout;
      this.writeTimeout = okHttpClient.writeTimeout;
      this.pingInterval = okHttpClient.pingInterval;
    }

其中new Builder()里面指定了Dispatcher (管理线程池)、链接池、超时时间等。

接下来看看Dispatcher里面有什么

简单来讲,这个类就是用来调度你的请求,里面定义了最大并发请求数,初始值为64,作为一个成熟的框架,里面有get,set方法可以修改和获取这个值的

private int maxRequests = 64;

public synchronized void setMaxRequests(int maxRequests) {
    if (maxRequests < 1) {
      throw new IllegalArgumentException("max < 1: " + maxRequests);
    }
    this.maxRequests = maxRequests;
    promoteCalls();
}

 public synchronized int getMaxRequests() {
    return maxRequests;
  }

然后也初始化了一个参数叫做  private int maxRequestsPerHost = 5;也是有get,set方法,就是每个主机最高并发请求数

 /**
   * Set the maximum number of requests for each host to execute concurrently. This limits requests
   * by the URL's host name. Note that concurrent requests to a single IP address may still exceed
   * this limit: multiple hostnames may share an IP address or be routed through the same HTTP
   * proxy.
   *
   * <p>If more than {@code maxRequestsPerHost} requests are in flight when this is invoked, those
   * requests will remain in flight.
   *
   * <p>WebSocket connections to hosts <b>do not</b> count against this limit.
   */
  public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
    if (maxRequestsPerHost < 1) {
      throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
    }
    this.maxRequestsPerHost = maxRequestsPerHost;
    promoteCalls();
  }

  public synchronized int getMaxRequestsPerHost() {
    return maxRequestsPerHost;
  }

这里借用一下阿里一位大神的图

接下来是这个类的重点

 private @Nullable Runnable idleCallback;

  /** Executes calls. Created lazily. */
  private @Nullable ExecutorService executorService;

  /** 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<>();
idleCallback其实是做一个线程的管理,一旦网络请求完毕都会把当前并发的线程回归为闲散线程

这一点在其他文章中最多提到这个finish方法但是没过多说为什么

然后是下面三个双向队列Deque容器

RealCall中 有两个重要的方法 execute() 和 enqueue(Callback responseCallback)。 execute() 是直接在当前线程执行请求,enqueue(Callback responseCallback) 是将当前任务加到任务队列中,执行异步请求,同步请求,readyAsyncCalls这个队列就是按顺序添加网络请求到队列,便于统一管理,具体调用看图

runningAsyncCalls是运行的异步调用队列。包括尚未完成的或者已经取消的
runningSyncCalls是运行的同步调用队列。包括尚未完成的或者已经取消的

这两个同理,就不一一截图了,整体流程个人觉得是这样

另外说一点,LinkedList是实现这个Deque接口的也是双向队列,可以在队列首尾进行add或者remove等操作,但是LinkedList还实现了List接口,因此他具备list的一些特性,从性能上来讲Deque的性能比LinkedList高三倍左右,这里估计主要是从性能上考虑而使用的Deque,然后就是先进先出机制,先执行请求完成或者取消的任务,可以直接从队列remove掉

 

接下来看看Interceptor

在 OkHttp3 的拦截器链中, 内置了5个默认的拦截器,分别用于重试、请求对象转换、缓存、链接、网络读写。如图(图片来自网络)

 

首先看一下重试机制:retryAndFollowUpInterceptor 

简单来说就是拦截器在处理网络请求过程如抛出异常,首先如果可恢复则重试,否则跳出循环。其次如果没什么异常则校验下返回状态、代理鉴权、重定向等,如果需要重定向则继续,否则直接跳出循环返回结果。最后如果重定向,则要判断下是否已经达到最大可重定向次数, 达到则抛出异常,跳出循环

个人自己写了个重试机制的代码,大体上如下

 //创建延迟线程
    public Runnable mDelayThread = new Runnable() {
        HttpTask mHttpTask = null;

        @Override
        public void run() {
            while (true) {
                try {
                    mHttpTask = mDelayQueue.take();
                    if (mHttpTask.getRetryCount() < 3) {
                        mThreadPoolExecutor.execute(mHttpTask);
                        mHttpTask.setRetryCount(mHttpTask.getRetryCount() + 1);
                        Log.e("DHD", "---------重试次数---------" + mHttpTask.getRetryCount());
                    }else {
                        Log.e("DHD", "---------重试超出次数限制---------");
                        return;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mThreadPoolExecutor.execute(mHttpTask);
            }
        }
    };

BridgeInterceptor

一个实现应用层和网络层直接的数据格式编码的桥。 第一: 把应用层客户端传过来的请求对象转换为 Http 网络协议所需字段的请求对象。 第二, 把下游网络请求结果转换为应用层客户所需要的响应对象。 这个设计思想来自适配器设计模式

CacheInterceptor 

实现了数据的选择策略, 来自网络还是来自本地? 这个场景也是比较契合策略模式场景,CacheInterceptor 根据这个策略去选择走网络数据还是本地缓存。

缓存的策略过程:
1、 请求头包含 “If-Modified-Since” 或 “If-None-Match” 暂时不走缓存
2、 客户端通过 cacheControl 指定了无缓存,不走缓存
3、客户端通过 cacheControl 指定了缓存,则看缓存过期时间,符合要求走缓存。
4、 如果走了网络请求,响应状态码为 304(只有客户端请求头包含 “If-Modified-Since” 或 “If-None-Match” ,服务器数据没变化的话会返回304状态码,不会返回响应内容), 表示客户端继续用缓存。

OkHttp3 内部缓存默认实现是使用的 DiskLruCache,参考郭霖大神的文章

 

ConnectInterceptor 

负责打开连接; CallServerIntercerceptor 是核心连接器链上的最后一个连接器,负责从当前连接中写入和读取数据。看源码


/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @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);
  }
}

单独看 ConnectInterceptor 的代码很简单,不过连接正在打开的过程需要看看 streamAllocation.newStream(client, doExtensiveHealthChecks),内部执行过程。还需要看看 StreamAllocation 这个类的作用。。。(后续再补)

还一个比较重要的ConnectionPool

主要是管理HTTP和HTTP/2连接的重用,以减少网络延迟。共享相同的HTTP请求,此类实现了一个策略,该策略的连接将保持开放以供将来使用。代码实现和前面的Dispatcher类似,有兴趣的可以自己看看

public final class ConnectionPool {
  /**
   * Background threads are used to cleanup expired connections. There will be at most a single
   * thread running per connection pool. The thread pool executor permits the pool itself to be
   * garbage collected.
   */
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;

  /**
   * Create a new connection pool with tuning parameters appropriate for a single-user application.
   * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
   * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
   */
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

  /** Returns the number of idle connections in the pool. */
  public synchronized int idleConnectionCount() {
    int total = 0;
    for (RealConnection connection : connections) {
      if (connection.allocations.isEmpty()) total++;
    }
    return total;
  }

  /**
   * Returns total number of connections in the pool. Note that prior to OkHttp 2.7 this included
   * only idle connections and HTTP/2 connections. Since OkHttp 2.7 this includes all connections,
   * both active and inactive. Use {@link #idleConnectionCount()} to count connections not currently
   * in use.
   */
  public synchronized int connectionCount() {
    return connections.size();
  }

  /**
   * Returns a recycled connection to {@code address}, or null if no such connection exists. The
   * route is null if the address has not yet been routed.
   */
  @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

  /**
   * Replaces the connection held by {@code streamAllocation} with a shared connection if possible.
   * This recovers when multiple multiplexed connections are created concurrently.
   */
  @Nullable Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, null)
          && connection.isMultiplexed()
          && connection != streamAllocation.connection()) {
        return streamAllocation.releaseAndAcquire(connection);
      }
    }
    return null;
  }

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

  /**
   * Notify this pool that {@code connection} has become idle. Returns true if the connection has
   * been removed from the pool and should be closed.
   */
  boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewStreams || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }

  /** Close and remove all idle connections in the pool. */
  public void evictAll() {
    List<RealConnection> evictedConnections = new ArrayList<>();
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();
        if (connection.allocations.isEmpty()) {
          connection.noNewStreams = true;
          evictedConnections.add(connection);
          i.remove();
        }
      }
    }

    for (RealConnection connection : evictedConnections) {
      closeQuietly(connection.socket());
    }
  }

  /**
   * Performs maintenance on this pool, evicting the connection that has been idle the longest if
   * either it has exceeded the keep alive limit or the idle connections limit.
   *
   * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
   * -1 if no further cleanups are required.
   */
  long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

  /**
   * Prunes any leaked allocations and then returns the number of remaining live allocations on
   * {@code connection}. Allocations are leaked if the connection is tracking them but the
   * application code has abandoned them. Leak detection is imprecise and relies on garbage
   * collection.
   */
  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<StreamAllocation>> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      Reference<StreamAllocation> reference = references.get(i);

      if (reference.get() != null) {
        i++;
        continue;
      }

      // We've discovered a leaked allocation. This is an application bug.
      StreamAllocation.StreamAllocationReference streamAllocRef =
          (StreamAllocation.StreamAllocationReference) reference;
      String message = "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?";
      Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }
}

另外okhttp3最新的源码已经是kt写的了~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值