Block探究

2020-11-05  本文已影响0人  Maji1
  1. block的实质是什么?
  2. 一共有几种block?
  3. 都是什么情况下生成的?

block的实质是什么?

如何查看block源码:

先写一段简单的block代码:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int num = 10;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"a = %d,b = %d",a,b);
            NSLog(@"num = %d",num);
        };
        block(1,2);
    }
    return 0;
} 

转化为c++源码:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int num = 10;
        void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
    }
    return 0;
}

对比两段代码,发现定义block的源码:
void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));

__main_block_impl_0结构体:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

定义时将__main_block_impl_0结构体的地址赋值给了block


参数1:(void *)__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int num = __cself->num; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_1c8b6c_mi_0,a,b);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_1c8b6c_mi_1,num);
}
参数2:&__main_block_desc_0_DATA
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)};
参数3:_num

我们定义的局部变量。因为在block块内用到了变量num,所以block在声明的时候会将num作为参数传入,捕获参数num
如果在block块内没有使用到num,就不会作为参数传入。

注意⚠️:这里就是为什么在定义block之后修改局部变量的值,再调用block,修改的值无法生效的原因。
定义block时已经将局部变量的值传入__main_block_impl_0结构体中,调用block时直接从__main_block_impl_0结构体中将值取出来。



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

该结构体内含有isa指针,因此可以证明block本质上就是一个OC对象。


调用block(1,2);的源码:
 ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

注意⚠️:block__main_block_impl_0类型的结构体,怎么可以直接强转为__block_impl类型?

因为__block_impl__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。


block捕获变量

我们修改下代码:

int globalNum = 30;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int num = 10;
        static int staticNum = 20;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"a = %d,b = %d",a,b);
            NSLog(@"num = %d, count = %d, globalCount = %d",num, staticNum, globalNum);
        };
        num = 5;
        staticNum = 15;
        block(1,2);
    }
    return 0;
}

再看下源码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int num;
  int *staticNum;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int *_staticNum, int flags=0) : num(_num), staticNum(_staticNum) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int num = __cself->num; // bound by copy
  int *staticNum = __cself->staticNum; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_c17f7e_mi_0,a,b);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_c17f7e_mi_1,num, (*staticNum), globalNum);
        }

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 argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int num = 10;
        static int staticNum = 20;
        void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num, &staticNum));
        num = 5;
        staticNum = 15;
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
    }
    return 0;
}

可以看出,num是值传递,staticNum是指针传递& staticNumglobalNum没有传递,而是直接访问。

1、局部变量-自动变量(auto变量)

局部变量前面自动添加auto关键字,auto只存在于局部变量中,离开作用域就销毁。

上述代码中已经验证,自动变量会捕获到block内部,block内部会专门新增加一个参数来存储变量的值。访问方式为 值传递

2、局部变量-静态变量(static变量)

static修饰的变量同样会被block捕获,访问方式为 指针传递

局部变量可能会销毁,调用block时如果该变量被销毁了,就不能访问该变量的地址,所以只能传递值。静态变量不会被销毁,所以可以传地址,传地址不回增加内存的消耗。
所以,在block调用之前修改地址中保存的值,block中的地址是不会变的。所以值会随之改变。

3、全局变量
不会被block捕获,不用传递,直接访问。


block内使用self

OC代码:

@implementation CQTest

- (void)testDemo1 {
    void(^block)(void) = ^{
        NSLog(@"%@",self);
    };
    block();
}
+ (void)testDemo2 {
    
}
@end

C++代码:

struct __CQTest__testDemo1_block_impl_0 {
  struct __block_impl impl;
  struct __CQTest__testDemo1_block_desc_0* Desc;
  CQTest *self;
  __CQTest__testDemo1_block_impl_0(void *fp, struct __CQTest__testDemo1_block_desc_0 *desc, CQTest *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
......
static void _I_CQTest_testDemo1(CQTest * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__CQTest__testDemo1_block_impl_0((void *)__CQTest__testDemo1_block_func_0, &__CQTest__testDemo1_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

static void _C_CQTest_testDemo2(Class self, SEL _cmd) {

}

对象方法类方法 都会默认将self作为参数传递给方法内部,所以 self局部变量。前面已经验证 局部变量 才会被 block 捕获。


block内使用 成员变量实例属性 的区别

OC代码:

- (void)testDemo1 {
    void(^block)(void) = ^{
        NSLog(@"self.num = %@",self.num);
        NSLog(@"_num = %@",self->_num);
    };
    block();
}

C++代码:

struct __CQTest__testDemo1_block_impl_0 {
  struct __block_impl impl;
  struct __CQTest__testDemo1_block_desc_0* Desc;
  CQTest *self;
  __CQTest__testDemo1_block_impl_0(void *fp, struct __CQTest__testDemo1_block_desc_0 *desc, CQTest *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __CQTest__testDemo1_block_func_0(struct __CQTest__testDemo1_block_impl_0 *__cself) {
  CQTest *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_CQTest_4b6f9e_mi_0,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("num")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_CQTest_4b6f9e_mi_1,(*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_CQTest$_num)));
    }

一共有几种block?每种类型都是什么情况下生成的?

打印看下block的类型:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        void(^block)(int ,int) = ^(int a, int b){
        };
        NSLog(@"\n %@ \n %@ \n %@ \n %@ \n",
              [block class],
              [[block class] superclass],
              [[[block class] superclass] superclass],
              [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}

输出日志:

 __NSGlobalBlock__ 
 __NSGlobalBlock 
 NSBlock 
 NSObject

这里打印的是__NSGlobalBlock类型,继承之NSBlock,但是最终还是继承之NSObject,再次证明blockOC对象。

前面的代码中我们看到impl.isa 指向的都是 _NSConcreteStackBlock 类对象地址。其实block的类型共三种:

都是什么情况下生成的?

看段代码:

void (^block1)(void) = ^{
  NSLog(@"block1");
};
int a = 10;
void (^block2)(void) = ^{
   NSLog(@"block2-%d",a);
};
NSLog(@"\n block1:%@ \n block2:%@ \n block3:%@ \n",
  [block1 class],
  [block2 class],
  [^{
    NSLog(@"block3-%d",a);
} class]);

看下书输出日志:

 block1:__NSGlobalBlock__ 
 block2:__NSMallocBlock__ 
 block3:__NSStackBlock__

block在内存中六大区域的位置
类型 描述 对应block类型
存储局部变量,当其作用域执行完毕之后,就会被系统立即收回 NSStackBlock
存储OC对象,手动申请的字节空间,需要手动释放 NSMallocBlock
BSS段 未初始化的全局变量和静态变量,一旦初始化就会从BSS段中回收掉,转存到数据段中
数据段 存储已经初始化的全局变量和静态变量,以及常量数据,直到结束程序时才会被立即收回 NSGlobalBlock
常量区 存放常量字符串,程序结束后由系统释放
代码段 存放函数的二进制代码,内存区域较小,直到结束程序时才会被立即收回

__NSMallocBlock__调用了copy之后不会改变类型。
__NSStackBlock__调用了copy之后就会变成__NSMallocBlock__类型。
__NSMallocBlock__调用了copy之后引用计数会增加。

所以,在 MRC 环境下开发时,经常需要使用copyblock拷贝到堆中。即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。
而在ARC环境下系统会自动copyblock不会被销毁。

我们在ARC环境下定义全局的block属性时经常使用copy关键字,这是沿用了 MRC 环境下的书写风格,其实在ARC环境下使用copystrong关键字是一样的。


ARC中在什么情况下系统会自动将block进行一次copy操作?
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {  }]
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
        dispatch_block_t block);

block如何捕获 OC 对象?

前面我们捕获的除了self都是基本数据类型,下面研究下捕获OC对象的方式。

ARC环境下代码:

typedef void(^Block)(void);
int main(int argc, char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            CQTest *test = [[CQTest alloc] init];
            test.num = @"123";
            block = ^{
                NSLog(@"%@", test.num);
            };
            NSLog(@"%@", [block class]);//输出__NSMallocBlock__
        }//test不会被释放
    }//test被释放
    return 0;
}

C++代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  CQTest *test;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, CQTest *_test, int flags=0) : test(_test) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

ARC下,test在使用后并不会被立即释放。因为block代码块内了强引用test,系统会对block自动copyblock存到堆区。

上述代码如果在MRCtest就会被提前释放。因为这时的block存在栈区,不会强引用test

__weak弱引用test:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            CQTest *test = [[CQTest alloc] init];
            test.num = @"123";
            __weak CQTest *weakTest = test;
            block = ^{
                NSLog(@"%@", weakTest.num);
            };
            NSLog(@"%@", [block class]);
        }//test会被释放
    }
    return 0;
}

__weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错,添加说明-fobjc-arc -fobjc-runtime=ios-8.0.0

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

C++代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  CQTest *__weak weakTest;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, CQTest *__weak _weakTest, int flags=0) : weakTest(_weakTest) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  CQTest *__weak weakTest = __cself->weakTest; // bound by copy

                NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_415682_mi_1, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)weakTest, sel_registerName("num")));
}

再继续往下看C++代码:

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

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

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

_Block_object_assign调用时机及作用:

block进行copy操作的时候会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部再调用_Block_object_assign函数。

_Block_object_assign函数会自动根据__main_block_impl_0结构体内部对象的指针类型,对对象产生 强引用 还是 弱引用
可以理解为_Block_object_assign函数内部会对对象test进行引用计数器的操作,如果__main_block_impl_0结构体内test指针是__strong类型,则为强引用,引用计数+1,如果指针是__weak类型,则为弱引用,引用计数不变。

_Block_object_dispose调用时机及作用:

block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。

_Block_object_dispose会对对象做释放操作,类似于release,也就是断开对对象的引用,而对象是否被释放还是取决于对象自己的引用计数。


总结:

1、block捕获的变量为对象时,__main_block_desc_0结构体中会出现像个参数copydisposeblock希望对捕获的对象进行内存管理。
2、block捕获的对象为auto时,如果block存储在栈区(此种情况为MRC下),不会对捕获的对象强引用。
3、一旦 block 被拷贝到堆上,copy函数会调用_Block_object_assign函数,根据捕获对象的指针类型(__strong,__weak,unsafe_unretained)进行 强引用 或者 弱引用
4、一旦 block 从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto变量。


__block的作用

__block用于解决block内部不能修改auto变量值的问题,__block不能修饰 静态变量(static) 和 全局变量。

OC代码:

 __block int num = 5;
        Block  block = ^{
            num = 6;
            NSLog(@"%d", num);
};

C++代码:


int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 5};
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_dfce68_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));

    }
    return 0;
}

struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_num_0 *num = __cself->num; // bound by ref

            (num->__forwarding->num) = 6;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hx_3p3kcn8d1dqf0225mh9pb2vr0000gn_T_main_25b6a2_mi_0, (num->__forwarding->num));
}

__block将变量包装成一个结构体对象,然后再把变量存储在结构体里面。block内部存储对象指针,所以可以通过指针找到内存地址修改变量的值。

__block修饰对象:
__block  CQTest *test = [[CQTest alloc] init];
Block  block = ^{
    NSLog(@"%@", test.num);
};
block();

C++代码:

struct __Block_byref_test_0 {// 48 共占用内存空间
  void *__isa; // 8 内存空间
__Block_byref_test_0 *__forwarding; // 8 内存空间
 int __flags; // 4 内存空间
 int __size; // 4 内存空间
 void (*__Block_byref_id_object_copy)(void*, void*); // 8 内存空间
 void (*__Block_byref_id_object_dispose)(void*); // 8 内存空间
 CQTest *test; // 8 内存空间
};

__attribute__((__blocks__(byref))) __Block_byref_test_0 test = {(void*)0,(__Block_byref_test_0 *)&test, 33554432, sizeof(__Block_byref_test_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((CQTest *(*)(id, SEL))(void *)objc_msgSend)((id)((CQTest *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CQTest"), sel_registerName("alloc")), sel_registerName("init"))};
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_test_0 *)&test, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

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__weak同时修饰对象:

OC代码:

CQTest *test = [[CQTest alloc] init];
__block __weak CQTest *weakTest = test;
Block  block = ^{
    NSLog(@"%@", weakTest.num);
};
block();

C++代码:

struct __Block_byref_weakTest_0 {
  void *__isa;
__Block_byref_weakTest_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 CQTest *__weak weakTest;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_weakTest_0 *weakTest; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakTest_0 *_weakTest, int flags=0) : weakTest(_weakTest->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

但是在mrc环境下,尽管调用copy操作,__block结构体不会对test产生强引用,依然是弱引用。

__block修饰的变量内存管理:

1、当block内存在栈上时,并不会对__block变量产生内存管理。
blcokcopy到堆上时会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)。

2、当blockcopy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。
而此时栈中的__Block_byref_test_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_test_0结构体,堆中__Block_byref_test_0结构体内的__forwarding指针依然指向自己。

3、当block从堆中移除的时,就会调用__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

4、一旦使用__block修饰的变量,__main_block_impl_0结构体内一律使用强指针引用生成的结构体。
对结构体内部变量的引用取决于我们在外部定义变量时的指针类型。


block循环引用问题

ARC环境下:
CQTest *test = [[CQTest alloc] init];
test.block = ^{
    NSLog(@"%@", test.num);
};

解决方式:
1、使用__weak__unsafe_unretained修饰符可以解决循环引用的问题。

2、__block修饰符也可以解决循环引用的问题。

 __block CQTest *test = [[CQTest alloc] init];
test.block = ^{
    NSLog(@"%@", test.num);
    test = nil;
};
test.block();

__block解决循环引用的条件:1、必须执行block()。 2、block代码块内必须将test设置为nil

MRC环境下:

1、可通过__unsafe_unretained来解决问题,但是使用的问题跟ARC下相同。__weakMRC下不能用。
2、使用__block来解决。在MRCblock即使手动调用了copy,自动生成的结构体对test依然是弱引用。所以可以解决循环引用的问题。

__strong__weak
 __weak typeof(self) weakSelf = self;
test.block = ^{
  __strong typeof(weakSelf) strongSelf = weakSelf;
  NSLog(@"%@", strongSelf.num);
};

上一篇下一篇

猜你喜欢

热点阅读