iOS底层第八、九、十天 -- Block

引导语:

  • Block原理是怎样的?本质是什么?
  • _block的作用是什么?使用需要注意什么?
  • block的属性修饰词为什么是copy?使用block有哪些使用注意?
  • block在修改NSMutableArray时需不需要添加_block?

我们通常代码中使用的block是什么:为什么能够保存一段代码,而控制block内部代码块执行时机呢?


Q:Block本质是什么?
block是封装了函数调用、以及函数调用环境的OC对象。
函数调用:block 的{}代码块。
函数调用环境:block传入的参数、捕获的auto变量。
相关图:

Block本质.png

查看block源码:

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;
  // 构造函数(类似于OC的init方法),返回结构体对象
  __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;
  }
};

// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_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)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        // 定义block变量
        void (*block)(void) = &__main_block_impl_0(
                                                   __main_block_func_0,
                                                   &__main_block_desc_0_DATA
                                                   );

        // 执行block内部的代码
        block->FuncPtr(block);
    }
    return 0;
}
Block代码本质.png
  • FuncPtr:指向调用函数的地址
  • __main_block_desc_0 :block描述信息
  • Block_size:block的大小

C语言中不允许结构体内有方法函数
C++允许结构体内些方法函数(构造函数),构造函数类似OC中的init


第二部分:Block捕获变量

Q:下述代码输出值为多少?

int age=10;
void (^Block)(void) = ^{
    NSLog(@"age:%d",age);
};
age = 20;
Block();

输出值为 age:10
原因:创建block的时候,已经把age的值存储在里面了。

Q:下列代码输出值分别为多少?

auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
    NSLog(@"age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

输出结果为:age:10,num:11
原因:auto变量block访问方式是值传递,static变量block访问方式是指针传递
源码证明:

int age = __cself->age; // bound by copy
int *num = __cself->num; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d2875b_mi_0, age, (*num));

int age = 10;
static int num = 25;

block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, age, &num));

age = 20;
num = 11;

上述代码可查看 static修饰的变量,是根据指针访问的

Q:为什么block对auto和static变量捕获有差异?
auto:自动变量,离开作用域销毁,不采用指针访问;
static:变量一直保存在内存中,可以访问内存,指针访问即可。

Q:block对全局变量的捕获方式是?
block不需要对全局变量捕获,都是直接采用取值的

Q:为什么局部变量需要捕获?
考虑作用域的问题,需要跨函数访问,就需要捕获

Q:block的变量捕获(capture)
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

变量.png

Q:block内访问self是否会捕获(capture)
会,self是当调用block函数的参数,参数是局部变量,self指向调用者

Q:block里访问成员变量是否会捕获?
会,成员变量_xxx的访问其实是self->xxx,先捕获self,再通过self访问里面的成员变量.


第三部分 Block类型

Q:block有哪几种类型?
block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

  • __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
  • __NSStackBlock __ ( _NSConcreteStackBlock )
  • __NSMallocBlock __ ( _NSConcreteMallocBlock )

代码示例:

void (^block1)(void) = ^{
    NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);

输出结果:
NSGlobalBlock
__NSGlobalBlock
NSBlock
NSObject
null

上述代码输出了block1的类型,也证实了block是对象,最终继承NSObject.

如果我们在lldb的过程中观看block的类型时,发现编译阶段block都是stack形式,但是打印出来会显示不同类型。这是因为OC位动态语言,在这个过程中会对其进行处理。所以一切以runtime运行时的结果为准。

Q:各类型的block在内存中如何分配的?

  • __NSGlobalBlock __ 在数据区
  • __NSMallocBlock __ 在堆区
  • __NSStackBlock __ 在栈区

堆:动态分配内存,需要程序员自己申请,程序员自己管理
栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况
编译阶段:此阶段会把代码 编入代码区、data区内

block内存分配.png

Q:如何判断block是哪种类型?

Block类型 环境
_NSGlobalBlock 没有访问auto变量
_NSStackBlock 访问了auto变量
_NSMallocBlock _NSStackBlock 调用了copy

Q:对每种类型block调用copy操作后是什么结果?

Block类型 原配置存储 复制效果
_NSGlobalBlock 数据区 什么也不会做
_NSStackBlock 栈区 从栈复制到堆上
_NSMallocBlock 堆区 引用计数增加

总结:栈上block用完,其内部的数据变成垃圾数据,说不准是啥。所以copy栈数据到堆上使用才可以。

Q:在ARC环境下,编译器什么情况下会自动将栈上的block复制到堆上?

  1. block作为函数返回值时
  2. 将block赋值给__strong指针时
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时
  4. block作为GCD API的方法参数时

ARC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);


第四部分:对象类型的auto变量

Q:ARC下述代码中Person对象是否会释放?
示例代码:

typedef void(^XBTBlock)(void);
XBTBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 10;
    
    block = ^{
        NSLog(@"======= %d",p.age);
    };
}

Person.m
- (void)dealloc{
    NSLog(@"Person - dealloc");
}

输出结果:不会打印Person - dealloc

转化C++代码后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

上述结果:
block为堆block,block里面有一个Person指针,Person指针指向Person对象。只要block还在,Person就还在。block强引用了Person对象。

疑问:此处的isa指针明明指向生成的stackBlock呀,为什么最后还是强引用person对象呢?是不是什么地方有错误?
推论:此情况只是编译时候产生的,我们在runtime的时候对这个类型进行了改变,我们通过打印class 进行block类型查看即可。所以可以引导出以下结论。
结论:ARC情况下会自动将stackBlock拷贝到堆上生成mallocBlock,从而强引用里面person对象

Q:上述代码改换成MRC,Person对象会释放么?
会的!
堆空间的block会对Person对象retain操作,拥有一次Person对象。

Q:下列代码中Person是否会被释放?

@autoreleasepool {
        XBTBlock block;
        
        {
            Person *p = [[Person alloc] init];
            p.age = 10;
            __weak Person *weakPersn = p;
            block = ^{
                NSLog(@"======= %d",weakPersn.age);
            };
        }
        NSLog(@"--------------");
}

答案:会释放
原因: __weak修饰对象后,我们block捕获的就是一个弱引用的对象,所以block不会强引用person对象,在person出了作用域后就可以释放了。

小结

无论MRC还是ARC,栈空间上的block,不会持有对象;堆空间的block,会持有对象。

Q:当block内部访问了对象类型的auto变量时,是否会强引用?
答案:分情况讨论,分为栈block和堆block

栈block: 如果block是在栈上,将不会对auto变量产生强引用(栈上的block随时会被销毁,自身都难保,也没必要去强引用其他对象)

堆block:
1.如果block被拷贝到堆上:
a) 会调用block内部的copy函数
b) copy函数内部会调用_Block_object_assign函数
c) _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

2.如果block从堆上移除
a) 会调用block内部的dispose函数
b) dispose函数内部会调用_Block_object_dispose函数
c) _Block_object_dispose函数会自动释放引用的auto变量(release

正确答案:

  • 如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象
  • 如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

Q1:gcd的block中引用 Person对象什么时候销毁?
代码:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    Person *person = [[Person alloc] init];
    person.age = 10;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%d",person.age);
    });
    
    NSLog(@"touchesBegan");
}

输出结果:
14:36:03.395120+0800 test[1032:330314] touchesBegan
14:36:05.395237+0800 test[1032:330314] age:10
14:36:05.395487+0800 test[1032:330314] Person-dealloc

原因:gcd的block默认会做copy操作,即dispatch_after的block是堆block,block会对Person强引用,block销毁时候Person才会被释放。

Q2:上述代码如果换成__weak,Person什么时候释放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%p",weakPerson);
    });

    NSLog(@"touchesBegan");
}

输出结果:
14:38:42.996990+0800 test[1104:347260] touchesBegan
14:38:42.997481+0800 test[1104:347260] Person-dealloc
14:38:44.997136+0800 test[1104:347260] age:0x0

原因:使用__weak修饰过后的对象,堆block会采用弱引用,无法延时Person的寿命,所以在touchesBegan函数结束后,Person就会被释放,gcd就无法捕捉到Person。

Q3:如果gcd内包含gcd,Person会什么时候释放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-----age:%p",person);
        });
        NSLog(@"1-----age:%p",weakPerson);
    });

    NSLog(@"touchesBegan");
}

输出结果:
14:48:01.293818+0800 test[1199:403589] touchesBegan
14:48:05.294127+0800 test[1199:403589] 1-----age:0x604000015eb0
14:48:08.582807+0800 test[1199:403589] 2-----age:0x604000015eb0
14:48:08.583129+0800 test[1199:403589] Person-dealloc

原因:gcd内部只要有强引用Person,Person就会等待执行完再销毁!所以Person销毁时间为7秒。

Q4:如果gcd内部先强引用后弱引用,Person什么时候释放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-----age:%p",weakPerson);
        });
        NSLog(@"1-----age:%p",person);
    });

    NSLog(@"touchesBegan");
}

输出结果
14:52:29.036878+0800 test[1249:431302] touchesBegan
14:52:33.417862+0800 test[1249:431302] 1-----age:0x6000000178d0
14:52:33.418178+0800 test[1249:431302] Person-dealloc
14:52:36.418204+0800 test[1249:431302] 2-----age:0x0

原因:Person会等待强引用执行完毕后释放,只要强引用执行完,就不会等待后执行的弱引用,会直接释放的,所以Person释放时间为4秒。_weak修饰person后,则block不会强引用它,所以出了其作用域就会被销毁。


第五部分:__block修饰符

Q:block能否修改变量值?
auto修饰变量:block无法修改,因为block使用的时候是内部创建了变量来保存外部的变量的值,block只有修改内部自己变量的权限,无法修改外部变量的权限。
static修饰变量:block可以修改,因为block把外部static修饰变量的指针存入,block直接修改指针指向变量值,即可修改外部变量值。
全局变量值:全局变量无论哪里都可以修改,当然block内部也可以修改。

Q:__block int age = 10,系统做了哪些?
答案:编译器会将__block变量包装成一个对象
查看c++源码:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;//age的地址
 int __flags;
 int __size;
 int age;//age 的值
};

Q:__block 修饰符作用?

  • _block可以用于解决block内部无法修改auto变量值的问题
  • __block不能修饰全局变量、静态变量(static)
  • 编译器会将__block变量包装成一个对象
  • __block修改变量:age->__forwarding->age
  • __Block_byref_age_0结构体内部地址和外部变量age是同一地址


    __block的forwarding指针指向.png

Q:__block 能修改auto变量的本质?

  • 编译器会将__block变量包装成一个对象(在内存单独有空间保存数据)
  • __block修改变量:age->__forwarding->age

说明:block内部访问的&age地址是__block对象结构体内部的age地址。

Q:block在修改NSMutableArray,需不需要添加__block?
不需要。block内部可以拿到数组的&p地址,通过指针对数组进行加减等操作是可以的。

Q:block可以向NSMutableArray添加元素么?

NSMutableArray *arr = [NSMutableArray array];

Block block = ^{
    [arr addObject:@"123"];
    [arr addObject:@"2345"];
};

答案:可以,因为是addObject是使用NSMutableArray变量,而不是通过指针改变NSMutableArray。但是如果是arr = nil,这就是改变了NSMutableArray变量,会报错,这时需要用__block修饰才行。

5.1 __block的内存管理

当block在栈上时,并不会对__block变量产生强引用。
block用到某个对象时候,会对这个对象进行内存管理。

Q:block的属性修饰词为什么是copy?
block一旦没有进行copy操作,就不会在堆上
block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期。在栈上出来作用域数据可能变成脏数据

为什么block不能在栈上.png

Q:当block被copy到堆时,对__block修饰的变量做了什么?

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)
  • 对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用


    block0复制到堆上.png
block1复制到堆上.png

Q:当block从堆中移除时,对__block修饰的变量做了什么?

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)


    block被废弃.png
block1被废弃.png

Q:block对象类型的auto变量、__block变量的区别?
从几方面回答:

1.当block在栈上时,对它们都不会产生强引用
2.当block拷贝到堆上时

  • 都会通过copy函数来处理它们
  • 对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用。注意第三个参数值是不一样的
    __block变量(假设变量名叫做a)
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    对象类型的auto变量(假设变量名叫做p)
    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

3.当block从堆上移除时

  • 都会通过dispose函数来释放它们
    __block变量(假设变量名叫做a)
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    对象类型的auto变量(假设变量名叫做p)
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

4.__block的__forwarding指针

  • 栈上__block的__forwarding指向本身
  • 栈上__block复制到堆上后,栈上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身


    forwarding指向.png

Q:被__block修饰的对象类型在block上如何操作的?

分几方面回答:

1.当__block变量在栈上时,不会对指向的对象产生强引用
2.当__block变量被copy到堆时

  • 会调用__block变量内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
  • MRC环境下不会根据对象的修饰符引用,都是弱引用(注意是在有__block修饰的前提下)

3.如果__block变量从堆上移除

  • 会调用__block变量内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放指向的对象(release)

__block修饰对象、auto修饰对象内存指向图有什么区别?
auto修饰:block内的person指针,指向的MJPerson类对象内存。
__block修饰:block内的strongPerson指针,指向__block类对象 结构体struct_block_byref_person。struct_block_byref_person内的weakPerson根据属性强弱指向MJPerson的类对象。

__block内存流程.jpg

无__block内存流向.jpg


第六部分 block循环引用

block中如何形成的循环?
1.block强持有对象person,对象person强持有block。
2.block强持有__block对象,__block强持有person,person强持有block

block循环.jpg

block循环图1.png

block2循环与破解图.png

Q:ARC下如何解决block循环引用的问题?

三种方式:__weak、__unsafe_unretained、__block

1.第一种方式:__weak

Person *person = [[Person alloc] init];
//        __weak Person *weakPerson = person;
__weak typeof(person) weakPerson = person;

person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

2.第二种方式:__unsafe_unretained

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

3.第三种方式:__block

__block Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", person.age);
    person = nil;
};
person.block();

4.三种方法比较

  • __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil。
  • __unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。
  • __block:必须把引用对象置位nil,并且要调用该block();

Q:MRC下如何解决block循环引用的问题?
MRC环境下不支持弱指针,也就是不支持__weak。
所以两种方式:__unsafe_unretained、__block

1.第一种方式:__unsafe_unretained

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

2.第二种方式:__block

__block Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", person.age);
};

说明:本文参照李明杰老师课程、《iOS-Block本质》

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

推荐阅读更多精彩内容