我爱编程

Blocks的实现

2018-06-11  本文已影响0人  wilsonhan

Blocks是“带有自动变量值的匿名函数”。本文通过Blocks的实现来理解Blocks。

本文目录

  1. Blocks的实质
  2. 截获自动变量
  3. 修改Block外部变量的两种方式
  4. Block存储域
  5. 截获对象
  6. __block变量和对象
  7. Block循环引用
  8. copy/release

使用工具:clang(LLVM编译器)将OC代码转换成可读的源代码。

clang -rewrite-objc 源代码文件名

Block的实质

Block的实质就是OC对象,Block函数代码实际上被作为简单的C语言函数来处理。

首先写一段最简单的Blocks代码

int main(int argc, const char * argv[]) {
    //声明并定义一个block对象
    void (^blk)(void) = ^{
        printf("Hello,world!\n");
    };
    //调用block对象
    blk();
    return 0;
}

转换成C代码之后是这样,Block实际上是由结构体声明的。

struct __block_impl {
  void *isa;//这里与OC中类对象一样,指针指向的是类对象
  int Flags;//标志
  int Reserved;//版本升级所需要的区域
  void *FuncPtr;//函数指针
};

static struct __main_block_desc_0 {
  size_t reserved;//保留区域
  size_t Block_size;//Block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

//结构体__block_impl和__main_block_desc_0组成了最简单的block结构体__main_block_impl_0
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;//定义block的类型,总共有三种,全局的静态block,栈中的block,堆中的block。
    impl.Flags = flags;//初始化flag
    impl.FuncPtr = fp;//传递函数地址
    Desc = desc;//初始化__main_block_desc_0结构体
  }
};

//block中的函数声明
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Hello,world!\n");
    }

//main函数
int main(int argc, const char * argv[]) {
    //定义一个void *的blk指针,等号右边是使用__main_block_impl_0的构造函数进行初始化,传入的第一个参数是函数指针,第二个参数是描述信息结构体
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    //调用函数指针,执行函数
    //(void (*)(__block_impl *))这部分是将FuncPtr转换成该类型的函数指针,返回值为void *,传参为void,编译器会在传参前添加一个传递结构体自身的指针
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

通过上面的源代码,可以很清晰的了解到Block是如何实现的,而在Block的结构体__block_impl中,发现了isa指针,与基于objc_object结构体的OC类对象结构体一样,所以,Block其实就是一个OC对象。

截获自动变量

再来看稍微复杂一点的代码

int main(int argc, const char * argv[]) {
    
    int dmy = 256;//没有被block使用到的变量
    int val = 10;//被block捕获的变量
    const char *fmt = "val = %d\n";//被block捕获的变量
    void (^blk)(void) = ^{
        printf(fmt, val);//使用fmt字符串打印val变量
    };
    //定义完block对象后,首先修改val变量,看修改后block区块里捕获的对象是否也修改
    val = 2;
    //同样修改fmt指针指向的常量字符串
    fmt = "These values were changed. val = %d\n";
    //调用block函数blk()
    blk();

    return 0;
}

转换之后的C代码

struct __main_block_impl_0 {
  struct __block_impl impl;//block基本信息结构体
  struct __main_block_desc_0* Desc;//描述结构体
  //这里可以看到两个函数内的局部变量,被声明到了block的结构体中
  const char *fmt;
  int val;
  //构造函数,其中构造函数也初始化了fmt和val变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block函数定义
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    const char *fmt = __cself->fmt; //拷贝block结构体里的fmt变量
    int val = __cself->val; //拷贝val变量

    printf(fmt, val);
    }

int main(int argc, const char * argv[]) {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    //block对象blk的声明和定义,传入函数指针和描述结构体,以及fmt和val两个变量,这里的传值是拷贝
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

    val = 2;

    printf("val = %d\n", val);
    fmt = "These values were changed. val = %d\n";
    //调用blk函数
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

通过上面的代码,可以发现,Block在函数内捕获的fmt和val两个自动变量均在Block结构体内重新声明了相同类型和名称的变量,并且在构造函数中通过拷贝的方式将值传递进来,也就是说,在定义blk对象的这条语句时,就已经将fmt和val的值传递进了Block结构体实例对象内,此时Block对象中保存的值是此时此刻fmt和val的值的拷贝,之后无论如何修改main函数中fmt和val的内容,都不会影响到block中的保存的fmt和val的副本。

当然,若在函数内创建一个指针变量,例如在上边的代码添加

int *p = &val;//将val的地址赋值给指针p

此时,Block的结构体内也会申请一个int指针p,在构造函数的参数列表,传递的也是指针类型,所以在Block对象里修改p的地址对应的内容时,main函数中的指针p和val的值也会被修改。

在Blocks中,截获自动变量的方法并没有实现对C语言数组的截获,使用指针可以解决该问题。
const char text[] = "Hello";
改成 const char *text = "Hello";

修改Block外部变量的两种方式

Block类型变量

  • 自动变量
  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

如果想修改一个外部变量,有两种方式可以实现。

方法一:静态变量、静态全局变量、全局变量

这三种变量,静态全局变量和全局变量的访问方式没有任何改变,Blocks可以直接使用。静态变量则是通过指针的方式传递。

写一段包括这三种变量的代码

int g_val = 1;//全局变量
static int gs_val = 2;//全局静态变量

int main(int argc, const char * argv[]) {
    //静态变量
    static int s_val = 3;
    //block代码块,截获这三种变量
    void (^blk)(void) = ^{
        g_val *= 1;
        gs_val *= 2;
        s_val *= 3;
    };

转换后的代码如下:


int g_val = 1;
static int gs_val = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *s_val;//对于静态变量s_val,使用s_val的指针对其进行访问

  //在构造函数里初始化s_val
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_s_val, int flags=0) : s_val(_s_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *s_val = __cself->s_val; // bound by copy

        g_val *= 1;//对全局变量和全局静态变量的访问与转换前没有任何区别
        gs_val *= 2;//...
        (*s_val) *= 3;//使用指针的方式访问s_val
    }

int main(int argc, const char * argv[]) {
    
    static int s_val = 3;
    
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &s_val));
    return 0;
}

对于静态变量s_val,使用指针对其访问,保存静态变量s_val的指针,传递给__main_block_impl_0结构体的构造函数并保存。这是超出作用域使用变量的最简单方法。

而对于自动变量来说,如果使用指针的方式进行访问,当变量作用域结束的同时,原来的自动变量被废弃,此时Block中超过变量作用域的变量则不能通过指针访问,访问结果是未定义的。解决这个问题,则可以使用__block说明符。

方法二:__block存储域类说明符(__block storage-class-specifier)

C语言中有一下存储域类说明符:

__block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中。如,auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区域中。

下面来写一段__block说明符的代码。

int main(int argc, const char * argv[]) {
    //为val变量添加__block说明符
    __block int val = 10;
    //现在可以在Block代码块中为val正确赋值
    void (^blk)(void) = ^{
        val = 1;
    };
    return 0;
}

转换后的源代码如下:

//新的结构体,用来保存用__block修饰的对象
struct __Block_byref_val_0 {
  void *__isa;//结构体对象
__Block_byref_val_0 *__forwarding;//指向自身
 int __flags;
 int __size;
 int val;//保存被__block修饰的val变量的值
};
//Block的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; //保存了用__block修饰的变量的结构体
  //构造函数,使用_val->__forwarding来初始化val,这个设计的用意在后边说明
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//Block代码块的内容,该例为对val进行赋值
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
    }

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

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

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

int main(int argc, const char * argv[]) {
    //使用__Block_byref_val_0结构体来创建val对象,并对每一个值进行初始化
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    //定义Block对象
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

上面的代码可以看到,使用__block说明符修饰的自动变量,在源代码中实际上是一个__Block_byref_val_0的结构体实例,在__main_block_impl_0中以指针的形式持有,对该变量的修改实际上是通过结构体中的__forwarding指针指向的结构体实例,对val值进行修改。

Block存储域

在之前的代码中,可以看到在__main_block_impl_0结构体的构造函数中,将结构体中的isa变量赋值为_NSConcreteStackBlock,而与之类似的还有_NSConcreteGlobalBlock和_NSConcreteMallocBlock类。

其中,当全局变量区域定义Block代码块和Block语法的表达式不使用截获的自动变量时,Block即是_NSConcreteGlobalBlock对象。

虽然通过clang转换的源代码通常是_NSConcreteStackBlock对象,但实现上却有不同。

将Block从栈复制到堆上

当Block语法记述的变量作用域结束时,栈上的Block和__block变量都会被废弃,此时通过Blocks提供的复制方法,将Block和__block变量从栈上复制到堆上,那么即使记述Block变量的作用域结束,堆上的Block还可以继续存在。

复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa。

首先看ARC有效时,自动生成的将Block从栈复制到堆上的代码。

typedef int (^blk_t)(int);

blk_t func(int rate){
    return ^(int count){return rate * count;};
}

转换过后的源代码:

blk_t func(int rate){

    blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    
    //objc_retainBlock函数实际上就是_Block_copy函数,该函数将栈上的Block复制到堆上,复制后,将堆上的地址作为指针赋值给变量tmp。
    //这里tmp是typedef int (^blk_t)(int),它的源代码实际上是typedef int (*blk_t)(int)函数指针,所以可以存储指针。
    tmp = objc_retainBlock(tmp);
    
    //将堆上的Block作为Objective-C对象,注册到autoreleasepool中,然后返回该对象。
    return objc_autoreleaseReturnValue(tmp);
}

上面是编译器自动生成的代码,而编译器在以下情况无法自动生成复制到堆上的代码

  • 向方法或函数的参数传递Block时

以下方法或函数不用手动赋值

  • Cocoa框架的方法且方法名中含有usingBlock等时,如NSArray类的enumerateObjectsUsingBlock实例方法以及dispatch_async函数时,但在NSArray的initWithObjects方法中不能自动生成。
  • Grand Central Dispath的API

程序员可以手动调用Block的copy方法。

typedef int (^blk_t)(int);

blk_t blk = ^(int count){return rate * count;};

blk = [blk copy];

对于在不同区域的Block调用copy方法所进行的动作如下表:

Block的类 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。

__block变量存储域与__forwarding指针

前面提到,__block对象会被定义为__Block_byref_val_0的结构体实例,并成为__main_block_impl_0结构体实例的一个成员变量,所以当Block被从栈复制到堆上时,__block变量也会受到影响。总结如下表:

__block变量的配置存储域 Block从栈复制到堆时的影响
从栈复制到堆并被Block持有
被Block持有

在一个Block中使用__block变量,当该Block从栈复制到堆时,这些__block变量也全部从栈复制到堆。此时,Block持有__block变量。

__forwarding指针

在__block的结构体中,有一个__forwarding成员变量,在转换成C的源代码中可以看到,所有对__block变量的操作,均是通过__forwarding变量来操作,使用该变量,不管__block变量配置在栈上还是堆上,都能够正确地访问该变量。

最初建__block变量时:

当Block从栈复制到堆上时, __block变量也从栈复制到堆上,此时:

整个过程如下图所示:

复制__block变量.png

通过该功能,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。

截获对象

在Block语法中调用语法外的对象,如下代码:

    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = [^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        } copy];
    
    }
    
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);

执行结果为

array count = 1
array count = 2
array count = 3

在执行最后三行代码的时候,array已经跑出了变量作用域,此时array对象被废弃,但结果运行正常。

转换后的源代码如下,这里仅放了部分源代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;//强持有
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 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用结构体中,声明了一个id array成员对象,默认是__strong修饰,该成员对象以强持有的方式持有Block语法外的array变量,这就保证了在array变量被废弃时,Block的成员对象array仍然持有着NSMutableArray变量,所以代码可以正常运行。

copy和dispose

上面的源代码中新增了两个函数,__main_block_copy_0和__main_block_dispose_0,runtime通过这两个函数在运行过程中将Block从栈复制到堆上以及将堆上的Block废弃。

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

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

这里边的_Block_object_assign函数相当于retain函数,将对象赋值在对象类型的结构体成员变量中。
_Block_object_dispose相当于release函数,释放赋值在对象类型的结构体成员变量中的对象。

copy和dispose函数的调用时机
函数 调用时机
copy函数 栈上的Block复制到堆时
dispose函数 堆上的Block被废弃时
Block从栈复制到堆的时机
  1. 调用Block的copy实例方法时
  2. Block作为函数返回值返回时
  3. 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  4. 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时

在ARC有效时,上述的2与3情况,编译器会自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法效果相同。在4的情况下,在调用的方法内部会对传递来的Block调用Block的copy实例方法或者_Block_copy函数。

这里的第3条,在ARC有效的情况下,如执行这样的语句

void (^blk)(void) = ^{
    ...
};
NSLog(@"%@", blk);

上面定义的blk对象,实际上就是附有__strong修饰符的id类型,所以NSLog语句执行的结果就是blk是一个NSConcreteMallocBLock。

BLOCK_FIELD_IS_OBJECT

在上面两个函数中,可以注意到一个参数BLOCK_FIELD_IS_OBJECT,在前面使用__block的例子中生成的源代码里,也出现了类似的参数BLOCK_FIELD_IS_BYREF。

编译器通过该参数区分copy函数和dispose函数的对象类型是对象还是__block变量。

这里通过runtime的源代码中可以查看到枚举的全部定义

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_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    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.
};

在ARC有效的情况下,编译器也会有不自动调用copy的情况,除了以下几种情况外,推荐手动调用copy实例方法。

__block变量和对象

__block说明符可指定任何类型的自动变量。如下例子所示:

__block id __strong obj = [[NSObject alloc] init];

转换后的源代码:

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*);
 id obj;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    //dst的地址加40,包括4个指针(isa,__forwarding,两个函数指针)32个字节,两个int为8个字节,将指针移动到obj的起始地址
    //131是上面枚举中BLOCK_FIELD_IS_OBJECT和BLOCK_BYREF_CALLER取或的结果
    _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);
}

int main(int argc, const char * argv[]) {
    __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)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
    return 0;
}

在__block变量为附有__strong修饰符的id类型或对象类型自动变量的情形下会发生与在Block中使用附有__strong修饰符的id类型或对象类型自动变量的情况下相同的过程。当__block变量从栈复制到堆时,使用_Block_object_assign函数,废弃时使用_Block_object_dispose函数。

使用__weak说明符的__block变量

当使用__weak说明符修饰的变量在作用域结束后,即是Block代码块中使用了该变量,也会被释放、废弃,变量会变成nil。

__unsafe_retained说明符

__unsafe_retained说明符表明不归编译器管理对象,被它修饰的变量不会像__strong或__weak修饰符那样处理,注意不要通过悬垂指针访问已被废弃的对象。

不能同时使用__autoreleasing和__block

Block循环引用

这个很容易理解,在类中的Block语法中直接调用self或者调用类的成员对象,会导致Block强持有类,但同时类也强持有Block对象,导致循环引用。

解决方法1
在Block语法外声明一个self的弱引用,该方法只在ARC有效的情况下游泳

__weak typeof(self) weakSelf = self;

上述方法让Block以weak的方式持有self,这样就不会引起循环引用,导致内存泄漏。
而这样会引发另一个问题,Block中的代码不是立即执行,在执行的时候可能该weak指针已经被销毁了,所以self会变成nil,这样需要在Block语法内部再添加一局

__strong typeof(weakSelf) strongSelf = weakSelf;

在Block语法内部使用strongSelf来获取self的相关属性和方法,当外部self被release后,strongSelf在block的语法局部还持有该self,当Block语法执行完毕后,strongSelf的生命周期结束被release,此时self的引用计数为0,对象被销毁。

解决方法2
使用__block,在成员方法内

__block id tmp = self;
blk_ = ^ {
    NSLog(@"self = %@", tmp);
    tmp = nil;
};

但该方法有局限性,必须保证Block对象的代码执行一次。

使用__block变量的优点如下:

copy/release

在ARC无效时,需要手动将Block从栈复制到堆上,同样需要手动释放Block。

推荐使用copy实例方法代替retain实例方法。
使用release方法释放Block。

同样可以使用C语言的Block_copy函数和Block_release函数,它与copy和release方法效果相同。

ARC无效时的__block

在ARC无效时,__block说明符被用来避免Block中的循环引用。当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain(不会被retain则意味着不会导致循环引用),而若没有被__block说明符修饰,则会被retain。

上一篇下一篇

猜你喜欢

热点阅读