KVO探究

KVO原理

  • KVO是基于runtime机制实现的
    当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
  • 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
  • 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
  • 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 `observeValueForKey:ofObject:change:context: ``也会被调用。

弄清为什么改变isa指针的指向,能修改调用的方法。

  • 查看实例对象内部结构
    打开xcode,按command+shift+O搜索objc.h,我们可以看到
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

我们可以看出

  1. Class是个objc_class类型的结构体
  2. 所有实例对象的本质是objc_object类型的结构体,里面存放这个实例对象的isa指针。
  3. id是一个objc_object类型的指针,这应该就是id可以指向任意对象的原因
  4. SEL是个objc_selector结构体类型的指针,存放的是方法名
  5. IMP函数指针,存放方法的具体实现地址。
  • 再来看看 objc_class
struct objc_class {
    Class _Nonnull isa  // 所属类的指针

#if !__OBJC2__
    Class _Nullable super_class       // 父类
    const char * _Nonnull name      // 类名                         
    long version       // 版本
    long info          // 运行期使用的位标识
    long instance_size            // 实例大小
    struct objc_ivar_list * _Nullable ivars     // 实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists    // 方法列表              
    struct objc_cache * _Nonnull cache      // 方法缓存                   
    struct objc_protocol_list * _Nullable protocols     // 协议列表
#endif

} OBJC2_UNAVAILABLE;

我们可以看出类中也有isa指针,这个isa指针是指向meta元类中。实例对象,类和元类的关系其实有一张图很明显。

0_1326963670oeC1.gif.png

isa的指向是从实例对象->类对象->元类对象->根元类对象->自己。

  • 当我们向一个实例对象发送消息,即调用了实例方法时,在运行时会顺着isa指针指向的类对象中查找相对应的方法,先从缓存中查找,如果缓存未命中,则从方法列表中查找。如果未找到,则一直顺着父类找到NSObject,如果还未找到,则走消息转发。
  • 当我们向一个类对象发送消息时,即使用类方法调用,在运行时会顺着isa,找到类的元类,在元类中查找类方法。先从缓存中查找,如果缓存未命中,则从方法列表中查找。如果未找到,则一直顺着父元类一直找直到找到NSObject,如果还未找到,则走消息转发。
那我们可以知道,由isa指针指向的对象才是真正调用方法的对象。在类中存储着实例对象的实例方法,元类中存储着类对象的类方法,KVO中的isa- swizzle就是交换isa的指向,本来是在A类中查找调用setter方法,运行时改成了在B类中查找调用setter方法。

使用KVO的方法。

  • 添加观察者
_dog = [[Animal alloc] init];
[_dog setValue:@"牧尘" forKey:@"name"];
// 添加KVO
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

// 触发KVO
_dog.name = @"小明";
  • KVO的监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}
  • 通知的移除
- (void)dealloc {
    [_dog removeObserver:self forKeyPath:@"name"];
}
  • 打印结果
2018-06-29 13:51:37.175922+0800 KVO与NSKeyValueObserving[38410:509677] 新值为小明
2018-06-29 13:51:37.176043+0800 KVO与NSKeyValueObserving[38410:509677] 旧值为牧尘

如果是简单使用,那知道这个就可以了。

使用探究

  • 我们再看下KVO的方法,可以看出这是通过KVO是对键值进行的观察。
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
  • 那么我们知道KVC访问的方式是通过下面两中方式。
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

能用第一种setValue:forKey:方式访问的肯定也能通过setValue: forKeyPath:方式访问,但是通过keyPath方式访问变量不一定能用第一种方式访问。

疑问一 :能通过KVO监听对象中另一个对象的属性么?

新增Food类,实例化为Animal类的对象。

#import <Foundation/Foundation.h>

@class Food;
/**
 动物类
 */
@interface Animal : NSObject
 // 姓名
@property (nonatomic, strong) NSString *name;
 // 食物 
@property (nonatomic, strong) Food *food;

@end


#pragma mark --- 食物
 // 食物类
@interface Food : NSObject
 // 水果
@property (nonatomic, strong) NSString *fruit;

@end
  • 对food.fruit进行监听
 [_dog setValue:@"苹果" forKeyPath:@"food.fruit"];
[_dog addObserver:self forKeyPath:@"food.fruit" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog setValue:@"荔枝" forKeyPath:@"food.fruit"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"food.fruit"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}
  • 结果
2018-06-29 14:13:45.002673+0800 KVO与NSKeyValueObserving[38503:523330] 新值为荔枝
2018-06-29 14:13:45.002896+0800 KVO与NSKeyValueObserving[38503:523330] 旧值为苹果

可以看出可以通过keyPath的方式对对象的对象进行监听。

疑问二 : 通过keyPath方式监听,只局限于属性监听么,实例对象不能监听么?
  • 属性可以通过点语法的方式访问变量,会调用setter方法。

  • KVC方式会查看如果有``setter方法,调用setter方式赋值,如果没有setter方法,检查accessInstanceVariablesDirectly。如果accessInstanceVariablesDirectly为YES,顺着_name,_isName,name,isName方式查找实例变量,有则赋值。没有则抛异常。

  • 测试通过实例变量,是不是setter方法能进行KVO的监听
    去除name的属性,新增isName实例变量。

@interface Animal() {
    NSString *isName;
}
@end

@implementation Animal

@end
[_dog setValue:@"牧尘" forKey:@"name"];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog setValue:@"小飞" forKey:@"name"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}
  • 结果
2018-06-29 14:36:51.210152+0800 KVO与NSKeyValueObserving[38565:537594] 新值为小飞
2018-06-29 14:36:51.210349+0800 KVO与NSKeyValueObserving[38565:537594] 旧值为牧尘

可以看出,KVO监听不仅能通过对属性进行监听,还能对类中的实例变量进行监听。

  • 为什么对实例变量也能监听在文章下说明。

手动触发KVO

  • NSKeyValueObserving中有一个方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

这个方法的含义是否自动对key进行观察,就是自动触发KVO。默认为YES。我们可以重写方法设置为NO,让我们手动触发。

  • 调用
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

方法来实现对key的手动监听。willChangeValueForKey:顾名思义,将要改变的时候调用的方法,那么didChangeValueForKey:就是已经改变的时候调用的方法,当NSKeyValueObservingOptionNSKeyValueObservingOptionOld| NSKeyValueObservingOptionNew

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _dog = [[Animal alloc] init];
    [_dog setValue:@"牧尘" forKey:@"name"];
    [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    NSLog(@"即将改变name");
    [_dog willChangeValueForKey:@"name"];
//  改变值
    [_dog setValue:@"小飞" forKey:@"name"];
//    _dog.name = @"小明";
    NSLog(@"已经改变name");
    [_dog didChangeValueForKey:@"name"];    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"调用了observeValueForKeyPath方法");
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}

打印

2018-06-29 16:58:38.552276+0800 KVO与NSKeyValueObserving[43395:635717] 即将改变name
2018-06-29 16:58:38.552577+0800 KVO与NSKeyValueObserving[43395:635717] 已经改变name
2018-06-29 16:58:38.552752+0800 KVO与NSKeyValueObserving[43395:635717] 调用了observeValueForKeyPath方法
2018-06-29 16:58:38.552873+0800 KVO与NSKeyValueObserving[43395:635717] 新值为小飞
2018-06-29 16:58:38.552998+0800 KVO与NSKeyValueObserving[43395:635717] 旧值为牧尘
  • 可以看出option值为newold只会在didChangeValueForKey :已经改变值后调用一次。
当option 为NSKeyValueObservingOptionPrior时
  • 结果
2018-06-29 17:04:28.115616+0800 KVO与NSKeyValueObserving[43412:639884] 即将改变name
2018-06-29 17:04:28.115937+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
2018-06-29 17:04:28.116069+0800 KVO与NSKeyValueObserving[43412:639884] 新值为(null)
2018-06-29 17:04:28.116329+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
2018-06-29 17:04:28.116471+0800 KVO与NSKeyValueObserving[43412:639884] 已经改变name
2018-06-29 17:04:28.116589+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
2018-06-29 17:04:28.116711+0800 KVO与NSKeyValueObserving[43412:639884] 新值为小飞
2018-06-29 17:04:28.116816+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
  • 我们可以得出结果- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;方法会在willChangeValueForKey :调用时触发一次,在didChangeValueForKey :调用时触发一次,其中调用willChangeValueForKey后是否触发方法和设置的NSKeyValueObservingOptions有关。

runtime到底是什么时候修改了对象的isa指针。

  • 重写model类的description方法
- (NSString *)description {
     // 查看当前isa指针指向的类
    Class runtimeClass = object_getClass(self);
     // 当前类的父类
    Class superClass = class_getSuperclass(runtimeClass);
    NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
    
    return  @"";
}
  • 在添加观察者前后打印代码
[_dog description];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog description];
  • 结果
2018-06-29 18:09:05.005066+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = Animal, superClass = NSObject
2018-06-29 18:09:05.005591+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = NSKVONotifying_Animal, superClass = Animal

我们可以看出,KVO在- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法调用后,在运行时动态生成了一个原来类的子类NSKVONotifying_XXX,导致我们通过点语法或者 KVC为属性赋值的时候,调用的是这个isa指向的类NSKVONotifying_XXX里的方法列表中的setter方法。

回到上面一个问题,实例变量为什么也能监听?

对实例变量通过KVC访问赋值,由于isa指针的混淆,KVC实际是对NSKVONotifying_XXX类调用。但是由于实例变量无法继承到NSKVONotifying_XXX中,但是KVO仍然会对NSKVONotifying_XXX调用- (void)setValue:(id)value forKey:(NSString *)key方法,NSKVONotifying_XXX调用失败,会调用super类的- (void)setValue:(id)value forKey:(NSString *)key,即赋值成功。我们可以测试一下

  • 创建一个Animal类的子类CatAnimal类中有个实例变量name。不存在name属性。
  • 重写AnimalCat- (void)setValue:(id)value forKey:(NSString *)key方法。
- (void)setValue:(id)value forKey:(NSString *)key {
    [super setValue:value forKey:key];
    NSLog(@"animal类中的value = %@", value);
}
  • 我们先将Cat类中的super链打断
- (void)setValue:(id)value forKey:(NSString *)key {
    NSLog(@"cat类中的value = %@",value);
  //  [super setValue:value forKey:key];
}
  • 测试打印
 Cat *cat = [[Cat alloc] init];
[cat setValue:@"小妖精" forKey:@"name"];
NSLog(@"catName = %@",[cat valueForKey:@"name"]);
  • 结果cat赋值失败,因为cat中没有name实例变量
NSKeyValueObserving[44918:845002] cat类中的value = 小妖精
2018-07-02 10:10:49.221572+0800 KVO与NSKeyValueObserving[44918:845002] catName = (null)
  • 我们将cat中的super链连接.
- (void)setValue:(id)value forKey:(NSString *)key {
    NSLog(@"cat类中的value = %@",value);
    [super setValue:value forKey:key];
}

打印结果,赋值成功,调用了父类中的- (void)setValue:(id)value forKey:(NSString *)key进行赋值,父类中有name实例变量。

NSKeyValueObserving[44937:846921] cat类中的value = 小妖精
2018-07-02 10:13:04.639062+0800 KVO与NSKeyValueObserving[44937:846921] animal类中的value = 小妖精
2018-07-02 10:13:04.639263+0800 KVO与NSKeyValueObserving[44937:846921] catName = 小妖精

KVO分别在KVC赋值的前后调用willChangeValueForKeydidChangeValueForKey实现监听调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context告诉我们值已经改变了。

这样KVC调用的赋值,在我们实际开发中也有用到,类似TextField@"_placeholderLabel.font"这样的键值,我们在子类化TextField,创建subClassTextField这样一个子类后,仍然可以用KVC的方法通过键值对TextFieldplaceholderLabel实例变量进行赋值。

查看KVC对实例变量调用KVO监听过程中,didChangeValueForKey的调用时机。

  • 设置系统自动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return YES;
}

重写- (void)setValue:(id)value forKey:(NSString *)key方法

- (void)setValue:(id)value forKey:(NSString *)key {
// 查看当前isa指针指向的类
    Class runtimeClass = object_getClass(self);
    // 当前类的父类
    Class superClass = class_getSuperclass(runtimeClass);
    NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
    NSLog(@"赋值前,name = %@",isName);
    [super setValue:value forKey:key];
    NSLog(@"赋值后,name = %@",isName);
}
  • 输出结果
2018-07-02 11:31:28.751652+0800 KVO与NSKeyValueObserving[45238:898392] runtimeClass = NSKVONotifying_Animal, superClass = Animal
2018-07-02 11:31:28.752193+0800 KVO与NSKeyValueObserving[45238:898392] 赋值前,name = 牧尘
2018-07-02 11:31:28.752513+0800 KVO与NSKeyValueObserving[45238:898392] 调用了observeValueForKeyPath方法
2018-07-02 11:31:28.752667+0800 KVO与NSKeyValueObserving[45238:898392] 新值为小飞
2018-07-02 11:31:28.752795+0800 KVO与NSKeyValueObserving[45238:898392] 旧值为牧尘
2018-07-02 11:31:28.752940+0800 KVO与NSKeyValueObserving[45238:898392] 赋值后,name = 小飞

我们可以看出,KVC调用实例属性触发didChangeValueForKey是在- (void)setValue:(id)value forKey:(NSString *)key方法中。

我们可以看出KVO监听的过程实际上是对值的监听,在值改变的前后调用willChangeValueForKeydidChangeValueForKey,无论是点语法还是KVC都是直接或间接对值的改变。

手动实现一个KVO

写法思路都一样,推荐一个文章
// 总结如下

1 通过NSSelectorFromString 获取setter的sel。 通过classGetInstanceMethod 获取 method。 判断method是否存在, 不存在return。
2 判断当前类是否是kvo类。 通过object_getClass 获取isa指向的类, 通过NSStringFromClass 回去类名。 判断类名是否有你的前缀。
3 如果不是kvo的类,则要去创建kvo的类。 通过NSClassFromString 获取类, 判断kvo名称的类是否已经被注册。 如果注册,则返回注册的类。 如果没注册,通过objc_allocateClassPair创建类, 通过class_addMethod 添加 class方法的实现,修改kvo class方法的实现, 学习Apple的做法, 隐瞒这个kvo_class。 然后objc_registerClassPair 注册类
4 通过class_addMethod 为kvo class添加setter方法的实现, 实现监听。 通过 valueForKey 获取 原始的指, 然后调用原类的setter方法赋值。 从关联对象中取出观察数组, callback告诉外部修改.
5 把对应的kvo信息存进 关联对象数组里面。

哪些情况下使用kvo会崩溃,怎么防护崩溃

使用不当 会crash,比如:

  • 添加和移出不是成对出现且存在多线程添加KVO的情况,经常遇到的crash是移出 - 内存dealloc的时候 或者对象销毁前没有正确移出Observer
    如何防护?
  • 1.注意移出对象 匹配
  • 2.内存野指针问题,一定要在对象销毁前移出观察者
  • 3.可以使用第三方库BlockKit添加KVO,blockkit内部会自动移除Observer避免crash.

kvo的优缺点

优点:

  • 方便两个对象间同步状态(keypath)更加方便,一般都是在A类要观察B类的属性的变化.
  • 非侵入式的得到某内部对象的状态改变并作出响应.(就是在不改变原来对象类的代码情况下即可做出对该对象的状态变化进行监听)
  • 可以嵌入更改前后的两个时机的状态. - 可以通过Keypaths对嵌套对象的监听.

缺点:

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

推荐阅读更多精彩内容