Block小结

一直使用Block,却没有认真研究过,简单记录一下。
Block的实质就是匿名函数,通过函数指针的调用来实现的,并对内部的引用到的数据进行管理(retain/release),封装后成为Block,最终变成对象。

1.Block结构

  void (^block)(void) = ^(void) {
        printf("hello world");
    };

通过clang -rewrite-objc filename 命令编译转换后:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("hello world");
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(){
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

 return 0;
}

大致就是通过调用指向匿名函数的函数指针来实现block,__block_impl存放着实现block的数据结构,上面是最基本的结构。
当block持有了变量,数据结构就会发生一些改变,变化体现在在struct __main_block_impl_0和struct __main_block_desc_0。

1.1

当是一个基本数据变量时,在__main_block_impl_0里面会加入一个新的变量,比如当block使用了age变量

printf("hello world:%d",age);//age 为外部变量

转换后的结构为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
        printf("my age is:%d", age);
    }

Desc变量后面多个一个int age的变量。

1.2

当Block中引用了对象时,不仅有上面的变化,__main_block_desc_0也会有变化

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

从上面可以看出,多了个copy和dispose函数,这俩其实就是retain和release,用来处理持有的对象,定义如下

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 3/*BLOCK_FIELD_IS_OBJECT*/);}

注释中也写明了是对象。

1.3

当引用了__block修饰的变量,比如block使用了__block NSOject *obj = [NSObject new],则会有一个新的数据结构体来表示__block修饰的变量,那就是__Block_byref结构体,

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

可以看到最后有个NSObject类型的obj变量。
而__main_block_impl_0中的变量属性则会变成上面结构体类型,结构体的名字和变量名有关。

  __Block_byref_obj_0 *obj; // by ref

block_desc结构体中也发生变化

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

多了个copy和dispose函数,函数调用时的参数是BLOCK_FIELD_IS_BYREF,表明在copy/release时处理block_impl中的obj是byref类型。
而且在对__main_block_impl_0的变量进行初始化时,传递的obj值是这样的初始化的

    __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {(void*)0,(__Block_byref_obj_0 *)&obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};

可以看到,Block_byrey结构体中的obj变量,还是通过objc_msgSend方式实现的,flag是33554432,代表BLOCK_HAS_COPY_DISPOSE,同时将__Block_byref_id_object_copy_131传给了__Block_byref_id_object_copy,__Block_byref_id_object_dispose_131传给了__Block_byref_id_object_dispose,这两个函数的定义如下

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

也是通过_Block_object_assign和_Block_object_dispose来管理变量的,参数131代表BLOCK_BYREF_CALLER(128)+BLOCK_FIELD_IS_OBJECT(3)。
当__block修饰的是基本数据类型时,则__Block_byref中没有copy和dispose函数。至于为何要多一个__forwarding的处理,我们后续再说。

2 Block的类型

常见的类型分为三种,全局__NSGlobalBlock__,栈__NSStackBlock__,堆__NSMallocBlock__,
NSStackBlock__类型copy时会变为__NSMallocBlock,__NSMallocBlock__类型copy时,对引用计数+1。
__NSGlobalBlock__类型是block内部不引用任何变量或全局block,对该类型copy或release时返回原值。

__NSGlobalBlock__型

 void (^block)(void) = ^(void){
       NSLog(@"hello world");
   };

NSMallocBlock__型,在arc下赋值的时会自动插入objc_retainBlock,所以会变成__NSMallocBlock,如果在非ARC下就是__NSStackBlock__型

int a = 0;
void (^block1)(void) = ^(void){
        NSLog(@"hello world,%d",a);
    };
    

NSStackBlock

int a = 0;
__unsafe_unretained void (^block1)(void) = ^(void){
        NSLog(@"hello world,%d",a);
    };

NSLog(@"%@",^(void){
        NSLog(@"hello world,%d",a);}
        );

各自的继承关系
__NSStackBlock__继承自__NSStackBlock,__NSStackBlock继承自NSBlock,NSBlock继承自NSObject。
__NSMallocBlock__继承自__NSMallocBlock,__NSMallocBlock继承自NSBlock。
__NSGlobalBlock__继承自__NSGlobalBlock,__NSGlobalBlock继承自NSBlock。

__NSStackBlock、__NSMallocBlock、__NSGlobalBlock和NSBlock定义在CoreFoundation,

    // 570425344 = 536870912(BLOCK_USE_STRET) + 33554432(BLOCK_HAS_COPY_DISPOSE)
    // 1342177280 = 1073741824(BLOCK_HAS_SIGNATURE) + 268435456(BLOCK_IS_GLOBAL)
    // 1375731712 = BLOCK_HAS_SIGNATURE(1073741824) + BLOCK_IS_GLOBAL(268435456) + BLOCK_HAS_COPY_DISPOSE(33554432)

3 一个完整的流程

通常我们对Block的copy实际是调用了NSBlock的copy方法,而它的copy方法也很简单,直接调用了libsystem_blocks.dylib中的_Block_copy。这是专门来处理block的框架,代码是开源的。
先看下flag的定义

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

BLOCK_IS_GLOBAL表示Global,BLOCK_NEEDS_FREE表示Malloc。

和blocks框架中的结构体定义

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#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
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

对比上面的clang转换后的代码,我们发现不同的是一些变量在不同的结构体中,但总体数据的顺序是一致的,这种定义更直观一些。
_Block_copy的实现

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;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);//增加引用计数flags+2
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;//Gbloal直接返回
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        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
        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;
    }
}

对Global和Malloc类型的处理都比较简单,对Stack类型的处理复杂一些,因为可能涉及到对持有变量和block自身的复制。
首先是malloc分配内存区域,然后复制size大小的数据到新的block中,将flag设置为BLOCK_NEEDS_FREE,表示是Malloc类型,并将flag加2,表示引用计数加1,然后调用_Block_call_copy_helper函数处理其他数据,最后将isa置为_NSConcreteMallocBlock,然后然后新的block。

// 如果有copy和dispose,就返回
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;
}

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

对于只持有基本数据类型的block来说,是没有copy和dispose的,所以_Block_call_copy_helper里面什么也没做。对于持有对象的block来说,最终走到copy函数。以上文的1.2为例,其copy函数为_Block_object_assign,函数共有三个参数,最后一个为3/BLOCK_FIELD_IS_OBJECT/。

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];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *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:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}

根据case判断,会执行_Block_retain_object,在开源代码中,_Block_retain_object其实就是_Block_retain_object_default,而后者是空函数,实际上肯定不会搞一个这样的空函数,代码中开源隐藏了一些代码实现。

static void _Block_retain_object_default(const void *ptr __unused) { }

static void _Block_release_object_default(const void *ptr __unused) { }

static void _Block_destructInstance_default(const void *aBlock __unused) {}

static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default;
static void (*_Block_destructInstance) (const void *aBlock) = _Block_destructInstance_default;

在Xcode中打上_Block_object_assign符号断点,其中有call _Block_retain_object的指令,追踪进去,直接变为libobjc.A.dylib中的objc_retain,开源代码中确实隐藏了一些信息。
对于持有的变量是block,则再执行_Block_copy,就是copy一下block,此处禁止套娃😂。
_Block_byref_copy函数是来处理持有__block修饰的变量。
我们以__block修饰的基本数据类型变量为例。上面我们说过在这种情况下,生成的__Block_byref结构体内是没有copy和dispose函数的,
还是先对bloack进行了copy。然后执行_Block_call_copy_helper,获取到copy函数,即_Block_object_assign。完整调用是

_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);

在函数内部会执行到,其将copy后的结果返回

*dest = _Block_byref_copy(object);

函数内部

static struct Block_byref *_Block_byref_copy(const void *arg) {
    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;
        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;
}

1,整个流程先判断是否在堆中,是则调用latching_incr_int累加引用计数,否则进行copy。对flag加上BLOCK_BYREF_NEEDS_FREE,表示在堆中,再加上4,表示2个引用。关键代码则是forwarding的赋值,都指向了copy。在block代码中使用到的__block变量,其实都是通过obj->__forwarding->obj这种方式使用的,这样保证了原block释放后,后续copy的block持有的__block的变量也不受影响。

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
    void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
    void (*byref_destroy)(struct Block_byref *);
};
struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};

可以看出和__Block_byref结构体相同,不过是分成了3个。Block_byref_2是可能存在的。Block_byref_3中的layout就是__block修饰的变量。
2,判断是否有copy和release函数。没有函数其实就是基本数据类型的变量,copy结构体中size变量后面的数据到新的Block_byref中去,src+1即size后面的数据。有则先复制其copy和release函数,如果有变量值则再将值赋给新的block,然后调用copy函数。copy函数就是__Block_byref_id_object_copy_131。其内部又调用了_Block_object_assign,来对是持有的变量进行copy。变量的位置就是40个字节的位置,flags和size共占8个字节。在_Block_object_assign内部,flags是131则又做了一次赋值。此处有一个问题源码中是直接赋值

*dest = object;

这种引用计数不会变化,也就是没有强制引用,但从实际来看,__block修饰的变量,在block内部确实是强制引用了的,

_Block_byref_copy最后返回了src->forwarding,即copy后的byref。

大致流程图
在这里插入图片描述
对流程的一些说明,
1.desc中copy是否存在是根据block中引用的外部变量的类型决定的,基本数据类型是没有copy的。
2.调用_Block_object_assign函数时,变量类型是事先指定好的。
3.byref中copy是否存在是根据__block修饰的变量类型决定的,基本数据类型是没有copy的。

_Block_use_RR2函数用来保存retain、release和destructInstance函数,前2个处理block持有的对象,后1个在block销毁时处理block自身,调用时机是在libSystem进行初始化时,libSystem_initializer调用libdispatch_init,然后_os_object_init,再调用的_Block_use_RR2。
_os_object_init中调用如下

Block_callbacks_RR callbacks = {
	sizeof(Block_callbacks_RR),
	(void (*)(const void *))&objc_retain,
	(void (*)(const void *))&objc_release,
	(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
static void*
_os_objc_destructInstance(id obj)
{
	// noop if only Libystem is loaded
	return obj;
}

_Block_use_RR2中保存了函数指针。

void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}

根据源码来看_Block_destructInstance保存的是_os_objc_destructInstance,但实际中_Block_destructInstance却指向了libobjc中的objc_destructInstance,这是怎么回事?原来_Block_use_RR2又被调用了,这次是CoreFoundation的__CFInitialize中调用了CFMakeNSBlockClasses,这里面初始化了各种Block类型,除了前面说的3种,还有NSAutoBlock、NSFinalizingBlock等。然后调用了_Block_use_RR2,将_Block_destructInstance指向了libobjc中的objc_destructInstance,不过这部分代码没有开源。

void _Block_release(const void *arg) {
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;
    if (aBlock->flags & BLOCK_IS_GLOBAL) return;
    if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;

    if (latching_decr_int_should_deallocate(&aBlock->flags)) {
        _Block_call_dispose_helper(aBlock);
        _Block_destructInstance(aBlock);
        free(aBlock);
    }
}

_Block_release里面对Global类型的直接返回,只处理MallocBlock,先将引用计数减2(为了方便位运算),若没有引用则进行销毁。先调用block内部的release,处理持有的变量。再处理block自身。最后释放掉。

4 Block的签名

签名其实就是字符串,描述了Block的参数和返回值。类似method_getTypeEncoding函数。

    int(^block)(id obj) = (id)^(id obj){
        NSLog(@"obj:%@", obj);
        return 10;
    };

签名就是"i16@?0@8",i代表返回值类型int,@?表示block自身,最后的@代表参数类型。
如果引用了变量,变量存放在desc指针的后面,即block+32字节的位置。如果是__block修饰的。这个位置存放的是bryef类型的指针,具体的对象在偏移20的字节的位置
关于Block内部的强引用问题,见block中 使用 __weak和__strong

来源
1 https://opensource.apple.com/source/objc4/objc4-706/
2 https://opensource.apple.com/source/libclosure/libclosure-67/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值