Android 线程池

1.线程池
线程池就是存放和管理线程的池子。
在做耗时操作时都会创建一个子线程,当大量的耗时操作产生,就会大量的创建和销毁线程,因此可能会造成过大的性能开销。而且当大量的线程一起运作时,因为线程底层的机制是切分CPU时间,大量的线程同时存在可能造成互相抢占资源的现象发生,造成资源紧张,从而导致阻塞。
适当的使用线程池可以很好的解决这些问题。

Android线程池的概念来源于Java的Executor,Executor是一个接口,真正的线程池的实现是ThreadPoolExecutor,它提供了一系列参数来配置线程池,通过不同的参数可以创建不同的线程池。

线程池的优点:
①复用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
②有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
③对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。

线程池的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue< Runnable> workQueue,
ThreadFactory threadFactory)
线程池中存在核心线程与非核心线程,核心线程一旦创建会一直执行任务或等待任务到来,而非核心线程只在任务队列塞满任务时去执行多出的任务,并且非核心线程在等待一段时间后将会被回收,这个时间作为参数可调配。
注意:线程池不需要重复创建,过多创建线程池容易发生资源泄露,因此可以使用单例模式创建一次即可。线程池的线程数理论上满足2*cpu核心数+1的时候性能最佳。
在这里插入图片描述
详细说明一下各个参数的具体含义:
①CorePoolSize:线程池的核心线程数
默认情况下,核心线程会一直存活,即使它们处于闲置状态。但是如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么核心线程就会存在超时策略,这个时间间隔由keepAliveTime决定,当等待时间超过keepAliveTime时长后,核心线程就会被停止。
②maximumPoolSize:线程池能容纳的最大线程数
当活动线程数达到这个数值后,后续的新任务将会被阻塞。
③keepAliveTime:非核心线程闲置时的超时时长
超过这个时长,非核心线程就会被回收,当线程池的allowCoreThreadTimeOut属性设置为True时,keepAliveTime同样会作用于核心线程。
④unit:keepAliveTime参数的时间单位
⑤workQueue:线程池中的任务队列
通过线程池execute方法提交的Runnable对象会存储在该队列中。该任务队列是BlockingQueue类型,属于阻塞队列,即队列为空时取出任务的操作会被阻塞,只有队列不为空时才能进行取出操作,而在满队列时添加操作会被阻塞。
⑥threadFactory:线程工厂
作用是为线程池创建新线程。ThreadFactory是一个接口,它只有一个方法newThread(Runnable r)用来创建线程。
ThreadFactory factory =new ThreadFactory() {
private final AtomicInteger mCount =new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, “new Thread #” + mCount.getAndIncrement());
}
};

2.线程池的使用
当调用ThreadPoolExecutor.execute(runnable)时会进行以下判断(这里不考虑延时任务):
①如果线程池中运行的线程数少于核心线程数,就新建一个线程,并执行该任务。
②如果线程池中运行的线程数大于等于核心线程数,则将任务添加到待执行队列中,等待执行;
③如果第二步添加到队列失败,就新建一个非核心线程,并在该线程执行任务;
④如果当前线程数已经达到最大线程数,就拒绝这个任务,抛出异常。

示例:
①创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,1, TimeUnit.SECONDS , new LinkedBlockingQueue< Runnable>(100));
②向线程池中提交任务
for(int i = 0;i<30;i++){
final int finali = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d(TAG, “run: " + finali +”,当前线程为", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
threadPoolExecutor.execute(runnable);
}
结果会每2s打印三个日志。
这个demo中设置的任务队列长度为100,所以不会开启额外的5-3=2个非核心线程。如果将任务队列设为25,则前三个任务被核心线程执行,剩下的30-3=27个任务进入队列会满,此时会开启2个非核心线程来执行剩下的两个任务。

注:execute一个线程之后,如果线程池中的线程数未达到核心线程数则启用一个核心线程去执行;如果线程池中的线程数已经达到核心线程数且等待队列未满,则将新任务放入队列中等待执行;如果线程池中的线程数已经达到核心线程数但未超过非核心线程数且等待队列已满,则开启一个非核心线程来执行任务;如果线程池中的线程数已经超过非核心线程数,则拒绝执行该任务,采取饱和策略,抛出RejectedExecutionException异常。

3.线程池的分类
Android中有四类不同功能特性的线程池:
①FixedThreadPool:只有核心线程,并且超时时间为0(即无超时时间),所以不会被回收。
Executors.java:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue< Runnable>());
}
这是一种数量固定的线程池,且当线程处于空闲时也不会被回收,除非线程池被关闭。
当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来。
由于FixedThreadPool中只有核心线程并且这些核心线程不会被回收,所以它能够更快速地响应外界的请求。

使用举例:
final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
for(int i = 0;i<30;i++){
final int finali = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d(TAG, "run: "+finali + “,当前线程为”,Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
fixedThreadPool.execute(runnable);
}
结果为每2s打印5个任务。
在这里插入图片描述
②CacheThreadPool:没有核心线程,并且最大线程数为int的最大值,超时时间为60s
public static ExecutorService newCacheThreadPool(){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L,TimeUnit.SECONDS, new SynchronousQueue< Runnable>());
}
这是一种线程数量不定的线程池,只有非核心线程,并且其最大线程数为Integer.MAX_VALUE(即线程池的线程数量可以无限大)。
当线程池中所有线程都处于活动状态时,线程池会创建新的线程来处理新任务,否则就会复用空闲线程来处理。
注意:这个线程池的等待队列是同步阻塞队列,队列中没有任何容量,这个队列可以理解为无法储存的队列,只有在可以取出的情况下才会向其内添加任务。
CacheThreadPool比较适合执行大量的、耗时较少的任务。当所有线程都处于闲置状态时,线程池中的线程都会超时而被停止,这时CacheThreadPool几乎不占任何系统资源。

使用举例:
final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for(int i = 0;i<30;i++){
final int finali = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d(“Thread”, "run: "+finali);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
cachedThreadPool.execute(runnable);
}
结果:过2s后直接打印30个任务。
结果分析:
(1)SynchronousQueue不存储元素,每次插入操作必须伴随一个移除操作,一个移除操作也要伴随一个插入操作。
(2)当一个任务执行时先用SynchronousQueue的offer提交任务,如果线程池中有线程空闲则调用SynchronousQueue的poll方法来移除任务并交给线程处理;如果没有线程空闲则开启一个新的非核心线程来处理任务。
(3)由于maximumPoolSize是无界的,所以如果线程处理任务速度小于提交任务的速度,则会不断地创建新的线程,这时要注意不要过度创建,应采取措施调整双方速度,不然线程创建太多会影响性能。
在这里插入图片描述
③ScheduledThreadPool:核心线程数固定,非核心线程无限大,非核心线程数超时时间为10s
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSzie) {
return new ScheduledThreadPoolExecutor( corePoolSzie);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());
}
它的核心线程数量是固定的,非核心线程数没有限制,并且当非核心线程闲置时会被立即回收。DelayedWorkQueue队列是包装过的DelayedQueue,这个类的特点是在存入时会有一个Delay对象一起存入,代表需要过多少时间才能取出,相当于一个延时队列。
ScheduledThreadPool这类线程池主要用于执行定时任务和具有固定周期的重复任务。

使用举例:
final ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.d(TAG, “This task is delayed to execute”);
}
};
scheduledThreadPool.schedule(runnable,10, TimeUnit.SECONDS);//延迟启动任务
//延迟5s后启动,每1s执行一次 //scheduledThreadPool.scheduleAtFixedRate ( runnable,5,1,TimeUnit.SECONDS);
//启动后第一次延迟5s执行,后面延迟1s执行 //scheduledThreadPool.scheduleWithFixedDelay(runnable,5,1,TimeUnit.SECONDS);
在这里插入图片描述
④SingleThreadExecutor:只有一个核心线程,并且无超时时间
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorSe rvice(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue< Runnable>()));
}
内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。
SingleThreadExecutor的意义在于统一外界所有任务到一个线程,这使得这些任务之间不需要处理线程同步的问题。

使用举例:
final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for(int i = 0;i<30;i++){
final int finali = i;x
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d(“Thread”, "run: "+finali);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
singleThreadExecutor.execute(runnable);
}
结果:每2s打印一个任务,由于只有一个核心线程,当被占用时,其他的任务需要进入队列等待。
在这里插入图片描述

5.线程池的源码解析
线程池执行有两个方法:submit()和execute(),这两个方法本质的含义是一样的。
在这里插入图片描述
从上图可以看出,submit()其实还是需要调用execute()去执行任务,而submit()和execute()本质上的不同是submit()将包装好的任务进行了返回。
submit()方法:
public < T> Future< T> submit(Callable< T> task){
if (task == null)
throw new NullPointerException();
RunnableFuture< T> ftask = newTaskFor(task);
execute(ftask); //还是通过调用execute
return ftask; //最后将包装好的Runable返回
}

//将Callable< T> 包装进FutureTask中
protected < T> RunnableFuture< T> newTaskFor(Callable< T> callable) {
return new FutureTask< T>(callable);
}

//FutureTask也是实现Runnable接口,因为RunableFuture本身就继承了Runnabel接口
public class FutureTask< V> implements RunnableFuture< V> {

}

public interface RunnableFuture< V> extends Runnable, Future< V> {
void run();
}

execute()方法:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//获得当前线程的生命周期对应的二进制状态码
int c = ctl.get();
//判断当前线程数量是否小于核心线程数量,如果小于就直接创建核心线程执行任务,创建成功直接跳出,失败则接着往下走.
if (workerCountOf© < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}

//判断线程池是否为RUNNING状态,并将任务添加至队列中.
if (isRunning© && workQueue.offer(command)) {
int recheck = ctl.get();
//审核下线程池的状态,如果不是RUNNING状态,直接移除队列中
if (! isRunning(recheck) && remove(command))
reject(command);
//如果当前线程数量为0,则单独创建线程,而不指定任务.
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果不满足上述条件,尝试创建一个非核心线程来执行任务,如果创建失败,调用reject()方法
else if (!addWorker(command, false))
reject(command);
}
下图是execute()方法的基本流程:
在这里插入图片描述
从execute()方法中能看出,addWorker()方法是创建线程(核心线程、非核心线程)的主要方法,而reject()就是线程创建失败的一个回调。

reject()方法:
看一下reject()方法,这里就是通过上述的Handler将通知发出去,然后针对不同类型的RejectedExecutionHandler,进行不同的处理。
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}

下面着重看下创建线程的方法:
addWorker()方法:
参数 :
Runnable firstTask:传递进来需要执行的任务,也可以设置为null(在SHUTDOWN情况下,单纯的创建线程来执行任务)。
boolean core:需要创建的线程是否需要是核心线程。
private boolean addWorker(Runnable firstTask, boolean core) {
//类似goto,是Java的标识符,在这里出现是为了防止在多线程的情况下,compareAndIncrementWorkerCount(),计算线程池状态出现问题,而设立重试的关键字.
retry:
for ( ; ; ) {
int c = ctl.get();
int rs = runStateOf©;

//看似判断条件很麻烦,分拆后主要两点,线程已经处于STOP或者即将STOP的状态;或者处于SHUTDOWN状态,并且传递的任务为null,此时队列不为空还需要增加线程,除了这种情况,其他情况都不需要增加线程
//以上的情况就不需要
if (rs >= SHUTDOWN && ! (rs== SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false;

//判断当前工作线程数量是否超过最大值,或者当前工作线程数量超过核心线程数或者最大线程数,这个值根据第二个布尔变量决定
for ( ; ; ) {
int wc = workerCountOf©;
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;

//这段函数是判断线程池状态的统计更新成没成功,如果成功直接跳出这个循环,继续执行
if (compareAndIncrementWorkerCount©)
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf© != rs)
continue retry;
//如果不成功则跳到外层循环入口,重新执行
retry inner loop
}
}
//下面是创建线程的过程,并且在创建线程的过程中加锁。Worker就是线程的一个包装类。这里分别对线程的创建成功和失败分别做出了处理.
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//创建线程的过程中加锁防止并发现象发生.
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
//从这里可以看出线程池创建线程,只会在两种情况下创建:1.线程池在RUNNING状态(rs<SHUTDOWN);2.线程池处于SHUTDOWN状态,并且任务为null,但是此时任务队列不为空,需要继续增加线程来加快处理进度.
if (rs < SHUTDOWN || (rs== SHUTDOWN && firstTask == null)) {
//在这里就是先检查下Thread状态,防止意外发生.
if (t.isAlive())
throw new IllegalThreadStateException();
workers.add(w);
//这里做了一个容量的判断
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//如果线程已经增加成功,然后设置标志
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
//最后如果线程没有开始,就分发到添加线程失败,通过标志位来判断线程是否被添加成功.
if (! workerStarted)
addWorkerFailed(w);
}
//如果添加成功就返回true,否则添加失败就返回false.
return workerStarted;
}
addWorker()方法的注意事项:
①增加一个线程,并且会为其绑定core或者maximum的线程标志。
②如果成功添加线程来执行当前任务,那么当前线程池的状态会被刷新.
③在添加第一个任务firstTask的这种情况下,新的工作线程会被创建后立即执行任务。
④该方法会在线程池STOP状态或者符合资格去关闭会返回false。
⑤线程工厂创建线程失败的时候,同样也会返回false.
⑥在由于线程创建失败,线程工厂返回的线程为null,或者发生异常(通常由于在线程执行的过程中发生了OOM),线程池会进行回滚操作。
在这里插入图片描述
addWorker()方法执行的几个阶段:
①第一阶段 – 状态检查:
在创建线程时,首先检查线程池状态,防止线程处于STOP、TIDYING、TERMINATED状态,如果处于上述状态直接返回false。
然后对于在SHUTDOWN状态下,只有当前任务队列不为空,并且传递的任务参数为null。这种状态下可以创建线程来执行剩余任务,除此之外全部直接返回false。
if (rs >= SHUTDOWN &&! (rs== SHUTDOWN && firstTask == null &&! workQueue.isEmpty()))
return false;
②第二阶段 :
判断当前线程池能否创建线程以及可以创建之后的数量添加校验。
(1)当前线程的数量是否超过线程池的最大容量,以及根据core参数来判断是否超过设置的核心线程数和最大线程数。
(2)通过第一步之后就可以创建线程,这里需要用到compareAndIncrementWorkerCount()通过原子操作来更新线程池的线程数量变化,如果变化数量失败,这里有一个重试机制,这个retry关键字就是来完成这个操作。
(3)这里注明下CAPACITY这个常量就是线程池的线程数量的极限。
for ( ; ; ) {
int wc = workerCountOf©;
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount©)
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf© != rs)
continue retry;
}
③第三阶段 – 创建线程
通过上述阶段,那么就可以创建线程了,这里设置了两个初始的标志位,来判断被创建线程的状态。
boolean workerStarted = false;
boolean workerAdded = false;
如果最终线程创建并添加成功,则返回true,如果线程最终没有被运行,则调用addWorkerFailed()方法。

addWorkedFailed()方法:
在addWorker()方法中,如果线程创建之后,没有最终运行(workerStarted=false)这时候会调用addWorkedFailed()方法。
//回滚工作线程的创建操作:1.如果线程的包装类Worker存在,就将其remove掉。 2.remove掉添加线程失败的Worker,需要刷新当前工作线程的数量。3.尝试终止操作,并且终止这个线程的操作.
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w);
decrementWorkerCount();
//尝试停止操作.
tryTerminate();
} finally {
mainLock.unlock();
}
}

tryTerminate()方法:
在addWorkedFailed()方法中,我们发现除了回滚操作,它还调用了tryTerminate()方法,尝试着去停止线程池。因为线程池创建线程失败一般由于异常引起(或OOM)。所以这时候需要让线程池进行停止操作。
注意事项:
如果发生以下两种情况,使用该方法将会将线程池转换为终止状态(TERMINATED):
1.SHUTDOWN状态下,队列为空的情况下.
2.STOP状态下.
如果符合上述条件,可以转换终止状态时,这时会中断当前线程池内空闲的线程,以确保终止的信号的传递。
final void tryTerminate() {
for ( ; ; ) {
int c = ctl.get();
//检测当前是RUNNING状态,或者已经停止(TERMINATED)的状态,或者SHUTDOWN状态下,队列不为空.
if (isRunning© || runStateAtLeast(c, TIDYING) || (runStateOf© == SHUTDOWN && ! workQueue.isEmpty()))
return;

//如果工作线程的数量不为空,这时候需要处理空闲线程,这里只中断一个其中一个线程,这里博主认为是将线程池的状态由SHUTDOWN向STOP状态过渡的信号.
if (workerCountOf© != 0) {
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//设置当前的线程池状态为TIDYING,如果设置失败,还会进入循环直到设置成功.
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//停止方法的空实现
terminated();
} finally {
//最终线程池会设置为停止状态
ctl.set(ctlOf(TERMINATED, 0));
//设置可重新入锁的标志,将被锁隔离的在外等待的所有线程唤醒.
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
}
}

interruptIdleWorkers()方法:
而在tryTerminate()方法中,这里中断线程的操作就是由interruptIdleWorkers()方法进行的.
这个方法作用很明确,就是设置线程中断操作的方法,唯一注意的地方就是参数onlyOne:
如果为true,只中断工作线程中的一个线程.
如果为false,中断所有的工作线程。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
//检查线程的状态
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
//如果onlyOne参数为True,则只执行一次就跳出.
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}

shutdown()方法:
而中断所有空闲的线程方法则是shutdown()方法,它的核心方法还是调用interruptIdleWorkers()方法。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//校验线程的状态
checkShutdownAccess();
//设置线程池状态为SHUTDOWN
advanceRunState(SHUTDOWN);
//中断所有空闲进程.调用的interruptIdleWorkers(false);
interruptIdleWorkers();
//需要自己实现,在中断所有线程可定制的操作
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
注意事项:
①在shutdown()执行时可以让现有的任务被执行,但是新的任务不在会被处理.
②如果已经是SHUTDOWN状态,那么继续调用不会产生任何效果.
③shutdown()方法只会中断空闲的线程,但是不会影响到已经存入队列的任务,如果需要停止线程池的运行,可以使用awaitTermination()方法.

awaitTermination()方法:
阻塞方法,强行等待当前队列中的任务全部为TERMINATED状态,可以设置超时时间.
参数:d
timeout —- 设置超时时间
unit —- 设置超时时间的单位
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
//设置时间
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//这是死循环,当线程池的状态为TERMINATED时,跳出循环返回true,也就是所有任务都完成.否则超时或者线程中断则返回false.
while (!runStateAtLeast(ctl.get(), TERMINATED)) {
if (nanos <= 0L)
return false;
nanos = termination.awaitNanos(nanos);
}
return true;
} finally {
mainLock.unlock();
}
}

6.线程池其它方法:
①shutDown() 关闭线程池,不影响已经提交的任务
②shutDownNow() 关闭线程池,并尝试去终止正在执行的线程
③allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收
④submit 一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值。
⑤beforeExecute() - 任务执行前执行的方法
⑥afterExecute() -任务执行结束后执行的方法
⑦terminated() -线程池关闭后执行的方法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值