block全解

最近的工作中比较频繁的用到了Block,不在是以前当做函数指针的替代或者某些API只有Blocks形式的接口才不得已用之了,发现自己对其了解还是太浅,特别是变量的生存期,按惯例还是翻译官方文档,原文 链接

 

 

介绍

Block 对象是C语言层面的语法,也是一个运行时特性. 它们很类似与标准的C函数,但是除了可执行的代码,它们还包含了与自动(栈)或托管(堆)的内存所绑定的变量。因此一个block维护了一系列的状态(即数据),在执行时会改变代码的行为。

你可以使用blocks编写函数表达式当参数传入API,也可以将其保存下来用于多线程。Blocks在回调中非常有用,因为block不仅包含着回调时需要执行的代码,还包含了执行代码时需要的数据。

你可以在Mac OS X 10.6和iOS 4.0之后的版本上的GCC附带的Clang上使用。blocks运行时库是开源的,见此 LLVM’s compiler-rt subproject repository.Blocks也已经呈现给了C标准工作组,见 N1370: Apple’s Extensions to C (其中包含了垃圾回收). 正如Objective-C和C++都是C的衍生语言,blocks也被设计成可以在这三种语言中使用 (如同Objective-C++). (语法也可以表现出其目标).


你应该阅读本文档来学习block的相关知识,以将block用于C,C++,Objective-C中,并提高你的程序的效率可可维护性。

 

文档结构

本文档包括以下章节:

  • "Blocks入门" 提供了快速,使用的blocks简介

  • “整体概念” 提供了blocks在概念上的介绍

  • "声明和创建Blocks" 为您展示如何声明block变量及实现blocks

  • "Blocks和变量" 描述blocks和变量之间的相互作用, __block 修饰符的作用

  • "使用Blocks" 详解各种用法范式 

Blocks入门

下面章节使用实际的例子帮助您入门blocks

声明和使用Block

你要使用^操作符去声明一个block变量,^也是标示着一段block文字的开始。block的实体包含在{}中,如下所示(形同C,;表示语句的终结):

 

Objective-c代码   收藏代码
  1. int multiplier = 7;  
  2. int (^myBlock)(int) = ^(int num) {  
  3.     return num * multiplier;  
  4. };  
 示例的详解如下(图片不好翻译凑合着看): 


 

注意block可以使用其定义范围内的变量.

如果你把block声明为一个变量,你可以把它当一个函数(function,本文中特指C语言形式的函数)一样调用:

 

Objective-c代码   收藏代码
  1. int multiplier = 7;  
  2. int (^myBlock)(int) = ^(int num) {  
  3.     return num * multiplier;  
  4. };  
  5.    
  6. printf("%d", myBlock(3));  
  7. // prints "21"  

 

直接使用Block

在很多场景下,你不需要定义一个block变量,作为替代,仅仅只需要在需要block参数的地方写block文字即可。下例使用了qsort_b 函数. qsort_b 很类似标准的 qsort_r 函数,不过它使用block作为最后一个参数.

 

Objective-c代码   收藏代码
  1. char *myCharacters[3] = { "TomJohn""George""Charles Condomine" };  
  2.    
  3. qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {  
  4.     char *left = *(char **)l;  
  5.     char *right = *(char **)r;  
  6.     return strncmp(left, right, 1);  
  7. });  
  8.    
  9. // myCharacters is now { "Charles Condomine""George""TomJohn" }  

 

Cocos中的Blocks

许多 Cocoa frameworks 中的方法(method,特指Objecitve-C的方法即[])使用block作为参数, 常见于对集合中对象的操作或一个操作完成之后的回调. 下例展示和如何在NSArray 的方法 sortedArrayUsingComparator: 中使用block。该方法使用一个block作为参数. 例子中的block被定义为一个 NSComparator 类型的局部变量:

 

Objective-c代码   收藏代码
  1. NSArray *stringsArray = [NSArray arrayWithObjects:  
  2.                                  @"string 1",  
  3.                                  @"String 21",  
  4.                                  @"string 12",  
  5.                                  @"String 11",  
  6.                                  @"String 02", nil];  
  7.    
  8. static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch |  
  9.         NSWidthInsensitiveSearch | NSForcedOrderingSearch;  
  10. NSLocale *currentLocale = [NSLocale currentLocale];  
  11.    
  12. NSComparator finderSortBlock = ^(id string1, id string2) {  
  13.    
  14.     NSRange string1Range = NSMakeRange(0, [string1 length]);  
  15.     return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];  
  16. };  
  17.    
  18. NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];  
  19. NSLog(@"finderSortArray: %@", finderSortArray);  
  20.    
  21. /*  
  22. Output:  
  23. finderSortArray: (  
  24.     "string 1",  
  25.     "String 02",  
  26.     "String 11",  
  27.     "string 12",  
  28.     "String 21"  
  29. )  
  30. */  
 
__block变量

blocks有一个强大的特性,即它可以修改其当前词法范围内的变量. 只要对变量加上 __block 存储修饰符. 稍微修改上面的例子, 使用一个block变量去统计下例中有多少字符串是相同的。例子中的block还是直接使用的,并用到了叫 currentLocale 的只读变量:

 

Objective-c代码   收藏代码
  1. NSArray *stringsArray = [NSArray arrayWithObjects:  
  2.                          @"string 1",  
  3.                          @"String 21", // <-  
  4.                          @"string 12",  
  5.                          @"String 11",  
  6.                          @"Strîng 21", // <-  
  7.                          @"Striñg 21", // <-  
  8.                          @"String 02", nil];  
  9.    
  10. NSLocale *currentLocale = [NSLocale currentLocale];  
  11. __block NSUInteger orderedSameCount = 0;  
  12.    
  13. NSArray *diacriticInsensitiveSortArray = [stringsArray sortedArrayUsingComparator:^(id string1, id string2) {  
  14.    
  15.     NSRange string1Range = NSMakeRange(0, [string1 length]);  
  16.     NSComparisonResult comparisonResult = [string1 compare:string2 options:NSDiacriticInsensitiveSearch range:string1Range locale:currentLocale];  
  17.    
  18.     if (comparisonResult == NSOrderedSame) {  
  19.         orderedSameCount++;  
  20.     }  
  21.     return comparisonResult;  
  22. }];  
  23.    
  24. NSLog(@"diacriticInsensitiveSortArray: %@", diacriticInsensitiveSortArray);  
  25. NSLog(@"orderedSameCount: %d", orderedSameCount);  
  26.    
  27. /*  
  28. Output:  
  29.    
  30. diacriticInsensitiveSortArray: (  
  31.     "String 02",  
  32.     "string 1",  
  33.     "String 11",  
  34.     "string 12",  
  35.     "String 21",  
  36.     "Str\U00eeng 21",  
  37.     "Stri\U00f1g 21"  
  38. )  
  39. orderedSameCount: 2  
  40. */  

整体概念

 

Block对象提供了创建特殊函数的方法,函数体可用C,Objective-C,C++等C类的语言做表达式. 在其他的语言环境中,block变量可能会被叫做"闭包(closure)", 而在这里,除非和标准C术语的一段(block)代码混淆的情况之外,一般称为"blocks"。

Block 功能性

一个block就是一块匿名的代码块:

  • 和函数一样有含类型的参数列表
  • 有直接声明或可推断出的返回值
  • 可以获得当前词法范围的状态
  • 有能力修改当前词法范围的状态
  • 可以和当前词法范围的其他block共同修改状态
  • 可以持续共享和修改当前词法范围的状态,甚至在当前词法范围销毁之后

你可以copy一个block还可以将其传到别的线程以延后执行 (或本线程的执行循环里). 编译器和运行时会把block里用到的所有变量保存到该block的所有拷贝的生存期后。尽管blocks可以由纯C或C++写成,但block本身始终是一个Objective-C变量.

用法

Blocks一般都是小段的,自成体系的代码块. 因此,特别适合用在封转并行操作所需的数据,或用于集合中,以及操作完成后的回调.

Blocks基于两大理由,是传统回调函数的优秀替代品:

  1. 允许你把具体实现代码写在调用该方法的地方.

    Blocks也经常是framework的方法参数.

       
  2. 可以访存局部变量. 不需要像以前的回调一样,把在操作后所有需要用到的数据封装成特定的数据结构, 你完全可以直接访问局部变量.

 

 

声明和创建Blocks

声明Block引用

 

Block变量保持blocks的引用. 声明block的语法类似于声明函数指针,区别之处在于使用^而不是*.block的类型取决于C的类型系统. 下面都是有效的block变量声明:

Objective-c代码   收藏代码
  1. void (^blockReturningVoidWithVoidArgument)(void);  
  2. int (^blockReturningIntWithIntAndCharArguments)(int, char);  
  3. void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);  

 

 

 

 

 

Blokcs也支持可变参数即(...). 如果不带参数则必须在参数列表中指定 void .

Blocks被设计为完全的类型安全,编译器可由一整套的元数据去判断blocks,blocks的参数,及返回值的有效性(这句没太看懂,原文是Blocks are designed to be fully type safe by giving the compiler a full set of metadata to use to validate use of blocks, parameters passed to blocks, and assignment of the return value)。 你可以把block强制转换为任意类型的指针,反之亦然。不过,你不能像对指针一样用*运算符对block进行解引用(dereference)操作,因为block的大小无法在编译时算出.

你也可以创建blocks的类型,在几个地方用到同样函数签名的block时,这是一种很好的做法:

 

 

Objective-c代码   收藏代码
  1. typedef float (^MyBlockType)(float, float);  
  2.    
  3. MyBlockType myFirstBlock = // ... ;  
  4. MyBlockType mySecondBlock = // ... ;  

 

 

创建Block

使用 ^ 操作符标示出block表达式的字面开始部分. 后面跟随着包含参数列表的().block主体则是包含在{}之中。 下例展示了如何定义一个简单的block,并将其赋值给之前定义的变量 (oneFrom).最后由常规的 ; 也是c语言的语句结束符结束

 

Objective-c代码   收藏代码
  1. int (^oneFrom)(int);  
  2.    
  3. oneFrom = ^(int anInt) {  
  4.     return anInt - 1;  
  5. };  

  如果你不想显式的声明block的返回值,也可以由block内容自动推断出来。如果返回值是可推断的,并且参数列表为 void, 你也可以省略参数列表. 当有多个返回语句时,它们必须是类型一直的(必要时会进行强行转换).

 

 

全局Blocks

在文件层面上,你可以使用block当全局的字面文字:

Objective-c代码   收藏代码
  1. #import <stdio.h>  
  2.    
  3. int GlobalInt = 0;  
  4. <p>int (^getGlobalInt)(void) = ^{ return GlobalInt; };</p>  

 


Blocks和变量

本部分阐述blocks和变量之间的交互作用,包括内存管理.

变量的类型

在block的主体代码块中,变量可以分为五种.

你可以引用三种标准类型的变量,就如同函数传参:

  • 全局变量,包括静态局部变量

  • 全局函数 (理论上来说不是变量)

  • 作用域(enclosing scope)内的局部变量和参数

Blocks还支持另外两种类型的变量:

  1. 在函数级别的 __block 变量. 它们在block中是可变的 (同时也在作用域中可变),如果有block被拷贝到了堆(heap)上,则它们也会被保存.

  2. const imports.

最后,在一个方法的具体实现中,blocks可以引用Objective-C的实体变量,见 “对象和Block变量.”

block使用变量适用如下规则:

  1. 全局变量是可访存的,包括作用域内的静态变量.

  2. block的参数是可访存的 (和函数的参数一样).

  3. 作用域内的局部栈(非静态)变量被当做 const 变量.

    它们的值以block表达式在程序的点为准. 在嵌套blocks中,则是里该block最近的作用域中的值为准.

  4. 词法作用域中的局部变量有 __block 存储修饰符的,是按引用传递并可以修改.

    所有的变动都反应到作用域中,包括其他在本作用域内定义的block中做的修改.

    详见 “__block 存储类型.”

  5. 在block内部声明的局部变量,就和函数内声明的变量一样.

    每次调用block都产生变量的新的拷贝,这些变量轮流被当做 const 或按引用传递的变量用在block内部.

下例使用局部非静态变量:


Objective-c代码   收藏代码
  1. int x = 123;  
  2.    
  3. void (^printXAndY)(int) = ^(int y) {  
  4.    
  5.     printf("%d %d\n", x, y);  
  6. };  
  7.    
  8. printXAndY(456); // prints: 123 456  
 

必须要注意,如果想在block内改变x的值,会导致错误:

 

Objective-c代码   收藏代码
  1. int x = 123;  
  2.    
  3. void (^printXAndY)(int) = ^(int y) {  
  4.    
  5.     x = x + y; // error  
  6.     printf("%d %d\n", x, y);  
  7. };  
 

如果想要在block内改变变量,你要使用 __block 类型的存储修饰符,详见“ __block 存储类型.”

 

The __block Storage Type

你可以指明一个导入的变量为可变的即可读写的,只需要使用 __block 类型存储修饰符. __block 存储类型类不同于 registerauto,  但和static 存储类型一样,对于局部变量提供了可变的能力.

 

__block 变量生存于存储区内,并供当前词法范围的所有blocks共享使用. 因此,该存储区将存活到block栈frame销毁之后,甚至在其他拷贝或声明了这些block的block销毁后 (比如压到队列中供后续执行). 在给定的词法范围里的多个blocks可以同时使用一个共享的变量.(这段我整个没看懂,原文是__block variables live in storage that is shared between the lexical scope of the variable and all blocks and block copies declared or created within the variable’s lexical scope. Thus, the storage will survive the destruction of the stack frame if any copies of the blocks declared within the frame survive beyond the end of the frame (for example, by being enqueued somewhere for later execution). Multiple blocks in a given lexical scope can simultaneously use a shared variable.)

 

作为优化, block变量和block本身一样开始是存储在栈上. 但如果用Block_copy (如果是Objecitve-C环境下, 可以直接向block发送 copy)对block进行拷贝, 变量就会拷贝到堆上. 因此 __block 变量的地址是可以改变的.

对于 __block 变量还有两个更严格的限制: 不能是变长数组,也不能是含有C99的变长数组的结构体.

下例示范如何使用 __block 变量:

 

 

Objective-c代码   收藏代码
  1. __block int x = 123; //  x lives in block storage  
  2.    
  3. void (^printXAndY)(int) = ^(int y) {  
  4.    
  5.     x = x + y;  
  6.     printf("%d %d\n", x, y);  
  7. };  
  8. printXAndY(456); // prints: 579 456  
  9. // x is now 579  
 
下例展示几种变量和blocks之间的交互:

 

Objective-c代码   收藏代码
  1. extern NSInteger CounterGlobal;  
  2. static NSInteger CounterStatic;  
  3.    
  4. {  
  5.     NSInteger localCounter = 42;  
  6.     __block char localCharacter;  
  7.    
  8.     void (^aBlock)(void) = ^(void) {  
  9.         ++CounterGlobal;  
  10.         ++CounterStatic;  
  11.         CounterGlobal = localCounter; // localCounter fixed at block creation  
  12.         localCharacter = 'a'; // sets localCharacter in enclosing scope  
  13.     };  
  14.    
  15.     ++localCounter; // unseen by the block  
  16.     localCharacter = 'b';  
  17.    
  18.     aBlock(); // execute the block  
  19.     // localCharacter now 'a'  
  20. }  
 
对象和Block变量

Blocks支持Objecitve-C和C++对象,还包括其他的可以看成变量的blocks.

Objective-C对象

在引用计数的环境下,默认情况如果你引用了一个Objective-C对象,它将会被retain,即使你只是使用了这个对象的实体变量. 如果对象使用了 __block 存储修饰符,则不会被retain.

Note: 在垃圾回收的环境下,如果你对变量同时使用了 __weak 和 __block 修饰符, block将不会保证其生存期.

 

如果你在方法中使用了block,并且使用到了对象的实体变量,那么内存管理的规则将会更微妙:

  • 如果你使用了改实体变量的引用,则 self 将被retain;

  • 如果你是按指访问该实体变量,则存储那个实体变量将被retain.

下例展示这两种情况的差别:

 

Objecitve-c代码   收藏代码
  1. dispatch_async(queue, ^{  
  2.     // instanceVariable is used by reference, self is retained  
  3.     doSomethingWithObject(instanceVariable);  
  4. });  
  5.    
  6.    
  7. id localVariable = instanceVariable;  
  8. dispatch_async(queue, ^{  
  9.     // localVariable is used by value, localVariable is retained (not self)  
  10.     doSomethingWithObject(localVariable);  
  11. });  
 
C++对象

你可以在block内使用C++变量. 在成员函数中,对成员变量和函数的引用实际上都是隐式使用了 this 指针,故而都是可变的. 当block被拷贝的时候,有两种情况需要留心:

  • 如果你使用的是栈基(stack-based)的C++变量,并且有 __block 存储修饰符, 通常使用拷贝构造函数来构造对象.

  • 除此以外的栈基C++对象, 则必须要有const拷贝构造函数(形如 Object(const Object&o)),拷贝这些对象时将使用该方法.

Blocks

当拷贝一个block,该block内所有引用到的其他block都将被拷贝,如果改block使用到的block变量里引用到了别的block,则别的block也将被拷贝.

当拷贝一个栈基block,你将得到一个新block. 如果拷贝一个堆基的block,则仅仅是增加其引用计数,在拷贝函数和方法返回后还会降回去.

 

 

使用Blocks

调用Block

把一个block声明为一个变量,你就可以把它当做函数一样用,如下所示:

 

Objective-c代码   收藏代码
  1. int (^oneFrom)(int) = ^(int anInt) {  
  2.     return anInt - 1;  
  3. };  
  4.    
  5. printf("1 from 10 is %d", oneFrom(10));  
  6. // Prints "1 from 10 is 9"  
  7.    
  8. float (^distanceTraveled) (float, float, float) =  
  9.                           ^(float startingSpeed, float acceleration, float time) {  
  10.    
  11.     float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);  
  12.     return distance;  
  13. };  
  14.    
  15. float howFar = distanceTraveled(0.09.81.0);  
  16. // howFar = 4.9  
你也可以把block当参数传给函数或者方法.某些场合,你也可以"内联(inline)"的创建block.

使用Block作为函数参数

你可以传一个block当参数给函数,就和其他类型的参数一样. 很多情况,你都没必要声明blocks, 而是简单的在哪里需要就哪里直接创建. 下例展示使用qsort_b 函数. qsort_b 类似于标准的 qsort_r 函数,不过只是把最后一个参数改为block.

 

Objective-c代码   收藏代码
  1. char *myCharacters[3] = { "TomJohn""George""Charles Condomine" };  
  2.    
  3. qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {  
  4.     char *left = *(char **)l;  
  5.     char *right = *(char **)r;  
  6.     return strncmp(left, right, 1);  
  7. });  
  8. // Block implementation ends at "}"  
  9.    
  10. // myCharacters is now { "Charles Condomine""George""TomJohn" }  

 

注意block被整个包含在函数的参数列表中.

下例演示如何在 dispatch_apply 函数中使用block. dispatch_apply 声明如下:

 

Objective-c代码   收藏代码
  1. void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));  

 

函数将block投递到调度队列中供多次调用. 一共三个参数,其中第一个是调用总次数,第二个是投递到的队列,最后是block本身,而这个block带有一个参数,即当前被调用的次数.

 

可以仅仅使用 dispatch_apply 去打印打钱的调度下标,如下:

 

Objective-c代码   收藏代码
  1. #include <dispatch/dispatch.h>  
  2. size_t count = 10;  
  3. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
  4.    
  5. dispatch_apply(count, queue, ^(size_t i) {  
  6.     printf("%u\n", i);  
  7. });  

 

使用Block作为方法参数

 

Cocoa 提供了很多使用blocks的方法, 你可以传递block作为参数,就像调用其他的方法一样.

下例展示如何使用给定的过滤条件为数组排出前五个元素.

 

Objectvie-c代码   收藏代码
  1. NSArray *array = [NSArray arrayWithObjects: @"A", @"B", @"C", @"A", @"B", @"Z",@"G", @"are", @"Q", nil];  
  2. NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];  
  3.    
  4. BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);  
  5.    
  6. test = ^ (id obj, NSUInteger idx, BOOL *stop) {  
  7.    
  8.     if (idx < 5) {  
  9.         if ([filterSet containsObject: obj]) {  
  10.             return YES;  
  11.         }  
  12.     }  
  13.     return NO;  
  14. };  
  15.    
  16. NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];  
  17.    
  18. NSLog(@"indexes: %@", indexes);  
  19.    
  20. /*  
  21. Output:  
  22. indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]  
  23. */  

 

下例是判断一个 NSSet 对象是否包含了一个局部变量,并设置另一个局部变量(found),在查找到的情况下 为 YES (并停止查找) . 注意 found 被声明为了 __block 变量, 且block是用内联方式定义的:

 

Objective-c代码   收藏代码
  1. __block BOOL found = NO;  
  2. NSSet *aSet = [NSSet setWithObjects: @"Alpha", @"Beta", @"Gamma", @"X", nil];  
  3. NSString *string = @"gamma";  
  4.    
  5. [aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {  
  6.     if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame) {  
  7.         *stop = YES;  
  8.         found = YES;  
  9.     }  
  10. }];  
  11.    
  12. // At this point, found == YES  

拷贝Blocks

一般而言,你没有表去copy(或retain)一个block. 除非你希望使用该block在当前声明的范围销毁之后. 拷贝会将block移到堆(heap)中.

你可以使用C函数去拷贝或释放blocks:

 

C代码   收藏代码
  1. Block_copy();  
  2. Block_release();  
 

 

如果你正在使用Objective-C, 你可以向block对象发送  copyretain, 和  release (也包括  autorelease) 消息.

为了避免内存泄露, Block_copy() 和Block_release()必须要对应使用. 同样的也要对应 copy 或 retain 和 release (或 autorelease)的使用.除非是在垃圾回收的环境.

应避免的做法

block文本(即, ^{ ... }) 是表示block的局部栈数据结构(stack-local data structure)的地址. 所以这些栈数据仅在当前声明的范围内有效,必须避免如下的使用:

 

Objective-c代码   收藏代码
  1. void dontDoThis() {  
  2.     void (^blockArray[3])(void);  // an array of 3 block references  
  3.    
  4.     for (int i = 0; i < 3; ++i) {  
  5.         blockArray[i] = ^{ printf("hello, %d\n", i); };  
  6.         // WRONG: The block literal scope is the "for" loop  
  7.     }  
  8. }  
  9.    
  10. void dontDoThisEither() {  
  11.     void (^block)(void);  
  12.    
  13.     int i = random():  
  14.     if (i > 1000) {  
  15.         block = ^{ printf("got i at: %d\n", i); };  
  16.         // WRONG: The block literal scope is the "then" clause  
  17.     }  
  18.     // ...  
  19. }  

 

调试

 

你可以在blocks中设断点并单步跟踪. 你也可以在GDB里直接用 invoke-block命令调用blocks,如下所示:

 

Shell代码   收藏代码
  1. $ invoke-block myBlock 10 20  
 

 

如果要传递C的字符串,你必须用引用括起来, 比如把  this string 传给  doSomethingWithString block, 得这么写:
Shell代码   收藏代码
  1. $ invoke-block doSomethingWithString "\"this string\""  
   
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值