ios底层原理-代码块(block)的本质(一)

问题

1.什么是block,block的本质是什么?
2.block的属性修饰词为什么是copy?使用block有哪些使用注意?
3.block为什么会发生循环引用?
4.block的变量捕获究竟是怎样进行的?
5.block在修改NSMutableArray,需不需要添加__block?
6.__block和__weak的作用是什么?有什么使用注意点?
7.block中访问的对象和变量什么时候会销毁?

让我们带着这一系列问题,开始探寻block的本质

1.什么是block,block的本质是什么

  • block本质上也是一个对象,而这个对象是一个结构体,其内部含有一个isa指针指向自己的类。
  • block是封装了函数调用以及函数调用环境的OC对象。
  • block是封装函数及其上下文的OC对象。

首先写一个简单的block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"this is block,a = %d,b = %d",a,b);
            NSLog(@"this is block,age = %d",age);
        };
        block(3,5);
    }
    return 0;
}

使用命令行将代码转化为c++查看其内部结构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

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;//描述block的信息
  int age;
// 构造函数(类似于OC的init方法),返回结构体对象
  __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; //block内代码地址
    Desc = desc;  // 储存block对象占用的大小
  }
};
// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_0,a,b);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_1,age);
        }

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 age = 10;
        // 定义block变量以及实现
        void(*block)(int , int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
         // 执行block内部的代码
        ((void (*)(__bloc k_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);


        return 0;
    }

}
Block源码

从定义和实现block变量开始

        // 定义block变量
        void(*block)(int , int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

抛开无关紧要的代码,block除了传入两个int的参数还调用了__main_block_impl_0这个函数,并将函数的地址赋值给了block,于是找到方法的定义

__main_block_imp_0结构体

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; //block内代码地址
    Desc = desc;  // 储存block描述
  }
};

观察结构体内,有2个结构体变量和1个基本变量,同时还有一个构造函数,而我们上面block实现传入的函数不正是这个函数吗?
而这个函数最终返回的不就是一个名为__main_block_impl_0的结构体呀

那么也就是说最终将一个__main_block_imp_0结构体的地址赋值给了block变量

所以我们需要 看一看这个__main_block_imp_0结构体里到底有什么猫腻?仔细一看,哇、美滋滋,只有4个参数 void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0
再看看block实现的时候调用__main_block_imp_0函数传入了什么(void *)__main_block_func_0, &__main_block_desc_0_DATA, age。居然只有三个参数,很好nice,说明flags默认传的0基本与我们没有和什么关系,继续看第一个参数__main_block_func_0这就是我们的fp呀看看__main_block_func_0中有些什么。

  • flags,标志变量,在实现block的内部操作时会用到
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_0,a,b);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_1,age);
        }

__main_block_func_0函数中首先取出block中age的值,紧接着可以看到两个熟悉的NSLog,可以发现这两段代码恰恰是我们在block块中写下的代码。
那么__main_block_func_0函数中其实存储着我们block中写下的代码。而__main_block_impl_0函数中传入的是(void *)__main_block_func_0,也就说将我们写在block块中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中保存在结构体内。
也就是说我们的fp就是__main_block_func_0函数的地址。

而这个时候我们再回到__main_block_imp_0结构体的构造函数。

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; //block内代码地址
    Desc = desc;  // 储存block描述
  }
};

这个魂淡居然把我们的block实现地址传给了implFunctionPtr,找到impl的定义,居然又是一个结构体__block_impl,好的,忍它一次,点进去。

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

简单,又是2个清爽的成员变量和2个清爽的指针变量,到了这里我只知道flags是系统传的我不管,然后我block的实现地址传给了FuncPtr,还有两个参数是什么东东呢?
此时此刻我们看着isa这个指针不就是指向所属类的指针,也就是block的类型吗
但是__main_block_impl_0构造函数传的是&_NSConcreteStackBlock这个东东,这个东东又是什么👻?哇好蒙蔽,这都不知道是哪里传过来的,不要怕点进去看

__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];

德玛西亚,这个block其实就是当前类所属的指针的地址,只是苹果对它做了堆栈的处理。

Reserved这个只是保留变量而已

总结一下:

  • isa,指向所属类的指针,也就是block的类型
  • flags,标志变量,在实现block的内部操作时会用到
  • Reserved,保留变量
  • FuncPtr,block执行时调用的函数指针
    可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象(runtime里面,对象和类都是用结构体表示)。

此时此刻 __main_block_func_0函数的地址我们已经看吃透了,看__main_block_imp_0的第二个参数&__main_block_desc_0_DATA点进去看是一个__main_block_desc_0结构体

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

它也传入了2个参数,reserved系统传了0,而Block_size传入的不正是当前block的size大小吗,又是苹果最喜欢的内存处理,这也是ios的魅力所在呀。

到了这里block的定义和实现已经走完了,可是还没有调用呀?

block的调用

// 执行block内部的代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);

通过上述代码可以发现调用block是通过block找到__block_implFunPtr直接调用
通过上面分析我们知道block指向的是__main_block_impl_0类型结构体,但是我们发现__main_block_impl_0结构体中并不直接就可以找到FunPtr,而FunPtr是存储在__block_impl中的,为什么block可以直接调用__block_impl中的FunPtr呢?

重新查看上述源代码可以发现,(__block_impl *)block将block强制转化为__block_impl类型的,因为__block_impl__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。并找到FunPtr成员。

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_0,a,b);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_1,age);
        }

上面我们知道,FunPtr中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。并且回头查看__main_block_func_0函数,可以发现第一个参数就是__main_block_impl_0类型的指针。也就是说将block传入__main_block_func_0函数中,便于重中取出block捕获的值。

总结:

  • block结构体内部之间的关系


    block结构体内部之间的关系
  • block底层的数据结构


    block底层的数据结构

此时此刻你以为结束了吗?
nonono
look

block的类型

block分为三种类型

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

纳尼,这不是我们刚刚__main_block_impl_0中的implisa指向的&_NSConcreteStackBlock的类型吗?

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; //block内代码地址
    Desc = desc;  // 储存block描述
  }
};

不管那么多写段代码打印一下
我们通过代码用class方法或者isa指针查看具体类型。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int a = 10;
        static int b = 11;
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };

        NSLog(@"%@", [block class]);
        NSLog(@"%@", [[block class] superclass]);
        NSLog(@"%@", [[[block class] superclass] superclass]);
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}
打印内容

从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象

通过代码查看一下block在什么情况下其类型会各不相同

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 内部没有调用外部变量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 内部调用外部变量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
        // 3. 直接调用的block的class
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}
打印内容

上述代码转化为c++代码查看源码时却发现block的类型与打印出来的类型不一样,c++源码中三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。
我们可以猜测runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

这里就涉及到了block在内存中的存储位置


block内存分配

这五个区存储的内容也各有划分:

  • 栈区(stack):这一块区域系统会自己进行管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量、深递归、函数循环调用都可能耗尽内存而造成运行崩溃。
  • 堆区(heap):与栈区相对,这一块一般由我们开发人员管理,比如一些alloc、free的操作,存储一些自己创建的对象。
  • 全局区(静态区 static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的变量会分开存储在相邻的区域,程序结束后系统来释放。
  • 常量区:存储常量字符串和const常量。
  • 代码区:顾名思义,就是存我们写的代码。
  • _NSConcreteGlobalBlock(全局)
  • _NSConcreteStackBlock(栈)
    -_NSConcreteMallocBlock(堆)
block是如何定义其类型

block是如何定义其类型,依据什么来为block定义不同的类型并分配在不同的空间呢?

接着我们使用代码验证上述问题,首先关闭ARC回到MRC环境下,因为ARC会帮助我们做很多事情,可能会影响我们的观察。

点击【Project】 - 点击【Target 】-点击 【build Setting】- 输入【“AutoMatic”】 -点击【Object-C Automatic Reference Count】改为【NO】

// MRC环境!!!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Global:没有访问auto变量:__NSGlobalBlock__
        void (^block1)(void) = ^{
            NSLog(@"block1---------");
        };   
        // Stack:访问了auto变量: __NSStackBlock__
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@ %@", [block1 class], [block2 class]);
        // __NSStackBlock__调用copy : __NSMallocBlock__
        NSLog(@"%@", [[block2 copy] class]);
    }
    return 0;
}
打印内容

通过打印的内容我们可以知道:

  • 没有访问auto变量的block是__NSGlobalBlock__类型的,存放在数据段中。
  • 访问了auto变量的block是__NSStackBlock__类型的,存放在栈中。
  • __NSStackBlock__类型的block调用copy成为__NSMallocBlock__类型并被复制存放在堆中。

再看看ARC中的打印

打印内容

我了个去,发现访问了auto变量的block是__NSMallocBlock__类型了,可以猜想苹果在ARC环境下是不是帮我们做了一次copy操作呢?

__NSStackBlock__访问了aotu变量,并且是存放在栈中的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁,那么我们很有可能block内存销毁之后才去调用他,那样就会获取错误。所以ARC下直接帮我们自动进行了一次copy操作。
[__NSStackBlock __ copy]操作就变成了__NSMallocBlock __
此时此刻,我们已经知道ARC进行了copy操作那它究竟针对不同类型的block做了什么操作呢?请看下图:

不同block类型调用copy效果

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

当然,你以为结束了吗?仔细看:

ARC帮我们做了什么

在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。

什么情况下ARC会自动将block进行一次copy操作?
以下代码都在RAC环境下执行。

1. 将block赋值给__strong指针时
// ARC环境!!!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block内没有访问auto变量
        Block block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@",[block class]);
        int a = 10;
        // block内访问了auto变量,但没有赋值给__strong指针
        NSLog(@"%@",[^{
            NSLog(@"block1---------%d", a);
        } class]);
        // block赋值给__strong指针
        Block block2 = ^{
          NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@",[block2 class]);
    }
    return 0;
}

打印内容

我们发现打印的第二个block内访问了auto变量,但没有赋值给__strong指针直接打印了__NSStackBlock__

所以当block被赋值给__strong指针时,RAC才会自动进行一次copy操作。

此时此刻,我们定义block属性的时候是不是就可以用copy和strong呢?

block本身是像对象一样可以retain,和release。但是,block在创建的时候,它的内存是分配在栈上的,而不是在堆上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。因为栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.
使用retain也可以,但是block的retain行为默认是用copy的行为实现的,
因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block拷贝(copy)到堆,所以说为了block属性声明和实际的操作一致,最好声明为copy。

2. 将block作为函数返回值时
//  ARC环境!!!
typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    // block中访问了auto变量,此时block类型应为__NSStackBlock__
    Block block = ^{
        NSLog(@"---------%d", a);
    };
    return block;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block = myblock();
        block();
       // 打印block类型为 __NSMallocBlock__
        NSLog(@"%@",[block class]);
    }
    return 0;
}

打印内容

如果在block中访问了auto变量时,block的类型为__NSStackBlock__,上面打印内容发现blcok为__NSMallocBlock__类型的,并且可以正常打印出a的值,说明block内存并没有被销毁。

block进行copy操作会转化为__NSMallocBlock__类型,来讲block复制到堆中,那么说明RAC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作。

3.block作为Cocoa API中方法名含有usingBlock的方法参数时
    NSArray * arr = @[@1,@2,@3,@4];
    [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        //一系列装逼操作
    }];
4.block作为GCD API的方法参数时
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});        
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
       //一系列装逼操作
});

注意

  • 1,对于栈区block,在ARC情况下自动拷贝到堆区,MRC则放在栈区,所在函数执行完毕就会释放。那么要想在外面调用就要用copy指向它,这样
    就copy到了堆区。
  • 2,strong属性不会拷贝,会造成野指针错区从而crash。另外这里需要注意的是,ARC是编译器的一种特性,编译器在编译的时候会在合适的地方插入retain、release、autorelease,而不是iOS运行时的特性。
  • 3,堆区是动态的,不像代码区是不变化的,堆区会不断地创建销毁,当没有强指针指向的时候就会被销毁,如果再去访问这段代码,程序就会崩溃。所以在堆区要用strong或者copy修饰。
  • 4,block是一段代码块,即不可变,所以使用copy也不会深copy。
所以要用strong替代copy修饰block要满足两个条件:

用strong修饰且引用了外部变量

使用block的时候要注意循环引用导致内存泄露

一个block要使用self,会处理成在外部声明一个weak变量指向self,在block里又声明一个strong变量指向weakSelf?????
原因:block会把写在block里的变量copy一份,如果直接在block里使用self,(self对变量默认是强引用)self对block持有,block对self持有,导致循环引用,所以这里需要声明一个弱引用weakSelf,让block引用weakSelf,打破循环引用。
而这样会导致另外一个问题,因为weakSelf是对self的弱引用,如果这个时候控制器pop或者其他的方式引用计数为0,就会释放,如果这个block是异步调用而且调用的时候self已经释放了,这个时候weakSelf已就变成了nil。
当控制器(也可以是其他的控件)pop回来之后(或者一些其他的原因导致释放),网络请求完成,如果这个时候需要控制器做出反映,需要strongSelf再对weakSelf强引用一下。
但是,你可能会疑问,strongSelf对weakSelf强引用,weakSelf对self弱引用,最终不也是对self进行了强引用,会导致循环引用吗。不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容