ios多线程详解

一、基本概念

  • 进程:在系统中运行的一个应用程序就是一个进程,每个进程之间是相互独立的,每个进程均运行在其专用且受保护的内存空间内
  • 线程:一个进程的所有任务都是在线程中执行的,每个进程都至少有一个线程(主线程),同一线程的代码是顺序执行的
  • 多线程:一个进程可以开启多条线程,多条线程可以并行执行不同的任务,多线程的并行执行任务其实是CPU在多条线程之间切换调度
    优点
    适当的提高了程序执行效率和资源的利用率,当线程执行完所有任务时会自动销毁;
    缺点
    1.开启线程需要占用一定的内存空间;(开启一个线程需要占用512KB)
    2.开启大量线程占用过多内存,CPU在调用线程中开销过大,降低程序性能;
    3.程序设计更加复杂(如线程之间的通讯、线程间的资源共享等).
  • 主线程:一个进程运行后,默认会开启一条线程,成为主线程或UI线程,其主要作用就是处理UI事件、显示刷新UI界面
  • 加锁:
    加锁能有效的防止因多线程抢夺资源造成的数据安全问题,但锁是非常耗费性能的,开发中尽量不要使用锁,将加锁、抢夺资源的逻辑交给服务器处理,减小移动客户端的压力.
    1)互斥锁:把读和写的操作当成不可分割的部分,也叫做同步锁;

    // 写法
    @synchrosized(self) {
    do something...
    }

    作用:
    1.保证同一时间只有一条线程能访问共享资源,保证线程安全;
    2.当共享数据被一个线程锁定时,另外的线程进入’休眠状态’等待任务执行完毕;当锁定线程任务执行完毕,下个线程会自动唤醒,执行任务;
    2)自旋锁:和互斥锁类似,只是自旋锁不会引起调用者睡眠
    OC 在定义属性的时候用的natomic(原子性)内部存在一把自旋锁,是为setter方法加锁,同一时间只能有一个线程设值;
    作用:
    1.保证同一时间只有一条线程能访问共享资源,保证线程安全;
    2.当共享数据被一个线程锁定时,另外的线程会以死循环的方式等待解锁,一旦访问资源被解锁,下个线程立即执行任务;
    3)自旋锁的效率远高于互斥锁,但自旋锁一直只用CPU,使CPU的效率降低,而且自旋锁的使用非常容易造成死锁。

二、实现方案

1.pthread(C)

  • 一套通用的多线程API
  • 适用于Unix\Linux\Windows等系统
  • 跨平台\可移植
  • 使用难度大
  • 生命周期需要程序员管理

2.NSThread(OC)

  • 使用更加面向对象
  • 简单易用,可直接操作线程对象
  • 生命周期需要程序员管理

优点:简单、易用、轻便
缺点:需要自己管理线程生命周期,线程同步对数据的加锁有一定的系统开销
使用方法:
(1)创建(开启一个子线程):

// 类方法,直接创建线程并且开始运行线程
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

// 对象方法,先创建线程对象,然后再运行线程操作(调用start方法),在运行线程操作前可以设置线程的优先级等线程信息
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

// 参数意义:
// selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。
// target :selector消息发送的对象
// argument:传输给target的唯一参数,也可以是nil

(2)一些类方法:

// 获取当前线程
+ (NSThread*)currentThread;
// 判断当前是否主线程
+ (BOOL)isMainThread;
// 判断当前线程是否多线程
+ (BOOL)isMultiThreaded;
// 暂停执行当前代码,知道某一时刻
+ (void)sleepUntilDate:(NSDate *)date;
// 该方法可传递一个秒级别的参数ti,来暂停执行当前代码所在的线程ti秒。
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 该方法是真正可以让线程退出的方法,在某个线程中调用该方法,该线程就会直接退出,之后的代码就不会再执行,而且要特别指出,该方法如果直接在主线程中调用的话,连主线程都会被直接终止哦,主线程如果都被终止了,那等于整个程序就已经失去活力了,APP都会整个卡死。
+ (void)exit;
// 获取当前线程的优先级
+ (double)threadPriority;
// 通过以上方法可以查看和设置线程优先级,优先级高的线程会优先执行,这里不建议进行此类设置,会打乱系统之前的线程优先级安排。可能造成某些底等级的线程卡顿或者长时间得不到执行。
+ (BOOL)setThreadPriority:(double)p;
// 获取当前线程的调用栈
+ (NSArray *)callStackSymbols;
+ (NSArray *)callStackReturnAddresses;

(3)一些属性

// 是否正在执行
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
// 是否结束
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
// 是否取消
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);

(4)线程间通信方法

// 回到主线程,意思是调用者在主线程执行了某个方法
// waitUntilDone的意思是执行完主线程才执行该语句的后续语句
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes

// 创建子线程任务并执行
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);

3.GCD(C)

  • 旨在替代NSThread等线程技术
  • 充分利用设备的多核
  • 生命周期自动管理
3.1 简介

Grand Central Dispatch (GCD) 是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。
GCD优点:

  • GCD可用于多核的并行运算
  • GCD会自动利用更多的CPU内核(比如双核、四核)
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
3.2 任务和队列

GCD的两个核心概念就是任务和队列。
任务:就是执行的操作,换句话就是要执行的代码。执行任务的两种方式同步执行异步执行,两者的区别就是是否具备开启线程的能力。

  • 同步执行(sync):只能在当前线程中执行,不具备开启新线程的能力
  • 异步执行(async):可以在新线程中执行任务,具备开启新线程的能力

队列:用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则。即新任务总是被插入到队列的队尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务则从队列中释放一个任务,在GCD中有两种队列:串行队列并发队列

  • 串行队列:让任务一个接一个的执行(一个任务执行完毕后再执行下一个),不同串行队列中的任务互不干扰,可以通多创建多个串行队列实现并行执行的效果
  • 并行队列:可以让多个任务同时执行,并发功能只有在异步函数下才有效。并行队列中的任务开始执行的次序遵照其加入队列的次序,但是任务执行的过程都是同步进行的,不需要等待。也就是说,并发队列保证任务开始执行的次序,但是无法知道执行的次序。并行队列的处理量还是要根据当前系统的状态来决定,如果当前系统状态最多处理2个任务,那么任务1和任务2会排在前面,任务3什么时候执行,要看任务1和任务2谁先完成。
3.3 执行原理
并发队列串行队列主队列
同步(sync)没有开启新线程,串行执行任务没有开启新线程,串行执行任务没有开启新线程,串行执行任务
异步(async)有开启新线程,并发执行任务有开启新线程(1条),串行执行任务没有开启新线程,串行执行任务

注:在当前串行队列中,向此队列中添加同步任务会造成死锁。

// 例1
// 当前是主线程
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3

分析:首先执行任务1,然后遇到同步线程,那么他会进入等待,等待任务2执行,然后执行任务3。现在主队列中先是添加了任务1和任务3,然后有添加了任务2,任务2排在任务3后面。队列遵守FIFO(先进先出)的规则,这意味着任务2在任务3执行完成之后才能执行,这样就进入了相互等待的局面,造成死锁。

// 例2

dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
    NSLog(@"2"); // 任务2
    dispatch_sync(queue, ^{  
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5

分析:分析同上,自定的串行队列中的任务3和4相互等待,死锁。

3.4 GCD的使用步骤

GCD的使用步骤只有两步:

  1. 创建一个队列(串行队列或并行队列)
  2. 将任务添加到队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)
3.4.1 队列的创建方法
  • 可以使用dispatch_queue_create来创建对象,需要传入两个参数,第一个参数表示队列的唯一标识符,用于DEBUG,可为空;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
  • 对于并发队列,还可以使用dispatch_get_global_queue来创建全局并发队列。GCD默认提供了全局的并发队列,需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可。
3.4.2 任务的创建方法
// 同步执行任务创建方法
dispatch_sync(queue, ^{
    NSLog(@"%@",[NSThread currentThread]);    // 这里放任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    NSLog(@"%@",[NSThread currentThread]);    // 这里放任务代码
});
3.5 GCD的其他方法
3.5.1 GCD的栅栏方法 dispatch_barrier_async
  • 我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。
- (void)barrier
{
    dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });

    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}
3.5.2 GCD的延时执行方法dispatch_after
  • 当我们需要延迟执行一段代码时,就需要用到GCD的dispatch_after方法。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...
   NSLog(@"run-----");
});
3.5.3 GCD的一次性代码(只执行一次) dispatch_once
  • 我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了GCD的dispatch_once方法。使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});
3.5.4 GCD的快速迭代方法dispatch_apply
  • 通常我们会用for循环遍历,但是GCD给我们提供了快速迭代的方法dispatch_apply,使我们可以同时遍历。比如说遍历0~5这6个数字,for循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个数字。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(6, queue, ^(size_t index) {
    NSLog(@"%zd------%@",index, [NSThread currentThread]);
});
3.5.5 GCD的队列组 dispatch_group
  • 有时候我们会有这样的需求:分别异步执行2个耗时操作,然后当2个耗时操作都执行完毕后再回到主线程执行操作。这时候我们可以用到GCD的队列组。
dispatch_group_t group =  dispatch_group_create();

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的异步操作都执行完毕后,回到主线程...
});

4.NSOperation(OC)

  • 基于GCD(底层是GCD)
  • 比GCD多了一些更简单实用的功能
  • 使用更加面向对象
  • 生命周期自动管理
4.1 NSOperation简介

NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD更高一层的封装,但是比GCD更简单易用、代码可读性也更高。

NSOperation需要配合NSOperationQueue来实现多线程。因为默认情况下,NSOperation单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue才能实现异步执行。

因为NSOperation是基于GCD的,那么使用起来也和GCD差不多,其中,NSOperation相当于GCD中的任务,而NSOperationQueue则相当于GCD中的队列。NSOperation实现多线程的使用步骤分为三步:

  1. 创建任务:先将需要执行的操作封装到一个NSOperation对象中。
  2. 创建队列:创建NSOperationQueue对象。
  3. 将任务加入到队列中:然后将NSOperation对象添加到NSOperationQueue中。
4.2 NSOperation和NSOperationQueue的基本使用
4.2.1 创建任务

NSOperation是个抽象类,并不能封装任务。我们只有使用它的子类来封装任务。我们有三种方式来封装任务。

  • 使用子类NSInvocationOperation
// 1.创建NSInvocationOperation对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

// 2.调用start方法开始执行操作
[op start];

 - (void)run
{
    NSLog(@"------%@", [NSThread currentThread]);
}

从中可以看到,在没有使用NSOperationQueue、单独使用NSInvocationOperation的情况下,NSInvocationOperation在主线程执行操作,并没有开启新线程。

  • 使用子类NSBlockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    // 在主线程
    NSLog(@"------%@", [NSThread currentThread]);
}];

[op start];

我们同样可以看到,在没有使用NSOperationQueue、单独使用NSBlockOperation的情况下,NSBlockOperation也是在主线程执行操作,并没有开启新线程。

但是,NSBlockOperation还提供了一个方法addExecutionBlock:,通过addExecutionBlock:就可以为NSBlockOperation添加额外的操作,这些额外的操作就会在其他线程并发执行。

 - (void)blockOperation
{
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        // 在主线程
        NSLog(@"1------%@", [NSThread currentThread]);
    }];    

    // 添加额外的任务(在子线程执行)
    [op addExecutionBlock:^{
        NSLog(@"2------%@", [NSThread currentThread]);
    }];
    [op addExecutionBlock:^{
        NSLog(@"3------%@", [NSThread currentThread]);
    }];
    [op addExecutionBlock:^{
        NSLog(@"4------%@", [NSThread currentThread]);
    }];

    [op start];
}
  • 定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务。

先定义一个继承自NSOperation的子类,重写main方法
YSCOperation.h

#import <Foundation/Foundation.h>

@interface YSCOperation : NSOperation

@end

YSCOperation.m

#import "YSCOperation.h"

@implementation YSCOperation
/**
 * 需要执行的任务
 */
- (void)main
{
    for (int i = 0; i < 2; ++i) {
        NSLog(@"1-----%@",[NSThread currentThread]);
    }    
}

@end

然后使用的时候导入头文件YSCOperation.h。

// 创建YSCOperation
YSCOperation *op1 = [[YSCOperation alloc] init];

[op1 start];

可以看出:在没有使用NSOperationQueue、单独使用自定义子类的情况下,是在主线程执行操作,并没有开启新线程。

4.2.2 创建队列

和GCD中的并发队列、串行队列略有不同的是:NSOperationQueue一共有两种队列:主队列、其他队列。其中其他队列同时包含了串行、并发功能。下边是主队列、其他队列的基本创建方法和特点。

1. 主队列:凡是添加到主队列中的任务(NSOperation),都会放到主线程中执行

NSOperationQueue *queue = [NSOperationQueue mainQueue];

2. 其他队列(非主队列)
添加到这种队列中的任务(NSOperation),就会自动放到子线程中执行;
同时包含了:串行、并发功能

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
4.2.3 将任务加入到队列中

前边说了,NSOperation需要配合NSOperationQueue来实现多线程。
那么我们需要将创建好的任务加入到队列中去。总共有两种方法:
1. - (void)addOperation:(NSOperation *)op;
需要先创建任务,再将创建好的任务加入到创建好的队列中去

- (void)addOperationToQueue
{
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2. 创建操作  
    // 创建NSInvocationOperation    
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];    
    // 创建NSBlockOperation    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"1-----%@", [NSThread currentThread]);
        }
    }];

    // 3. 添加操作到队列中:addOperation:   
    [queue addOperation:op1]; // [op1 start]    
    [queue addOperation:op2]; // [op2 start]
}

2. - (void)addOperationWithBlock:(void (^)(void))block;
无需先创建任务,在block中添加任务,直接将任务block加入到队列中。

- (void)addOperationWithBlockToQueue
{
    // 1. 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2. 添加操作到队列中:addOperationWithBlock:
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"-----%@", [NSThread currentThread]);
        }
    }];
}

4.3 控制串行执行和并行执行的关键

之前我们说过,NSOperationQueue创建的其他队列同时具有串行、并发功能,上边我们演示了并发功能,那么他的串行功能是如何实现的?
这里有个关键参数maxConcurrentOperationCount,叫做最大并发数

  • maxConcurrentOperationCount默认情况下为-1,表示不进行限制,默认为并发执行。
  • maxConcurrentOperationCount为0,任务不会执行。
  • maxConcurrentOperationCount为1时,进行串行执行。
  • maxConcurrentOperationCount大于1时,进行并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整。
- (void)opetationQueue
{
    // 创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 设置最大并发操作数
    //    queue.maxConcurrentOperationCount = 2;
    queue.maxConcurrentOperationCount = 1; // 就变成了串行队列

    // 添加操作
    [queue addOperationWithBlock:^{
        NSLog(@"1-----%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.01];
    }];
    [queue addOperationWithBlock:^{
        NSLog(@"2-----%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.01];
    }];
    [queue addOperationWithBlock:^{
        NSLog(@"3-----%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.01];
    }];
    [queue addOperationWithBlock:^{
        NSLog(@"4-----%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.01];
    }];
    [queue addOperationWithBlock:^{
        NSLog(@"5-----%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.01];
    }];

    [queue addOperationWithBlock:^{
        NSLog(@"6-----%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.01];
    }];
}

4.4 操作依赖

NSOperation和NSOperationQueue最吸引人的地方是它能添加操作之间的依赖关系。比如说有A、B两个操作,其中A执行完操作,B才能执行操作,那么就需要让B依赖于A。具体如下:

- (void)addDependency
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1-----%@", [NSThread  currentThread]);
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2-----%@", [NSThread  currentThread]);
    }];

    [op2 addDependency:op1];    // 让op2 依赖于 op1,则先执行op1,在执行op2

    [queue addOperation:op1];
    [queue addOperation:op2];
}

4.5 一些其他方法

  • - (void)cancel;NSOperation提供的方法,可取消单个操作
  • - (void)cancelAllOperations; NSOperationQueue提供的方法,可以取消队列的所有操作
  • - (void)setSuspended:(BOOL)b; 可设置任务的暂停和恢复,YES代表暂停队列,NO代表恢复队列
  • - (BOOL)isSuspended; 判断暂停状态

这里的暂停和取消并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。

暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值