GCD技术

GCD

GCD 核心概念

  1. 任务添加到队列,并且指定执行任务的函数

  2. 任务使用 block 封装

  • 任务的 block 没有参数也没有返回值

执行任务的函数

  • 必须等待当前语句执行完毕,才会执行下一条语句

  • 不会开启线程

  • 在当前执行 block 的任务

  • 不用等待当前语句执行完毕,就可以执行下一条语句

  • 会开启线程执行 block 的任务

  • 异步是多线程的代名词

  • 异步 dispatch_async

  • 同步 dispatch_sync

队列 - 负责调度任务

  • 专门用来在主线程上调度任务的队列

  • 不会开启线程

  • 主线程空闲时才会调度队列中的任务在主线程执行

  • dispatch_get_main_queue();

  • 一次可以"调度"多个任务

  • dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);

  • 一次只能"调度"一个任务

  • dispatch_queue_create("itheima", NULL);

  • 串行队列

  • 并发队列

  • 主队列

阶段性小结

  • 开不开线程由执行任务的函数决定

    • 异步开,异步是多线程的代名词

    • 同步不开

  • 开几条线程由队列决定

    • iOS 8.0 之后,GCD 能够开启非常多的线程

    • iOS 7.0 以及之前,GCD 通常只会开启 5~6 条线程

    • 串行队列开一条线程

    • 并发队列开多条线程,具体能开的线程数量由底层线程池决定

- 队列的选择

  • 多线程的目的:将耗时的操作放在后台执行!

  • 串行队列,只开一条线程,所有任务顺序执行

    • 如果任务有先后执行顺序的要求

    • 效率低 -> 执行慢 -> "省电"

    • 有的时候,用户其实不希望太快!例如使用 3G 流量,"省钱"

  • 并发队列,会开启多条线程,所有任务不按照顺序执行

    • 如果任务没有先后执行顺序的要求

    • 效率高 -> 执行快 -> "费电"

    • WIFI,包月

实际开发中,线程数量如何决定?

  • WIFI 线程数 6 条

  • 3G / 4G 移动开发的时候,2~3条,再多会费电费钱!


同步 & 异步

概念

  • 同步

    • 必须等待当前语句执行完毕,才会执行下一条语句

  • 异步

    • 不用等待当前语句执行完毕,就可以执行下一条语句

NSThread 中的 同步 & 异步

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    NSLog(@"start");    // 同步执行//    [self demo];
    // 异步执行
    [self performSelectorInBackground:@selector(demo) withObject:nil];    NSLog(@"over");
}

- (void)demo {    NSLog(@"%@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];    NSLog(@"demo 完成");
}

代码小结

  • 同步 从上到下顺序执行

  • 异步 是多线程的代名词


block

概念

  • block 是 C 语言的

  • 是一种数据类型,可以当作参数传递

  • 是一组预先准备好的代码,在需要的时候执行

动画 block 回顾

self.demoView.center = CGPointMake(self.view.center.x, 0);// 此方法会立即执行动画 block[UIView animateWithDuration:2.0 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:10 options:0 animations:^{    NSLog(@"动画开始");    self.demoView.center = self.view.center;
} completion:^(BOOL finished) {    // 会在动画结束后执行
   NSLog(@"动画完成");
}];NSLog(@"come here");

block 基本演练

  • 最简单的 block

- (void)blockDemo1 {    // 定义block
   // 类型 变量名 = 值
   void (^block)() = ^ {        NSLog(@"Hello block");
   };    // 执行
   block();
}

使用 inlineBlock 可以快速定义 block,不过 block 一定要过关

  • 当作参数传递

- (void)blockDemo2 {    void (^block)() = ^ {        NSLog(@"Hello block");
   };

   [self demoBlock:block];
}///  演示 block 当作参数传递- (void)demoBlock:(void (^)())completion {    NSLog(@"干点什么");

   completion();
}
  • 使用局部变量

- (void)blockDemo3 {    // 栈区变量
   int i = 10;    NSLog(@"%p", &i);    void (^block)() = ^ {        // 定义 block 的时候会对栈区变量进行一次 copy
       NSLog(@"Hello block %d %p", i, &i);
   };

   [self demoBlock:block];
}

如果 block 中使用了外部变量,会对外部变量做一次 copy

  • 在 block 中修改外部变量

- (void)blockDemo4 {    // 栈区变量
   __block int i = 10;    NSLog(@"%p", &i);    void (^block)() = ^ {        // 定义 block 的时候会对栈区变量进行一次 copy
       NSLog(@"Hello block %d %p", i, &i);
       i = 20;
   };    NSLog(@"block 定义完成 %p %d", &i, i);

   [self demoBlock:block];    NSLog(@"===>%d", i);
}

如果要在 block 内部修改栈区变量,需要使用 __block 修饰符,并且定义 block 之后,栈区变量的地址会变化为堆区地址

block 的内存位置

  • 全局区:如果block中没有使用任何全局变量

  • 栈区:如果 block 中使用了外部变量

    • MRC 模式可以看到

    • ARC 模式,系统会自动将 Block 复制到堆中

  • 堆区:将 block 设置给 copy 属性

@property (nonatomic, copy) void (^myBlock)();
- (void)blockDemo5 {    int i = 10;    void (^block)() = ^ {        NSLog(@"i --- %d", i);
   };    NSLog(@"%@", block);    self.myBlock = block;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    NSLog(@"%@", self.myBlock);
}

注意:虽然目前 ARC 编译器在设置属性时,已经替程序员复制了 block,但是定义 block时,仍然建议使用 copy 属性

GCD 常用代码

体验代码

异步执行任务

- (void)gcdDemo1 {    // 1. 全局队列
   dispatch_queue_t q = dispatch_get_global_queue(0, 0);    // 2. 任务
   void (^task)() = ^ {        NSLog(@"%@", [NSThread currentThread]);
   };    // 3. 指定执行任务的函数
   // 异步执行任务 - 新建线程,在新线程执行 task
   dispatch_async(q, task);    NSLog(@"come here");
}

注意:如果等待时间长一些,会发现线程的 number 发生变化,由此可以推断 gcd 底层线程池的工作

同步执行任务

- (void)gcdDemo1 {    // 1. 全局队列
   dispatch_queue_t q = dispatch_get_global_queue(0, 0);    // 2. 任务
   void (^task)() = ^ {        NSLog(@"%@", [NSThread currentThread]);
   };    // 3. 指定执行任务的函数
   // 同步执行任务 - 不开启线程,在当前线程执行 task
   dispatch_sync(q, task);    NSLog(@"come here");
}

精简代码

- (void)gcdDemo2 {    for (int i = 0; i < 10; ++i) {        dispatch_async(dispatch_get_global_queue(0, 0), ^{            NSLog(@"%@ %@", [NSThread currentThread], @"hello");
       });
   }
}

与 NSThread 的对比

  1. 所有的代码写在一起的,让代码更加简单,易于阅读和维护

  • NSThread 通过 @selector 指定要执行的方法,代码分散

  • GCD 通过 block 指定要执行的代码,代码集中

使用 GCD 不需要管理线程的创建/销毁/复用的过程!程序员不用关心线程的生命周期

如果要开多个线程 NSThread 必须实例化多个线程对象

NSThread 靠 NSObject 的分类方法实现的线程间通讯,GCD 靠 block

线程间通讯

- (void)gcdDemo3 {    dispatch_async(dispatch_get_global_queue(0, 0), ^{        NSLog(@"耗时操作 %@", [NSThread currentThread]);        // 耗时操作之后,更新UI
       dispatch_async(dispatch_get_main_queue(), ^{            NSLog(@"更新 UI %@", [NSThread currentThread]);
       });
   });
}

以上代码是 GCD 最常用代码组合!

  • 如果要在更新 UI 之后,继续做些事情,可以使用以下代码

- (void)gcdDemo4 {    dispatch_async(dispatch_get_global_queue(0, 0), ^{        NSLog(@"耗时操作");        dispatch_sync(dispatch_get_main_queue(), ^{            NSLog(@"更新UI");
       });        NSLog(@"更新UI完毕");
   });
}

网络下载图片

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_async(dispatch_get_global_queue(00), ^{
        NSLog(@"%s %@", __FUNCTION__, [NSThread currentThread]);

        // 1. 异步下载网络图片
        NSURL *url = [NSURL URLWithString:@"
http://f.hiphotos.baidu.com/image/pic/item/1f178a82b9014a901bef674aaa773912b21bee70.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];

        // 2. 完成后更新 UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            [self.imageView sizeToFit];

            self.scrollView.contentSize = image.size;
        });
    });

}


串行队列

特点

  • 先进先出的方式,顺序调度队列中的任务执行

  • 无论队列中所指定的执行任务函数是同步还是异步,都会等待前一个任务执行完成后,再调度后面的任务

队列创建

dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_SERIAL);dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", NULL);

串行队列演练

  • 串行队列 同步执行

/**
 提问:是否开线程?是否顺序执行?come here 的位置?
 */- (void)gcdDemo1 {    // 1. 队列
    dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_SERIAL);    // 2. 执行任务
    for (int i = 0; i < 10; ++i) {        NSLog(@"--- %d", i);        dispatch_sync(q, ^{            NSLog(@"%@ - %d", [NSThread currentThread], i);
        });
    }    NSLog(@"come here");
}
  • 串行队列 异步执行

/**
 提问:是否开线程?是否顺序执行?come here 的位置?
 */- (void)gcdDemo2 {    // 1. 队列
    dispatch_queue_t q = dispatch_queue_create("itheima", NULL);    // 2. 执行任务
    for (int i = 0; i < 10; ++i) {        NSLog(@"--- %@ %d", [NSThread currentThread], i);        dispatch_async(q, ^{            NSLog(@"%@ - %d", [NSThread currentThread], i);
        });
    }    NSLog(@"come here");
}

并发队列

特点

  • 先进先出的方式,并发调度队列中的任务执行

  • 如果当前调度的任务是同步执行的,会等待任务执行完成后,再调度后续的任务

  • 如果当前调度的任务是异步执行的,同时底层线程池有可用的线程资源,会再新的线程调度后续任务的执行

队列创建

dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);

并发队列演练

  • 并发队列 异步执行

/**
提问:是否开线程?是否顺序执行?come here 的位置?
*/- (void)gcdDemo3 {    // 1. 队列
   dispatch_queue_t q = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);    // 2. 执行任务
   for (int i = 0; i < 10; ++i) {        dispatch_async(q, ^{            NSLog(@"%@ - %d", [NSThread currentThread], i);
       });
   }    NSLog(@"come here");
}
  • 并发队列 同步执行

/**
提问:是否开线程?是否顺序执行?come here 的位置?
*/- (void)gcdDemo4 {    // 1. 队列
   dispatch_queue_t q = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);    // 2. 执行任务
   for (int i = 0; i < 10; ++i) {        dispatch_sync(q, ^{            NSLog(@"%@ - %d", [NSThread currentThread], i);
       });        NSLog(@"---> %i", i);
   }    NSLog(@"come here");
}

主队列

特点

  • 专门用来在主线程上调度任务的队列

  • 不会开启线程

  • 先进先出的方式,在主线程空闲时才会调度队列中的任务在主线程执行

  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度

队列获取

  • 主队列是负责在主线程调度任务的

  • 会随着程序启动一起创建

  • 主队列只需要获取不用创建

dispatch_queue_t queue = dispatch_get_main_queue();

主队列演练

  • 主队列,异步执行

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
   [self gcdDemo1];

   [NSThread sleepForTimeInterval:1];    NSLog(@"over");
}

- (void)gcdDemo1 {    dispatch_queue_t queue = dispatch_get_main_queue();    for (int i = 0; i < 10; ++i) {        dispatch_async(queue, ^{            NSLog(@"%@ - %d", [NSThread currentThread], i);
       });        NSLog(@"---> %d", i);
   }    NSLog(@"come here");
}

主线程空闲时才会调度队列中的任务在主线程执行

  • 主队列,同步执行

// MARK: 主队列,同步任务- (void)gcdDemo6 {    // 1. 队列
   dispatch_queue_t q = dispatch_get_main_queue();    NSLog(@"!!!");    // 2. 同步
   dispatch_sync(q, ^{        NSLog(@"%@", [NSThread currentThread]);
   });    NSLog(@"come here");
}

主队列主线程相互等待会造成死锁

同步任务的作用

同步任务,可以让其他异步执行的任务,依赖某一个同步任务

例如:在用户登录之后,再异步下载文件!

- (void)gcdDemo1 {    dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);    dispatch_sync(queue, ^{        NSLog(@"登录 %@", [NSThread currentThread]);
   });    dispatch_async(queue, ^{        NSLog(@"下载 A %@", [NSThread currentThread]);
   });    dispatch_async(queue, ^{        NSLog(@"下载 B %@", [NSThread currentThread]);
   });
}
  • 代码改造,让登录也在异步执行

- (void)gcdDemo2 {    dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);    void (^task)() = ^{        dispatch_sync(queue, ^{            NSLog(@"登录 %@", [NSThread currentThread]);
       });        dispatch_async(queue, ^{            NSLog(@"下载 A %@", [NSThread currentThread]);
       });        dispatch_async(queue, ^{            NSLog(@"下载 B %@", [NSThread currentThread]);
       });
   };    dispatch_async(queue, task);
}
  • 主队列调度同步队列不死锁

- (void)gcdDemo3 {    dispatch_queue_t queue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);    void (^task)() = ^ {        dispatch_sync(dispatch_get_main_queue(), ^{            NSLog(@"死?");
       });
   };    dispatch_async(queue, task);
}


主队列在主线程空闲时才会调度队列中的任务在主线程执行



Barrier 异步

  • 主要用于在多个异步操作完成之后,统一对非线程安全的对象进行更新

  • 适合于大规模的 I/O 操作

代码演练

  • 准备工作

@interface ViewController () {    // 加载照片队列
   dispatch_queue_t _photoQueue;
}@property (nonatomic, strong) NSMutableArray *photoList;@end- (NSMutableArray *)photoList {    if (_photoList == nil) {
       _photoList = [[NSMutableArray alloc] init];
   }    return _photoList;
}

NSMutableArray 是非线程安全的

  • viewDidLoad

- (void)viewDidLoad {
    [super viewDidLoad];

    _photoQueue = dispatch_queue_create("com.itheima.com", DISPATCH_QUEUE_CONCURRENT);    for (int i = 0; i < 20; ++i) {
        [self loadPhotos:i];
    }
}

  • 模拟下载照片并在完成后添加到数组

- (void)loadPhotos:(int)index {    dispatch_async(_photoQueue, ^{
       [NSThread sleepForTimeInterval:1.0];        NSString *fileName = [NSString stringWithFormat:@"%02d.jpg", index % 10 + 1];        NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];        UIImage *image = [UIImage imageWithContentsOfFile:path];

       [self.photoList addObject:image];        NSLog(@"添加照片 %@", fileName);
   });
}

运行测试

  • 由于 NSMutableArray 是非线程安全的,如果出现两个线程在同一时间向数组中添加对象,会出现程序崩溃的情况

  • 解决办法

NSLog(@"添加照片 %@", fileName);
dispatch_barrier_async(_photoQueue, ^{
   [self.photoList addObject:image];    NSLog(@"OK %@", [NSThread currentThread]);

});

使用 dispatch_barrier_async 添加的 block 会在之前添加的 block 全部运行结束之后,才在同一个线程顺序执行,从而保证对非线程安全的对象进行正确的操作!

Barrier 工作示意图

注意:dispatch_barrier_async 必须使用自定义队列,否则执行效果和全局队列一致

全局队列

  • 是系统为了方便程序员开发提供的,其工作表现与并发队列一致

全局队列 & 并发队列的区别

  • 全局队列

    • 没有名称

    • 无论 MRC & ARC 都不需要考虑释放

    • 日常开发中,建议使用"全局队列"

  • 并发队列

    • 有名字,和 NSThread 的 name 属性作用类似

    • 如果在 MRC 开发时,需要使用 dispatch_release(q); 释放相应的对象

    • dispatch_barrier 必须使用自定义的并发队列

    • 开发第三方框架时,建议使用并发队列

全局队列 异步任务

/**
提问:是否开线程?是否顺序执行?come here 的位置?
*/- (void)gcdDemo8 {    // 1. 队列
   dispatch_queue_t q = dispatch_get_global_queue(0, 0);    // 2. 执行任务
   for (int i = 0; i < 10; ++i) {        dispatch_async(q, ^{            NSLog(@"%@ - %d", [NSThread currentThread], i);
       });
   }    NSLog(@"come here");
}

运行效果与并发队列相同

参数

  1. 服务质量(队列对任务调度的优先级)/iOS 7.0 之前,是优先级

  • DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级

  • DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级

  • DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级

  • DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级

  • QOS_CLASS_USER_INTERACTIVE 0x21, 用户交互(希望最快完成-不能用太耗时的操作)

  • QOS_CLASS_USER_INITIATED 0x19, 用户期望(希望快,也不能太耗时)

  • QOS_CLASS_DEFAULT 0x15, 默认(用来底层重置队列使用的,不是给程序员用的)

  • QOS_CLASS_UTILITY 0x11, 实用工具(专门用来处理耗时操作!)

  • QOS_CLASS_BACKGROUND 0x09, 后台

  • QOS_CLASS_UNSPECIFIED 0x00, 未指定,可以和iOS 7.0 适配

  • iOS 8.0(新增,暂时不能用,今)

  • iOS 7.0

为未来保留使用的,应该永远传入0


结论:如果要适配 iOS 7.0 & 8.0,使用以下代码: dispatch_get_global_queue(0, 0);



延迟操作

// MARK: - 延迟执行- (void)delay {    /**
    从现在开始,经过多少纳秒,由"队列"调度异步执行 block 中的代码

    参数
    1. when    从现在开始,经过多少纳秒
    2. queue   队列
    3. block   异步执行的任务
    */
   dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));    void (^task)() = ^ {        NSLog(@"%@", [NSThread currentThread]);
   };    // 主队列//    dispatch_after(when, dispatch_get_main_queue(), task);
   // 全局队列//    dispatch_after(when, dispatch_get_global_queue(0, 0), task);
   // 串行队列
   dispatch_after(when, dispatch_queue_create("itheima", NULL), task);    NSLog(@"come here");
}

- (void)after {
   [self.view performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0];    NSLog(@"come here");
}

一次性执行

有的时候,在程序开发中,有些代码只想从程序启动就只执行一次,典型的应用场景就是“单例”

// MARK: 一次性执行- (void)once {    static dispatch_once_t onceToken;    NSLog(@"%ld", onceToken);    dispatch_once(&onceToken, ^{
       [NSThread sleepForTimeInterval:1.0];        NSLog(@"一次性吗?");
   });    NSLog(@"come here");
}
  • dispatch 内部也有一把锁,是能够保证"线程安全"的!而且是苹果公司推荐使用的

  • 以下代码用于测试多线程的一次性执行

- (void)demoOnce {    for (int i = 0; i < 10; ++i) {        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           [self once];
       });
   }
}

单例测试

单例的特点

  1. 在内存中只有一个实例

  2. 提供一个全局的访问点

单例实现

// 使用 dispatch_once 实现单例+ (instancetype)sharedSingleton {    static id instance;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{
       instance = [[self alloc] init];
   });    return instance;
}// 使用互斥锁实现单例+ (instancetype)sharedSync {    static id syncInstance;    @synchronized(self) {        if (syncInstance == nil) {
           syncInstance = [[self alloc] init];
       }
   }    return syncInstance;
}

面试时只要实现上面 sharedSingleton 方法即可

单例测试

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    long largeNumber = 1000 * 1000;

    // 测试互斥锁
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (long i = 0; i < largeNumber; ++i) {
        [Singleton sharedSync];
    }
    NSLog(@"互斥锁: %f", CFAbsoluteTimeGetCurrent() - start);

    // 测试 dispatch_once
    start = CFAbsoluteTimeGetCurrent();
    for (long i = 0; i < largeNumber; ++i) {
        [Singleton sharedSingleton];
    }
    NSLog(@"dispatch_once: %f", CFAbsoluteTimeGetCurrent() - start);

}



调度组

常规用法

- (void)group1 {    // 1. 调度组
   dispatch_group_t group = dispatch_group_create();    // 2. 队列
   dispatch_queue_t q = dispatch_get_global_queue(0, 0);    // 3. 将任务添加到队列和调度组
   dispatch_group_async(group, q, ^{
       [NSThread sleepForTimeInterval:1.0];        NSLog(@"任务 1 %@", [NSThread currentThread]);
   });
   dispatch_group_async(group, q, ^{        NSLog(@"任务 2 %@", [NSThread currentThread]);
   });
   dispatch_group_async(group, q, ^{        NSLog(@"任务 3 %@", [NSThread currentThread]);
   });    // 4. 监听所有任务完成
   dispatch_group_notify(group, q, ^{        NSLog(@"OVER %@", [NSThread currentThread]);
   });    // 5. 判断异步
   NSLog(@"come here");
}

enter & leave

// MARK: - 调度组 2- (void)group2 {    // 1. 调度组
   dispatch_group_t group = dispatch_group_create();    // 2. 队列
   dispatch_queue_t q = dispatch_get_global_queue(0, 0);    // dispatch_group_enter & dispatch_group_leave 必须成对出现
   dispatch_group_enter(group);
   dispatch_group_async(group, q, ^{        NSLog(@"任务 1 %@", [NSThread currentThread]);        // dispatch_group_leave 必须是 block 的最后一句
       dispatch_group_leave(group);
   });

   dispatch_group_enter(group);
   dispatch_group_async(group, q, ^{        NSLog(@"任务 2 %@", [NSThread currentThread]);        // dispatch_group_leave 必须是 block 的最后一句
       dispatch_group_leave(group);
   });    // 4. 阻塞式等待调度组中所有任务执行完毕
   dispatch_group_wait(group, DISPATCH_TIME_FOREVER);    // 5. 判断异步
   NSLog(@"OVER %@", [NSThread currentThread]);
}


转载于:https://my.oschina.net/venn0126/blog/551324

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值