ios 底层 Block探索

ios 底层 Block探索

IOS小彩虹2021-08-21 14:43:08330A+A-

Block的分类

Block一共有6种分类,常用的有三种。

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

常用的如下:

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

上面这种block的打印结果为<__NSGlobalBlock__: 0x10cbfe088>,即为全局的block

NSLog(@"%@",^{
    NSLog(@"1233 - %d",a);
});

这样直接打印block打印出来的是<_NSStackBlock__: 0x7ffeeb763440>_,即为栈block

int a = 10;
void (^block)(void) = ^{
    NSLog(@"1233 - %d",a);
};

这种block的打印结果为<__NSMallocBlock__: 0x600002cd8090>,即为堆block

上面这三种block就是我们在开发中常见的三种block,除此之外还有三种系统的block。我们可以在block的源码中看到下面六中block。

Block循环引用

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

我们知道上面的代码会引起循环引用。因为形成了self--->block--->self的循环引用。 我们可以通过__weak来打破这种闭环。

self.name = @"HelloBlock";
__weak typeof(self) weakSelf = self;
self.block = ^{
    NSLog(@"%@",weakSelf.name);
};
self.block();

添加了__weak后虽然不会引起循环引用了。但是如果我们像下面代码一样进行了延迟调用,然后在延迟期间,我们退出了当前的viewControllerself(即当前的ViewController)就会释放,然后打印weakSelf.name的结果就成为了null

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

延迟打印的结果是null,这不是我们期望的结果。我们希望block打印完成后再释放self,这样既不会循环引用,也可以获取到正常的name的值。

我们只需要在block里面加一个strongSelf,就可以解决。这样的话,block执行完成后才会给strongSelf发送release消息,strongSelf释放;然后viewController才会释放。

这种方式属于中介者模式,使用到了weakSelfstrongSelf作为中介者。

self.name = @"HelloBlock";
__weak typeof(self) weakSelf = self;
self.block = ^{
    __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);
    });
};
self.block();

我们还可以通过下面的这种中介者模式来防止循环引用,添加一个vc,然后在block执行完成后手动置为nil,从而打破了循环引用的闭环。但是这种方式一定要执行block,如果不执行的话,还是会导致循环引用。

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

我们还可以通过block传参的形式来防止循环引用问题,代码如下:

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

再次我们总结一下防止循环引用的三种方式:

  • _weak
  • 中介者模式,在block执行完之后,手动设置为nil
  • block传参

Block底层探索

    1. block的本质是什么
    1. block为什么需要调用
    1. block自动捕获外界变量
    1. __block的原理 block.c文件中内容如下
#include "stdio.h"
int main(){
    void(^block)(void) = ^{
        printf("HelloBlock");
    };
    
    block();
    return 0;
}

然后我们在终端中切换到该文件目录下,使用clang命令来探索一下这段代码对应的编译器底层的代码

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk block.c

这样我们就得到了一个block.cpp文件,文件的主要内容如下(经过简化):

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("HelloBlock");
}

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) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

    block->FuncPtr(block);
    return 0;
}

该文件中的main函数对应我们block.c中的main函数。就是简单的申明block和调用block。

void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

这句为block的声明,通过上面的代码可知,其中的__main_block_impl_0是一个struct结构体,上面的代码就是构建了一个结构体。所以Block的本质就是结构体。构建结构体的时候传递了两个参数,一个是__main_block_func_0,另一个是__main_block_desc_0_DATA。其中的__main_block_func_0是一个打印的函数,也就是我们block块内的实际代码内容。

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

将__main_block_func_0保存在了FuncPtr属性中。调动Block的时候就是通过FuncPtr来调用的

block->FuncPtr(block);

所以Block的本质是结构体。block定义的时候将block函数保存到结构体中,然后block调用的时候,从结构体中取出函数进行调用。

我们在block.c文件中添加一个外界变量a,然后在block中访问该变量a,然后再通过clang命令生成对应的block.cpp文件,关键的内容如下:

// 申明Block时初始化的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    //保存block的函数
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block函数内容
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
    
        printf("HelloBlock - %d",a);
    }

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(){

    int a = 10;
    //生命block
    void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);

    //调用block
    block->FuncPtr(block);
    return 0;
}

main函数后多了一个a变量,然后创建结构体的时候将a作为参数传递了进去,在结构体的构建方法的最后有个:a(_a),就是讲参数_a赋值给了__main_block_impl_0结构体的属性a

int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    //保存block的函数
    impl.FuncPtr = fp;
    Desc = desc;
  }

在block调用的函数中,将结构体属性a通过值拷贝赋值给了一个新的变量a。所以这样没法直接对外面我们自己的变量a进行操作。

// block函数内容
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    printf("HelloBlock - %d",a);
}

至此我们知道了block是怎么捕获外界变量的:通过生成一个属性来保存变量值。

下面我们将外界变量a换成__block修饰,然后在block里面进行修改。代码如下

int main(){
    __block int a = 10;
    void(^block)(void) = ^{
        a++;
        printf("HelloBlock - %d",a);
    };
    block();
    return 0;
}

对应的clang生命的c++代码如下:

// block构建的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block对应函数的调用
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   __Block_byref_a_0 *a = __cself->a; // bound by ref

    (a->__forwarding->a)++;
    printf("HelloBlock - %d",(a->__forwarding->a));
}

// __block变量对应结构体的定义
struct __Block_byref_a_0 {
    void *__isa;
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

int main(){
    // __block变量生成的结构体
    __Block_byref_a_0 a = {
        (void*)0,
        (__Block_byref_a_0 *)&a,
        0,
        sizeof(__Block_byref_a_0),
        10
    };
    
    // block的声明
    void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

    // block的调用
    block->FuncPtr(block);
    return 0;
}

可见,__block修饰的变量a被操作成了一个结构体__Block_byref_a_0,该结构体中保存了变量a的地址和值。在block的回调函数中将结构体a的指针赋值给了a变量,然后通过a->__forwarding->a访问到结构体中保存的外面变量a的地址,然后实现对外面变量a的++操作。

//block对应函数的调用
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   __Block_byref_a_0 *a = __cself->a; // bound by ref

    (a->__forwarding->a)++;
    printf("HelloBlock - %d",(a->__forwarding->a));
}

__block修改外面变量的原理:为对应的__block变量生成了一个结构体,该结构体中保存了外界变量的指针和值,传递了一个指针递给给block函数调用。从而可以实现修改外面变量。

Block的源码分析

我们可以在openSource上找到block的开源源码libclosure。下面来看下源码内容:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};
// 可选
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#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_layout结构体就是block的结构。Block_descriptor_2Block_descriptor_3block的可选属性,block中是否存在这两个属性需要由Block_layout结构体中的flags属性来决定。

flags的定义如下:

// 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_HAS_COPY_DISPOSE用来标识是否存在Block_descriptor_2;

BLOCK_HAS_SIGNATURE用来标识是否存在Block_descriptor_3

BLOCK_IS_GLOBAL用来标识是否存在为全局的block

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

如果flags & BLOCK_HAS_COPY_DISPOSE为假,Block_descriptor_2返回为NULL。否则就可以通过Block_descriptor_1进行内存偏移访问到Block_descriptor_2.

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

如果flags & BLOCK_HAS_SIGNATURE为假,Block_descriptor_3返回为NULL。否则就可以通过Block_descriptor_1Block_descriptor_2进行内存偏移访问到Block_descriptor_3

Block签名

下面我们看下Block的签名信息

#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;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};

我们从closure源码可以看到,blocksignature签名位于Block_descriptor_3中,而Block_descriptor_3可以根据 Block_layout中的flags判断是否存在。下面我们通过汇编来追踪下signature的内容。 最简单的方法,我们直接打印block也可以看到signature的内容:

po 0x000000016bbc1b98
<__NSStackBlock__: 0x16bbc1b98>
 signature: "v8@?0"
 invoke   : 0x104242140 (/private/var/containers/Bundle/Application/02DB297F-AB97-41D9-BE48-0D468E73DA77/004-Block结构与签名.app/004-Block结构与签名`__29-[ViewController viewDidLoad]_block_invoke)

我们还可以通过地址偏移来追踪signature的内容

(lldb) po 0x00000002834040f0
<__NSMallocBlock__: 0x2834040f0>
 signature: "v8@?0"
 invoke   : 0x104242140 (/private/var/containers/Bundle/Application/02DB297F-AB97-41D9-BE48-0D468E73DA77/004-Block结构与签名.app/004-Block结构与签名`__29-[ViewController viewDidLoad]_block_invoke)

(lldb) x/4gx 0x00000002834040f0
0x2834040f0: 0x00000001f490a0f0 0x00000000c1000002
0x283404100: 0x0000000104242140 0x0000000104244018
(lldb) x/4gx 0x0000000104244018
0x104244018: 0x0000000000000000 0x0000000000000024
0x104244028: 0x0000000104243366 0x0000000104243419
(lldb) po (char*)0x0000000104243366
"v8@?0"

上图中第一个x/4gx是打印的block结构体对象的内存地址,其中第四段是Block_descriptor_1的地址,然后接着我们使用x/4gx打印Block_descriptor_1的内容地址,通过flags可以知道Block_descriptor_2不存在,只存在Block_descriptor_1Block_descriptor_3。其中的0x0000000000000020就是Block_descriptor_10x000000010078f34c就是Block_descriptor_3。 因为在Block_descriptor_3中第一个变量就是signature,所以直接就可以打印出signature了。

现在我们知道了Block的签名形式为@?的形式,@代表对象,代表未知的。就是指的block对象。

block的拷贝

// 栈 -> 堆 研究拷贝
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);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {//全局的block不需要拷贝,直接返回
        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;
        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;
    }
}

我们closure源码中_Block_copy的方法内容很简单,就是判断如果是全局的block的话不需要拷贝,直接返回。否则就是栈block,在堆区创建一个,然后把invokeflagsdescription等都拷贝过去,然后将isa设置为MallocBlock。这样就完成了block本身的拷贝

__block变量的堆拷贝

__block NSString *lg_name = [NSString stringWithFormat:@"cooci"];
void (^block1)(void) = ^{ // block_copy
    lg_name = @"LG_Cooci";
    NSLog(@"LG_Block - %@",lg_name);
};
block1();

切换到文件目录下,使用clang令xcrun -sdk iphoneSimulator clang -rewrite-objc main.m,就会生成main.cpp文件。在cpp文件中我们发现下面的与copy相关的函数

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->lg_name, (void*)src->lg_name, 8/*BLOCK_FIELD_IS_BYREF*/);
}

然后调用了_Block_object_assign函数。我们到closure源码中查找这个函数

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:
            
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:

        *dest = _Block_copy(object);
        break;

      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
            
        *dest = _Block_byref_copy(object);
        break;

      default:
        break;
    }
}

switch判断, BLOCK_FIELD_IS_OBJECT为对象;

BLOCK_FIELD_IS_BLOCKblock

BLOCK_FIELD_IS_BYREF__block修饰的变量。

之前我们研究了_Block_copy也就是block的拷贝。下面我们看下其他的拷贝。

如果是BLOCK_FIELD_IS_BLOCK,也就是对象类型,调用了_Block_retain_object,但是该方法中什么都没有做,因为如果是对象类型的话会交给ARC来处理。

如果是BLOCK_FIELD_IS_BYREF类型的话,也就是__block修饰的结构体变量,调用了_Block_byref_copy方法。

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;
        
        // 问题 - __block 修饰变量 block具有修改能力
        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) {
            // ...... 省略了一些代码

            (*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;
}

使用 malloc在堆区拷贝了一份,然后赋值过去。有一点需要注意的是:

copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy

不管是原来的__block结构体变量还是拷贝的__block结构体变量,都指向了copy的对象。这样就对应上了__block对象可以修改外界变量的部分。因为他们都指向了同一块堆区的地址。 在堆__block结构体进行拷贝的方法中调用了下面的这句代码,从而完成了对__block结构体中变量进行了拷贝。

(*src2->byref_keep)(copy, src);

src中的byref_keep函数定义如下

void(*BlockByrefKeepFunction)(struct Block_byref*, struct Block_byref*);

src2的类型是Block_byref结构体。他的定义类型与Block结构体的定义类似,下面我们找打它的byref_keep函数位于Block_byref_2中,也就是第5个参数。

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;
    BlockByrefDestroyFunction byref_destroy;
};

我们到clang生成的cpp中找到对应的Block_byref_lg_name_0的第5个参数,即__Block_byref_id_object_copy_131

__Block_byref_lg_name_0 lg_name = {
    (void*)0,
    (__Block_byref_lg_name_0 *)&lg_name,
    33554432,
    sizeof(__Block_byref_lg_name_0),
    __Block_byref_id_object_copy_131, == keep
    __Block_byref_id_object_dispose_131,   
};

__Block_byref_id_object_copy_131的定义如下,它又调用了我们上面分析的_Block_object_assign,传递的参数通过地址偏移找到了__block结构体中保存的外界___block变量。然后进行贝操作。最终完成了__block变量的修改

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

总结:Block的三层拷贝:

  • 1、Block结构体的拷贝;
  • 2、__block结构体的拷贝;
  • 3、__block结构体中的__block变量的拷贝;

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示 | 支付宝红包
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们