《Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》---学习笔记(41-52)

41. 多用派发队列,少用同步锁

在OC中,如果多个线程要执行同一份代码,那么可能会出现问题。这种情况下,通常使用锁来实现某种同步机制。

在GCD出现之前,有两种方法:

  • 第一种:synchronization
- (void)someMethod
{
	@synchronized(self){
		//Safe
	}
}

这种会根据给定的对象(self),自动创建一个锁,并等待块中的代码执行完毕。
执行到这段代码结尾处,锁就释放了。
然鹅,过多的使用@synchronized(self),会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行

  • 第二种:NSLock对象
_lock = [[NSLock alloc] init];
- (void)someMethod
{
	[_lock lock];
	//Safe
	[_lock unlock];
}

也可以使用递归锁:NSRecursiverLock,线程可以多次持有该锁,而不会出现死锁(deadlock)现象。

  • 在有GCD之后,同步问题可以使用GCD,它更简单、更高效。

在设置属性的时候,如果需要属性为“原子的”,则经常需要用到同步,使用atomic来修饰属性,就可以实现。
但无法保证访问该对象时绝对是线程安全的。

有种简单而高效的方法可以代替同步块或锁对象,那就是使用“串行同步队列
将读写操作都安排在同一个队列里,即可保证数据同步

例如:
在这里插入图片描述即可实现数据的同步

还可以将设置方法写成异步执行:

在这里插入图片描述

多读单写

GCD有一个栅栏(barrier),可以实现多读单写功能。
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);

  • 栅栏只对并发队列有意义,串行队列的话,只是普通的一个一个执行

举个例子:

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString
{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString{
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

经过与之前写的一篇有关多读单写的文章多线程学习(二)有些地方有冲突,有些地方有记录不清的地方,做统一验证

有几处疑问:

  • 栅栏使用串行队列可以吗?什么效果?
  • 栅栏使用全局并发队列可以吗?(全局并发+dispatch_barrier_async,全局并发+dispatch_barrier_sync)
  • 栅栏使用dispatch_barrier_async和dispatch_barrier_sync是否都可以?
栅栏使用串行队列可以吗?什么效果?

两篇文章都提到了不能使用串行队列
验证:
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_SERIAL);
打印结果:

2022-03-20 11:33:44.526704+0800 008[1941:46200] -[ViewController read]_block_invoke
2022-03-20 11:33:45.528229+0800 008[1941:46200] -[ViewController read]_block_invoke
2022-03-20 11:33:46.530097+0800 008[1941:46200] -[ViewController read]_block_invoke
2022-03-20 11:33:47.535326+0800 008[1941:46200] -[ViewController write]_block_invoke
2022-03-20 11:33:48.540525+0800 008[1941:46200] -[ViewController write]_block_invoke
2022-03-20 11:33:49.544776+0800 008[1941:46200] -[ViewController write]_block_invoke
2022-03-20 11:33:50.546115+0800 008[1941:46200] -[ViewController read]_block_invoke

可以看出,读、写都是每秒执行一次,从而失去了多读单写的功能

栅栏使用全局并发队列可以吗?
  • 全局并发+dispatch_barrier_async

验证:

self.queue = dispatch_get_global_queue(0, 0);

- (void)write
{
    dispatch_barrier_async(self.queue, ^{
        sleep(1);
        NSLog(@"%s", __func__);
    });
}

结果:

2022-03-20 11:37:00.889954+0800 008[2015:50136] -[ViewController read]_block_invoke
2022-03-20 11:37:00.889936+0800 008[2015:50138] -[ViewController write]_block_invoke
2022-03-20 11:37:00.889946+0800 008[2015:50140] -[ViewController read]_block_invoke
2022-03-20 11:37:00.889989+0800 008[2015:50142] -[ViewController write]_block_invoke
2022-03-20 11:37:00.889960+0800 008[2015:50141] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890017+0800 008[2015:50137] -[ViewController write]_block_invoke
2022-03-20 11:37:00.890017+0800 008[2015:50149] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890041+0800 008[2015:50148] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890098+0800 008[2015:50156] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890096+0800 008[2015:50153] -[ViewController write]_block_invoke

基本上一下就执行完了,写是同步进行的,没有休眠,因此,不满足“单写”

  • 全局并发+dispatch_barrier_sync

验证:

self.queue = dispatch_get_global_queue(0, 0);

- (void)write
{
    dispatch_barrier_sync(self.queue, ^{
        sleep(1);
        NSLog(@"%s", __func__);
    });
}

结果:

2022-03-20 11:38:08.063348+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:08.065732+0800 008[2048:51476] -[ViewController read]_block_invoke
2022-03-20 11:38:08.065732+0800 008[2048:51472] -[ViewController read]_block_invoke
2022-03-20 11:38:08.065732+0800 008[2048:51471] -[ViewController read]_block_invoke
2022-03-20 11:38:09.064681+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:10.066012+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:11.067604+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:11.067633+0800 008[2048:51471] -[ViewController read]_block_invoke
2022-03-20 11:38:11.067635+0800 008[2048:51476] -[ViewController read]_block_invoke
2022-03-20 11:38:11.067635+0800 008[2048:51472] -[ViewController read]_block_invoke
2022-03-20 11:38:12.069155+0800 008[2048:51424] -[ViewController write]_block_invoke

读一下就执行完毕,满足多读
写是每一秒执行一次,也满足
但有个小问题,写执行完毕,要执行读的操作的时候,基本上最后一个写和第一个读是同时执行的

栅栏使用dispatch_barrier_async和dispatch_barrier_sync是否都可以?
  • 自定义并发+dispatch_barrier_sync

验证:

self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);

- (void)write
{
    dispatch_barrier_sync(self.queue, ^{
        sleep(1);
        NSLog(@"%s", __func__);
    });
}

结果:

2022-03-20 11:41:08.055321+0800 008[2114:54371] -[ViewController read]_block_invoke
2022-03-20 11:41:08.055321+0800 008[2114:54372] -[ViewController read]_block_invoke
2022-03-20 11:41:08.055321+0800 008[2114:54376] -[ViewController read]_block_invoke
2022-03-20 11:41:09.056482+0800 008[2114:54163] -[ViewController write]_block_invoke
2022-03-20 11:41:10.058033+0800 008[2114:54163] -[ViewController write]_block_invoke
2022-03-20 11:41:11.059538+0800 008[2114:54163] -[ViewController write]_block_invoke
2022-03-20 11:41:12.060971+0800 008[2114:54371] -[ViewController read]_block_invoke
2022-03-20 11:41:12.060979+0800 008[2114:54376] -[ViewController read]_block_invoke

满足多读单写

  • 自定义并发+dispatch_barrier_async
    验证:
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);

- (void)write
{
    dispatch_barrier_async(self.queue, ^{
        sleep(1);
        NSLog(@"%s", __func__);
    });
}

结果:

2022-03-20 11:42:58.414861+0800 008[2159:56378] -[ViewController read]_block_invoke
2022-03-20 11:42:58.414861+0800 008[2159:56382] -[ViewController read]_block_invoke
2022-03-20 11:42:58.414861+0800 008[2159:56383] -[ViewController read]_block_invoke
2022-03-20 11:42:59.420234+0800 008[2159:56382] -[ViewController write]_block_invoke
2022-03-20 11:43:00.425576+0800 008[2159:56382] -[ViewController write]_block_invoke
2022-03-20 11:43:01.426410+0800 008[2159:56382] -[ViewController write]_block_invoke
2022-03-20 11:43:02.431424+0800 008[2159:56378] -[ViewController read]_block_invoke
2022-03-20 11:43:02.431423+0800 008[2159:56380] -[ViewController read]_block_invoke
2022-03-20 11:43:02.431445+0800 008[2159:56382] -[ViewController read]_block_invoke
2022-03-20 11:43:03.436903+0800 008[2159:56378] -[ViewController write]_block_invoke
2022-03-20 11:43:04.440432+0800 008[2159:56378] -[ViewController write]_block_invoke

总结:

  • 栅栏不能使用串行队列
  • 使用自定义并发队列+同步/异步栅栏都可以实现多读单写
  • 使用全局并发队列+异步栅栏不可以🙅🏻‍♀️
  • 使用全局并发队列+同步栅栏可以

MJ老师讲的barrier只讲了dispatch_barrier_async,没有讲dispatch_barrier_sync
由于只讲了dispatch_barrier_async,全局global不能满足多读单写功能,因此得出不能使用全局global定义队列的结论
总体还是没有考虑到dispatch_barrier_sync的情况

42. 多用GCD,少用performSelector系列方法

- (id)performSelector:(SEL)aSelector;
SEL是选择因子,例如@selector(selectorName)

局限性:
- (id)performSelector:(SEL)aSelector;返回值是void或对象类型。

- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;最多接收两个参数

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;不能处理两个参数的选择子

因此,本书建议使用GCD,而非performSelector系列方法

43. 掌握GCD及操作队列的使用时机

在执行后台任务时,GCD不一定是最佳方式

GCD是纯C的API,而操作队列(NSOperationQueue)是OC对象。

NSOperation的好处:

  • 可以取消某个操作
    在运行任务之前,可以再NSOperation对象上调用cancel方法,不过,已经启动的任务无法取消
    而把块安排到GCD队列,就无法取消了。如果开发者想自己实现取消功能,需要编写很多代码,而那些代码其实已经由操作队列实现好了

  • 指定操作间的依赖关系

  • 通过键值观测机制监控NSOperation对象的属性
    NSOperation对象有许多属性都适合通过键值观测机制(简称KVO)来监听,比如可以通过isCancelled属性来判断任务是否已取消,isFinished属性判断任务是否已完成。

  • 指定操作的优先级
    GCD的队列也有优先级,但那是针对整个队列来说的,而不是针对每个块来说的。

44. 通过Dispatch Group机制,根据系统资源状况来执行任务

dispatch group能够把任务分组。其最重要的用法就是把将要并发执行的多个任务合为一组,在执行完毕后,统一处理结果。

创建一个group

dispatch_group_t dispatch_group_create(void);

想把任务编组,有两种方法:
方法一:

void dispatch_group_async(dispatch_group_t group,
	dispatch_queue_t queue,
	dispatch_block_t block);

在这里插入图片描述

方法二:

void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);

调用了dispatch_group_enter必须有与之对应的dispatch_group_leave
如果有enter,无leave,那么这一组任务就永远执行不完

在并发队列中,执行任务所用的并发线程数量,取决于各种因素,这也就是GCD为了处理并发任务而有的复杂调度器。

45. 使用dispatch_once来执行只需要运行一次的线程安全代码

使用dispatch_once可以简化代码,并且彻底保证线程安全

static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
    return _instance;
}

由于每次调用时,都必须使用完全相同的标记,因此,标记要声明为static:static dispatch_once_t onceToken;

stitic的作用:

  • 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。
  • static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
  • 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。

46. 不要使用dispatch_get_current_queue

当需要判断当前代码正在哪个队列上执行,可能会使用dispatch_get_current_queue()函数
但此函数的行为常常与开发者所预期的不同,已经废弃

第七章 系统框架

大部分代码都是来源于系统框架,比如NSObject就属于Foundation框架

47. 熟悉系统框架

将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这就是框架

有时候为iOS平台构建的第三方框架所使用的是静态库(static library),这是因为iOS应用程序不允许在其中包含动态库。这些东西严格来讲并不是真的框架,然而也经常被视为框架 。

iOS平台的系统框架是动态库

  • Cocoa框架:开发带图形界面时,用到的框架。其实Cocoa本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架
  • Foundation框架:包括NSObject、NSArray、NSDictionary等类,主要是NS前缀,其前身是NeXTSTEP公司开发的
  • CoreFoundation框架:Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API。两者联系紧密。有一个“无缝桥接”(tollfree bridging)功能,可以将CoreFoundation中的C语言数据结构平滑转换为Foundation中的OC对象,也可以反向转换。
  • CFNetwork框架:提供了C语言级别的网络通信能力
  • CoreAudio框架:提供C语言API可用来操作设备上的音频硬件。
  • AVFoundation框架:提供OC对象,可用来回放并录制音频及视频
  • CoreData框架:提供OC接口,可将对象放入数据库,便于持久保存
  • CoreText框架:提供C语言接口,高效执行文字排版及渲染操作

可以看出,OC编程经常需要使用底层的C语言级API。好处是可以绕过OC的运行期系统,从而提升执行速度,当然ARC只负责OC对象,所以使用这些API时,尤其需要注意内存管理问题。

48. 多用块枚举,少用for循环

本条所讲的collection包含NSArray、NSDictionary、NSSet这几个频繁使用的类型。

for循环

for循环遍历NSArray很简单,不叙述
for循环遍历NSDictionary需要将NSDictionary转换为NSArray:[dictionary allKeys]

    NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
    NSArray *array = [dic allKeys];
    for (int i = 0; i < array.count; i++) {
        NSString *key = array[i];
        NSString *value = dic[array[i]];
        
        NSLog(@"key = %@, value = %@", key, value);
    }

for循环遍历NSSet需要将NSSet转换为NSArray:[set allObjects]

    NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
    NSArray *array = [set allObjects];
    for (int i = 0; i < array.count; i++) {
        NSObject *obj = array[i];
        
        NSLog(@"obj = %@", obj);
    }

使用NSEnumerator遍历

NSEnumerator是一个抽象基类

本书说该类只定义了两个方法:

- (NSArray *)allObjects
- (id)nextObject

查了文档,现在是一个方法、一个属性:

@interface NSEnumerator<ObjectType> : NSObject <NSFastEnumeration>
- (nullable ObjectType)nextObject;
@end

@interface NSEnumerator<ObjectType> (NSExtendedEnumerator)
@property (readonly, copy) NSArray<ObjectType> *allObjects;
@end

nextObject返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。
等到枚举中的全部对象都已返回之后,再调用就将返回nil,表示已经达到枚举末端了。

遍历数组:

    NSArray *array = @[@"1", @"2", @"3", @"100"];
    NSEnumerator *enumerator = [array objectEnumerator];
    NSString *str;
    while ((str = [enumerator nextObject]) != nil) {
        NSLog(@"%@", str);
    }

遍历字典:

    NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
    NSEnumerator *enumerator = [dic keyEnumerator];
    NSString *key;
    while ((key = [enumerator nextObject]) != nil) {
        NSLog(@"key = %@, value = %@", key, dic[key]);
    }

遍历集合:

    NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
    NSEnumerator *enumerator = [set objectEnumerator];
    NSString *str;
    while ((str = [enumerator nextObject]) != nil) {
        NSLog(@"%@", str);
    }

快速遍历

其实就是for in
遍历数组:
不叙述

遍历字典:

    NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
    for (NSString *key in dic) {
        NSLog(@"key = %@, value = %@", key, dic[key]);
    }

遍历集合:

    NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
    for (NSString *str in set) {
        NSLog(@"%@", str);
    }

基于块的遍历方式

NSArray定义了下面这个方法:

- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;

当然,还有一系列类似的遍历方法
上面这个方法,有三个参数,分别是:

  • obj: 当前迭代所针对的对象
  • idx: 所针对的下标
  • *stop:指向布尔值的指针

遍历数组:

    NSArray *array = @[@"1", @"2", @"3", @"100"];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if(idx == 2){
            *stop = YES;
        }
        
        NSLog(@"array[%ld] = %@", idx, obj);
    }];
    
结果:
2022-03-22 09:55:57.656598+0800 008[2373:61464] array[0] = 1
2022-03-22 09:55:57.656722+0800 008[2373:61464] array[1] = 2
2022-03-22 09:55:57.656823+0800 008[2373:61464] array[2] = 3

遍历字典:

    NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
    [dic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSLog(@"%@, %@", key, obj);
    }];
结果:
2022-03-22 09:54:42.467207+0800 008[2318:59522] key1, value1
2022-03-22 09:54:42.467437+0800 008[2318:59522] key2, value2
2022-03-22 09:54:42.467534+0800 008[2318:59522] key3, value3

遍历集合:

    NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSLog(@"%@", obj);
    }];
结果:
2022-03-22 09:57:38.333864+0800 008[2404:63047] 3
2022-03-22 09:57:38.334076+0800 008[2404:63047] 1
2022-03-22 09:57:38.334170+0800 008[2404:63047] 6
2022-03-22 09:57:38.334283+0800 008[2404:63047] 4

块遍历的优势在于:遍历时可以直接从块里获取更多信息:
在遍历数组时,可以知道当前所针对的下标;
在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。

另外,在方法里面,可以将id直接转换为已知类型

49. 对自定义其内存管理语义的collection使用无缝桥接

Foundation框架的一些东西,比如NSArray,可以与CoreFoundation框架中的一些东西,比如CFArray可以类型平滑转换,这种转换就是”无缝桥接“,简称==”桥接“==
例如:

    NSArray *array = @[@"1", @"2", @"3", @"100"];
    CFArrayRef aCFArray = (__bridge CFArrayRef)(array);
    NSLog(@"%ld", (long)CFArrayGetCount(aCFArray));
结果:
2022-03-22 10:11:43.372291+0800 008[2714:74336] 4

转换操作中的 __bridge 告诉ARC如何处理转换所涉及的OC对象。
__bridge 本身的意思是:ARC仍然具备这个OC对象的所有权。
__bridge_retained则相反,意味着ARC将交出对象的所有权。

50. 构建缓存时选用NSCache 而非 NSDictionary

从网络缓存图片的时候,需要把内存中的图片保存到本地,这样,再稍后使用的时候,就不需要再次下载了

一般都是用字典来做,但,NSCache可能会更好些。
NSCache可以在系统资源将要耗尽时,自动删减缓存。此外,NSCache还会进行”最久未使用的“对象

NSCache是线程安全的

在操作缓存删减内容中,有两个与系统资源相关的尺度可供调整,一个是缓存中的对象总数,一个是所有对象的”总开销“

比如,可缓存的总对象数目上限设为100,将”总开销“上限设为5MB

51. 精简initialize与load的实现代码

有时候,类必须先执行某些初始化操作,才能正常使用。
在OC中,绝大多数类都继承NSObject这个根类,而该类有两个方法,可用来实现这种初始化操作。

+ (void)load

对于加入运行期系统中的每个类,以及分类来说,必定会调用此方法,而且仅调用一次。
调用时机:当包含类或者分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候。

  • 如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。(父子原分)

  • load方法,在运行期系统处于”脆弱状态“,在执行子类的load方法之前,必定会先执行所有超类的load方法。如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。

  • 如果某个类A没有实现Load方法,不管他的父类AA实现或者没有实现load范发个,系统都不会调用类A

  • 在load方法中,务必实现的精简一些,尽量减少操作,因为整个应用程序在执行load方法时都会阻塞

类相关初始化的操作:

+ (void)initialize

对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。
调用时机:运行期系统调用

  • 它是”惰性调用的“,即只有当程序用到了相关的类时,才会调用。
  • initialize方法,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码

52. 别忘了 NSTimer会保留其目标对象

计时器NSTimer和”运行循环“(run loop)相关联,运行循环到时候会触发任务。
只有把计时器放在运行循环里,它才能正常触发任务。

  • 计数器会保留其目标对象target,因此,要避免产生循环引用
  • 可以使用NSTimer的block方法,避免循环引用
  • 还可以使用NSProxy
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值