OC底层探索(二十三)Block用法及原理

OC底层文章汇总

Block的使用

  • return_type表示返回对象/关键字等(可以是void,并省略)

  • blockName表示block的名称

  • var_type表示参数类型(可以是void,并省略)

  • varName表示参数名称

return_type表示返回的对象/关键字等(可以是void,并省略)

blockName表示block的名称

var_type表示参数的类型(可以是void,并省略)

Block声明及定义语法

  1. varName表示参数名称
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);
  1. 当返回类型为void
void (^blockName)(var_type) = ^void (var_type varName) { // ... };
blockName(var);

可简写为:

void (^blockName)(var_type) = ^(var_type varName) { // ... };
blockName(var);
  1. 当参数类型为void
return_type (^blockName)(void) = ^return_type (void) { // ... };
blockName();

简写为:

return_type (^blockName)(void) = ^return_type { // ... };
blockName();
  1. 当返回类型和参数类型都为void
void (^blockName)(void) = ^void (void) { // ... };
blockName();

可简写为:

void (^blockName)(void) = ^{ // ... };
blockName();
  1. 匿名Block

Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:

^return_type (var_type varName)
{ //... };

typedef简化Block的声明

  1. 声明
typedef return_type (^BlockTypeName)(var_type);
  1. 作为属性
//声明 typedef void(^ClickBlock)(NSInteger index); //block属性 @property (nonatomic, copy) clickBlock ClickBlock;
  1. 作方法参数

声明 typedef void (^handleBlock)(); //block作参数

- (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
  ...

block循环引用的解决方案

环境准备

.m文件

typedef void(^QBlock)( void);
@interface ViewController ()
@property (nonatomic, copy) QBlock block;
@property (nonatomic, copy) NSString *name;

@property (nonatomic, strong) UITableView *tableView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循环引用
    self.name = @"lg_cooci";

   
    self.block = ^(void){
        NSLog(@"%@",self.name);
    };
    self.block();
    
    // block 

}
- (void)dealloc{
    NSLog(@"dealloc 来了");
}

@end

1. weak-strong-dance (强弱共舞)

weak-strong中介者模式

  • 添加 __weak typeof(self) weakSelf = self;

    • __weak是将对象存储到弱引用表
 __weak typeof(self) weakSelf = self;
    self.block = ^(void){
       NSLog(@"%@",weakSelf.name);
    };
    self.block(self);

此时还是有问题:如果在block内的执行延时处理呢?

    __weak typeof(self) weakSelf = self;
    self.block = ^(void){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name);
        });
    };
    self.block();
  • 通过代码执行就会发现weakSelfnil!!!

    • 执行延时操作期间,如果执行了析构函数,当前的self释放了,weakSelf释放了,所以变成了nil
  • 在block内添加__strong __typeof(weakSelf)strongSelf = weakSelf;

    • 延长 weakSelf生命周期
   __weak typeof(self) weakSelf = self;
    self.block = ^(void){
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };

2. __block

  • 添加 __block ViewController *vc = self;

typedef void(^KCBlock)(ViewController *);

   __block ViewController *vc = self;
    self.block = ^(void){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
    };
    self.block();

3. 当做参数传入

  • 将当前self当做参数传入
 self.block = ^(ViewController *vc){

self.block = ^(ViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.block(self);
    

4.proxy

TODO:后期添加

Block与内存管理

根据Block在内存中的位置分为种类型:

  • NSGlobalBlock是位于全局区的block,它是设置在程序的数据区域(.data区)中。

  • NSStackBlock是位于栈区,超出变量作用域,栈上的Block以及 __block变量都被销毁。

  • NSMallocBlock是位于堆区,在变量作用域结束时不受影响。

1. 位于全局区:GlobalBlock

  • 定义全局变量的地方有block语法时
void(^block)(void) = ^ { NSLog(@"Global Block");}; int main() {
 
}
  • block语法的表达式中没有使用应截获的自动变量时
int(^block)(int count) = ^(int count) { return count;
    };
 block(2);

2、 位于栈内存:StackBlock

  • block语法的表达式中使用截获的自动变量时
NSInteger i = 10; 
block = ^{ NSLog(@"%ld", i); 
};
block;

3. 位于堆内存:MallocBlock

中的block无法直接创建,其需要由_NSConcreteStackBlock类型的block拷贝而来(也就是说block需要执行copy之后才能存到堆中)。由于block的拷贝最终都会调用_Block_copy_internal函数。

void(^block)(void); int main(int argc, const char * argv[]) { @autoreleasepool {

       __block NSInteger i = 10;
       block = [^{
           ++i;
       } copy];
       ++i; 
       block(); NSLog(@"%ld", i);
   } return 0;
}

block分析

1. block的本质

  • 新建一个.c文件,声明一个block,执行clang,查看.cpp文件
#include "stdio.h"

int main(){

    void(^block)(void) = ^{
        printf("qqq - %d",a);
    };
    
    // block();
    return 0;
}
  • .c文件与.cpp文件的对比

在这里插入图片描述

  • 查看__main_block_impl_0的类型,

    • 结构体 : 明显的发现block是一个结构体
    • 匿名函数: __main_block_impl_0还是一个构造函数
      在这里插入图片描述
  • 函数的调用执行

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

可简化为:

  • 其中block作为隐藏参数
block->FuncPtr(block);

2. block自动捕获外界变量

  • 添加一个a变量
#include "stdio.h"

int main(){

     int a = 11;
    void(^block)(void) = ^{
        printf("qqq - %d",a);
    };
    
     block();
    
    return 0;
}
  • clang一下

    • 在声明block时,传入参数a
    • 在编译时, __main_block_impl_0 结构体新增一个变量a
    • 在方法__main_block_func_0执行函数中,新建a变量,并对传入的进行值拷贝
      在这里插入图片描述
  • 如果在__main_block_func_0的执行方法中,对a进行操作(+1),就会发生代码歧义,导致报错
    在这里插入图片描述

++++++++++++++++++华丽丽的的分割线+++++++++++++++++++++

  • a变量上添加__block
#include "stdio.h"

int main(){

     __block int a = 11;
    void(^block)(void) = ^{
        a++;
        printf("qqq - %d",a);
    };
    
     block();
    
    return 0;
}
  • clang一下

    • block新建一个__Block_byref_a_0类型的a变量
    • 新建block时,传入a地址
    • 函数调用时,新建一个__Block_byref_a_0类型的a变量,并将blocka指针 赋值新建__Block_byref_a_0类型的a变量,如果对a进行了操作block也会发生改变

在这里插入图片描述

3. block底层探索

3.1 找到源码

  • 新建一个工程,写一个全局block,并且在block处打一个断点
    在这里插入图片描述

  • 打开汇编调试
    在这里插入图片描述

  • 运行查看,就会发现objc_retainBlockobjc_storeStrong
    在这里插入图片描述

  • 添加符号断点objc_retainBlock,并进入查看,发现执行的是_Block_copy
    在这里插入图片描述

在这里插入图片描述

  • 向下执行,就会发现_Block_copylibsystem_blocks.dylib,下载block源码
    在这里插入图片描述

3.2 MallocBlock(堆区block)第一次拷贝

  • 读取当前寄存器,查看一下当前是在什么位置,在全局区位置

    • 验证了上述block的内存区域 位置
      在这里插入图片描述
  • 修改代码,在外部添加一个变量,并在block使用,查看寄存器

    • 发现竟然在区,我们知道此时block应该位于

在这里插入图片描述
在这里插入图片描述

  • 继续向下执行_Block_copy,在最后一行retq打一个断点,再读取一下寄存器

    • 发现竟然跑到区中了在这里插入图片描述

小结:
引用外部变量的block第一次拷贝流程:
stackBlock栈区block => _block_copy => mallocBlock堆区block

3.3 源码分析

1. block的结构
  • 在cpp文件中,我们发现block来自from Block_private.h文件中
    在这里插入图片描述

  • Block_private.h文件中,我们发现方法的传入参数都是Block_layout类型,猜测block的底层可能是Block_layout结构体。查看Block_layout

    • 第一个参数:isa
    • 第二个参数:flags – 标识符
    • 第三个参数:reserved – 感觉没意义
    • 第四个参数: invoke – 方法
    • 第五个参数:descriptor – block的descriptor
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};

  • flags 标识符,注意三个标识符

    • 是否有析构 : BLOCK_DEALLOCATING = (0x0001)
    • 是否进行复制:BLOCK_HAS_COPY_DISPOSE = (1 << 25)
    • 是否有签名:BLOCK_HAS_SIGNATURE = (1 << 30)

在这里插入图片描述

  • descriptorBlock_descriptor_1Block_descriptor_2Block_descriptor_3,其中Block_descriptor_2Block_descriptor_3是可选的

  • Block_descriptor_1

    • Block_descriptor_1 的属性分别为:reserved接收者和size大小
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
  • Block_descriptor_1方法实现
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
    return aBlock->descriptor;
}
  • Block_descriptor_2

    • Block_descriptor_2 只有当flagsBLOCK_HAS_COPY_DISPOSE时,才会存在
    • 属性分别为:copydispose
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};
  • Block_descriptor_2方法实现
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}
  • Block_descriptor_3

    • Block_descriptor_3 只有当flagsBLOCK_HAS_SIGNATURE时,才会存在
    • 属性分别为:signature方法签名、layout
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
  • Block_descriptor_3方法实现
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}
2. _block_copy 从栈区拷贝到堆区
  • 查看_Block_copy源码

    • 判断flags是否是BLOCK_NEEDS_FREE(释放)

      • BLOCK_NEEDS_FREE, 返回 栈区的aBlock
    • 判断flags是否是全局block

      • 全局block,返回 栈区的aBlock
    • 以上都不是,那就会copy

      • 开辟一个空间result

      • 区的aBlock 拷贝区的result

        • blockisa指向_NSConcreteMallocBlock
        • 函数指针的赋值
        • flags的赋值
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    //判断flags是否是BLOCK_NEEDS_FREE(释放)
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    //判断flags是否是全局block
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock; // 不需要
    }
    else { // 栈
        // Its a stack block.  Make a copy.
        //在堆区开辟一个空间
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        //将栈区的aBlock拷贝到堆区的result
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        //函数指针的赋值
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        //flags的赋值
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
3. block对外部变量的捕获
  • 查看Block_byref的源码:Block_byref、Block_byref_2和Block_byref_3。其中Block_byref_2和Block_byref_3是可选

    • 只有是BLOCK_BYREF_HAS_COPY_DISPOSE时,才会向Block_byref添加Block_byref_2
    • 只有是BLOCK_BYREF_LAYOUT_EXTENDED时,才会向Block_byref添加Block_byref_3
struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep; // 结构体 __block  对象
    BlockByrefDestroyFunction byref_destroy;
};

struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};
  • 添加block捕获外部由__block修饰的对象类型的变量,
   __block NSString *name = [NSString stringWithFormat:@"qqqwww"];
           void (^block1)(void) = ^{
               name = @"www";
                NSLog(@"qqq_Block - %@",name);
            };
            block1();
  • clang一下查看编译后的源码

    • 使用__block修饰的变量,在生成byref对象时,会添加两个函数指针属性:__Block_byref_id_object_copy__Block_byref_id_object_dispose

    • 其中__Block_byref_id_object_copy函数中执行的是_Block_object_assign,传入的是对象类型的参数。

      • 由于__Block_byref_name_0存在__isa__forwarding__flags__size(*__Block_byref_id_object_copy)(void*, void*)(*__Block_byref_id_object_dispose)(void*)name最后才是字符串类型的name,所以需要首地址平移40才可以获取到name

在这里插入图片描述

在这里插入图片描述

  • 在上面cpp文件中,可以看到__main_block_copy_0__main_block_dispose_0函数中调用的都是_Block_object_assign
    在这里插入图片描述

  • 在源码中查看_Block_object_assign

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
   
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
     
        //对象类型
        case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // objc 指针地址 weakSelf (self)
            // arc
        _Block_retain_object(object);
            // 持有
        *dest = object;
        break;
        //Block对象类型
      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
            
            // block 被一个 block 捕获

        *dest = _Block_copy(object);
        break;
    //__weak 和 __block
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
       
        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
       

        *dest = object;
        break;

      default:
        break;
    }
}
  • 其中 枚举为:
enum {
    // see function implementation for a more complete description of these fields and combinations
    //对象类型
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    //block类型
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    //__block修饰
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    //__weak修饰
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

  1. BLOCK_FIELD_IS_OBJECT 对象类型
 //对象类型
        case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // objc 指针地址 weakSelf (self)
            // arc
        _Block_retain_object(object);
            // 持有
        *dest = object;
        break;
  • 执行_Block_retain_object,查看源码

    • _Block_retain_object函数中什么都没有操作
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

static void _Block_retain_object_default(const void *ptr __unused) { }
  • 计数加一 持有状态:新建一个指针指向变量地址
 *dest = object;
  1. BLOCK_FIELD_IS_BLOCK block类型的对象
case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
            
            // block 被一个 block 捕获

        *dest = _Block_copy(object);
        break;
  • 继续执行_Block_copy
  1. BLOCK_FIELD_IS_BYREFBLOCK_FIELD_IS_WEAK __block和__weak修饰的对象
//__weak 和 __block
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        
        *dest = _Block_byref_copy(object);
        break;
  • 查看_Block_byref_copy方法

    • cpp文件中,可以看出,在编译时,将外接捕获对象变成了Block_byref类型的结构体。所以传入的对象argBlock_byref结构体类型的对象

    • 开辟一个内存空间copy,将传入arg对象copy一份,且arg->forwardingcopy->forwarding都指向同一个内存空间copy这是第二次拷贝!!!

    • 由于在编译时,生成了Block_byref_2对象,由执行了一遍_Block_object_assign,传入的是一个对象类型的参数,又进行了一次copy这是第三次拷贝!!!

static struct Block_byref *_Block_byref_copy(const void *arg) {
    
    // Block_byref  结构体
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        
        // 问题 - block 内部 持有的 Block_byref 锁持有的对象 是不是同一个
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

小结:

  • block 是一个结构体
  • block在捕获外界变量时:执行的copy次数
    • _block_copy中执行了一次copy,从栈区copy到堆区

    • 对象类型一共执行两次copyBLOCK_FIELD_IS_OBJECT: 值拷贝

    • block类型对象:BLOCK_FIELD_IS_BLOCK,继续执行_Block_copy

    • __weak、__block修饰的对象,一共执行三次copyBLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK

      • _Block_byref_copy 执行一次
      • keep中调用_Block_object_assign,执行了一次值拷贝
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OC(Objective-C)是一种面向对象的编程语言,它支持在类中使用方法和block。类方法是在类中定义的方法,可以直接通过类名来调用,而不需要实例化对象。而block是一种闭包,可以在代码中定义并传递给方法或函数,用于延迟执行特定的逻辑。 在OC中,可以使用类方法来创建和操作类的实例,例如通过一个工厂方法创建对象,或者在类方法中实现一些与类相关的逻辑。类方法通常使用“+”符号进行声明和实现。 而block可以在方法中作为参数传递,也可以在方法中定义和使用。block可以捕获其所在作用域的变量,可以在方法内部延迟执行一段代码,也可以用于实现回调等功能。block的定义和使用使用“^(){}”语法。 类方法和block可以结合使用,例如可以在类方法中接受一个block作为参数,并在合适的时机调用该block,以实现一些灵活的逻辑。通过类方法和block的组合,可以在OC中实现更加灵活和强大的功能,例如在异步操作中使用block来回调结果,或者在工厂方法中使用block来定制对象的初始化逻辑等。 总而言之,类方法和blockOC中的两个重要特性,它们可以分别用于类的操作和延迟执行逻辑,也可以结合使用以实现更加灵活的功能。在实际的OC开发中,类方法和block通常会被广泛使用,可以帮助开发者更加简洁和灵活地实现代码逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值