iOS进阶-详细介绍runtime

参考:

  1. https://www.jianshu.com/p/1a75373d5ba6
  2. https://blog.csdn.net/WflytoC/article/details/49926345

目录

  • runtime 概念
  • runtime的成员组成以及结构
  • runtime的消息转发机制流程
  • runtime 常用API 总结
  • runtime使用场景

一、runtime 概念

一套C语言标准库,oc 的运行时环境,它将程序类的类型确定、和函数调用逻辑从编译时移动到了运行,是oc成为一种动态语言。

二、runtime的成员组成以及结构

描述Objective-C对象所有的数据结构定义都在Runtime的头文件里,下面我们逐一分析。

1.id

运行期系统如何知道某个对象的类型呢?对象类型并不是在编译期就知道了,而是要在运行期查找。Objective-C有个特殊的类型id,它可以表示Objective-C的任意对象类型,id类型定义在Runtime的头文件中:

struct objc_object {
    Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为isa指针。

objc_object

objc_object是表示一个类的实例的结构体
它的定义如下(objc/objc.h):

struct objc_object{
     Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。

2.Class

Class对象也定义在Runtime的头文件中,查看objc/runtime.h中的objc_class结构体:
Objective-C中,类是由Class类型来表示的,它实际上是一个指
向objc_class结构体的指针。

typedef struct objc_class *Class;

struct objc_class { 
    Class isa                                 OBJC_ISA_AVAILABILITY; 
#if !__OBJC2__
    Class super_class                         OBJC2_UNAVAILABLE;   // 父类
    const char *name                          OBJC2_UNAVAILABLE;   // 类名
    long version                              OBJC2_UNAVAILABLE;   // 类的版本信息,默认为0
    long info                                 OBJC2_UNAVAILABLE;   // 类信息,供运行期使用的一些位标识
    long instance_size                        OBJC2_UNAVAILABLE;   // 该类的实例变量大小
    struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE;   // 该类的成员变量链表
    struct objc_method_list **methodLists     OBJC2_UNAVAILABLE;   // 方法定义的链表
    struct objc_cache *cache                  OBJC2_UNAVAILABLE;   // 方法缓存
    struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE;   // 协议链表
#endif
}

下面说下Class的结构体中的几个主要变量:

  • 1.isa:
    结构体的首个变量也是isa指针,这说明Class本身也是Objective-C中的对象。isa指针非常重要, 对象需要通过isa指针找到它的类, 类需要通过isa找到它的元类. 这在调用实例方法和类方法的时候起到重要的作用.
  • 2.super_class:
    结构体里还有个变量是super_class,它定义了本类的超类。类对象所属类型(isa指针所指向的类型)是另外一个类,叫做“元类”。
  • 3.ivars:
    成员变量列表,类的成员变量都在ivars里面。
  • 4.methodLists:
    方法列表,类的实例方法都在methodLists里,类方法在元类的methodLists里面。methodLists是一个指针的指针,通过修改该指针指向指针的值,就可以动态的为某一个类添加成员方法。这也就是Category实现的原理,同时也说明了Category只可以为对象添加成员方法,不能添加成员变量。
  • 5.cache:
    方法缓存列表,objc_msgSend(下文详解)每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,会优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。提高效率。

元类(Meta Class)

meta-class是一个类对象的类。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。
所以,调用类方法的这个类对象的isa指针指向的就是meta-class
当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。

即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下代码

image

看图说话:
上图中:superclass指针代表继承关系,isa指针代表实例所属的类。
类也是一个对象,它是另外一个类的实例,这个就是“元类”,元类里面保存了类方法的列表,类里面保存了实例方法的列表。实例对象的isa指向类,类对象的isa指向元类,元类对象的isa指针指向一个“根元类”(root metaclass)。所有子类的元类都继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。

1.Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。
2.isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用isKindOfClass:方法来确定实例对象的类。因为KVO的实现机制就是将被观察对象的isa指针指向一个中间类而不是真实的类。

Category

Category是表示一个指向分类的结构体的指针,其定义如下:

typedef struct objc_category *Category
struct objc_category{
     char *category_name                         OBJC2_UNAVAILABLE; // 分类名
     char *class_name                            OBJC2_UNAVAILABLE;  // 分类所属的类名
     struct objc_method_list *instance_methods   OBJC2_UNAVAILABLE;  // 实例方法列表
     struct objc_method_list *class_methods      OBJC2_UNAVAILABLE; // 类方法列表
     struct objc_protocol_list *protocols        OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性

struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表

3.SEL

//// http://www.jianshu.com/p/3e050ec3b759
SEL是选择子的类型,选择子指的就是方法的名字。在Runtime的头文件中的定义如下:

typedef struct objc_selector *SEL;

它就是个映射到方法的C字符串,SEL类型代表着方法的签名,在类对象的方法列表中存储着该签名与方法代码的对应关系,每个方法都有一个与之对应的SEL类型的对象,根据一个SEL对象就可以找到方法的地址,进而调用方法。
////http://www.jianshu.com/p/adf0d566c887
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
两个类之间,只要方法名相同,那么方法的SEL就是一样的,每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行
如在某一个类中定义以下两个方法: 错误

- (void)setWidth:(int)width;
- (void)setWidth:(double)width;

当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
@selector()就是取类方法的编号
通过下面三种方法可以获取SEL:
a、sel_registerName函数
b、Objective-C编译器提供的@selector()
c、NSSelectorFromString()方法

4.Method

Method代表类中的某个方法的类型,在Runtime的头文件中的定义如下:

typedef struct objc_method *Method;

objc_method的结构体定义如下:

struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
}

  • 1.method_name:方法名。
  • 2.method_types:方法类型,主要存储着方法的参数类型和返回值类型。
  • 3.IMP:方法的实现,函数指针。(下文详解)
    class_copyMethodList(Class cls, unsigned int *outCount)可以使用这个方法获取某个类的成员方法列表。

////
Method用于表示类定义中的方法
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。

5.Ivar

Ivar代表类中实例变量的类型,在Runtime的头文件中的定义如下:

typedef struct objc_ivar *Ivar;

objc_ivar的定义如下:

struct objc_ivar {
    char *ivar_name                   OBJC2_UNAVAILABLE; 
    char *ivar_type                   OBJC2_UNAVAILABLE; 
    int ivar_offset                   OBJC2_UNAVAILABLE; 
#ifdef __LP64__
    int space                         OBJC2_UNAVAILABLE;
#endif
}

class_copyIvarList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的成员变量列表。

6.objc_property_t

objc_property_t是属性,在Runtime的头文件中的的定义如下:

typedef struct objc_property *objc_property_t;

class_copyPropertyList(Class cls, unsigned int *outCount) 可以使用这个方法获取某个类的属性列表。

7.IMP

IMP在Runtime的头文件中的的定义如下:

typedef id (*IMP)(id, SEL, ...);

IMP是一个函数指针,它是由编译器生成的。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。
////
IMP实际上是一个函数指针,指向方法实现的地址。
其定义如下:

id (*IMP)(id, SEL,...)

第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。

前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。

8.Cache

Cache在Runtime的头文件中的的定义如下:

typedef struct objc_cache *Cache

objc_cache的定义如下:

struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};

每调用一次方法后,不会直接在isa指向的类的方法列表(methodLists)中遍历查找能够响应消息的方法,因为这样效率太低。它会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从isa指向的类的方法列表(methodLists)中查找方法。提高效率。

三、runtime的消息转发机制流程

  • [someObject msg_send([someObject class],SEL)]步骤:
第一步:检测selector是否可以被忽略,Mac OS X 系统有垃圾回收机制,不会理会retain ,release;
第二步: 检测selctor 对应的Target是否为nill,Rumtime 允许我们对一个nill对象执行任何方法,不会crash;
第三步:当前someObject对象通过isa指针找到对应的objc_Class(调用实例方法)或者objc_metaClass(调用类方法时);
第四步:在objc_Class内部的cache里通过SEL选择子进行匹配,如果找到对应的objc_Mehod,就用objc_Mehod内部的method_IMP找到对应的c函数执行,没有进入下一步;
第五步:在objc_Class内部的method_list里通过SEL选择子进行匹配,如果找到对应的objc_Mehod,就用objc_Mehod内部的method_IMP找到对应的c函数执行,没有进入下一步;
第六步: 通过objc_Class内部的super_class只找到父类,分类去对应的cache、method list寻找,找到就直接执行;没有继续下一步;
第七步:通过父类的root类去找,分类去对应的cache、method list寻找,找到就直接执行;没有继续下一步;
第八步: 如果没有找到,就会执行消息转发(message forwarding);
  • 详细介绍消息转发步骤:


    image.png

rumtime 在发送消息 没有找对用对应目标对象需要执行的任务时,允许我们进行3次修正:

  1. 方法动态解析: 目标通过自己是想新的IMP函数和resolveInstenceMethod或者resolveClassMethod ; 如果这两个方法参数都是没有找到对应地址的SEL变量,如果实现类存在对应的方法,首先runtime 为当前的SEL变量重新设置IMP指针,并且返回Yes,rumtime会重新执行消息发送;
void fooMethod(id obj, SEL _cmd)  
{
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

  1. 快速转发 : 找到实现 forwaringTargetingSelector ,参数是没有执行的SEL ,forwaringTargetingSelector内部如果如果自己存在SEL一样的函数,就会将当前的对象返回出去,runtime会重新想当前新的目标对象发送消息;
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(foo:)){
        return [[BackupClass alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
  1. 正常转发: 同快速转发都是想新的目标对象发送消息,但是可以代替快速转发做更多的事。
    forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。
    其中,参数invocation是从哪来的?在forwardInvocation:消息发送前,runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都继承了forwardInvocation:方法,我们可以将消息转发给其它的对象。
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if([alternateObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:alternateObject];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}

这里附加NSObject+CrashHandle代码

#import "NSObject+CrashLogHandle.h"

@implementation NSObject (CrashLogHandle)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    //方法签名
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}

@end

四、runtime 常用API 总结

第一类 : 为对象工作的API

object_getIvar : 获取对象的某个实例变量的值;
object_setIvar : 设置对象的某个实例变量的值;
object_getClassName : 获取当前对象的isa所指的类的名字;NSStringFromClass(Class )
object_getClass : 获取当前对象的isa所指的类;
object_setClass : 设置对象的isa所指的类;

第二类: 为Class工作的API

class_getName: 获取Class变量的名字; 相当于NSStringFromClass(Class )
class_getSupperClass : 获取父类;
class_isMetaClass : 判断是否是元类;
class_addIvar: 为class添加成员变量; 这个步骤要在 alloc之后register之前才有效果;
class_add_Method : 为当前的类添加实例方法
class_getInstanceMethod 获取当前类的实例方法
class_getClassMethod 获取当前类的实例方法
class——copyMethodList 获取当前类的所有的方法
class_replaceMethod 替换某个类的方法签名对应的实现
class_respondsToSelector 判断某个类是否存在SEL对应的函数

第三类 : 添加类相关

objc_allocateClassPair 创建一个类 里面要穿入你要继承的类Class,新的类名c字符串,大小
objc_disposeClassPair 销毁一个类和它对应的是元类
objc_registerClassPair 将创建的类假如内存,一般在添加完方法、成员变量、属性、协议、分类、扩展后使用

实例化一个类相关

class_createInstance 默认在malloc memory zone.

objc_destructInstance 销毁一个实例对象

成员变量相关

ivar_getName: 获取 Ivar的变量名字
ivar_getTypeEncoding: 获取 Ivar的变量类型

观象对象相关

objc_setAssociatedObject : 设置一个对象的关联对象,让源对象持有关联对象 参数 1 : 源对象 2、唯一的关键一是一个void的指针变量 3、关联对象 4关联规则
objc_getAssociatedObject : 通过关键字获取一个对象它所有持有对象 参数: 1、目标对象 2、关联对象的关键字
objc_removeAssociatedObjects 移除目标对象所有关联的对象 参数: 目标对象

发送消息相关 ****比较关键的

objc_msgSend : 向目标对象发送函数调用消息 参数:1、目标对象 2、SEL函数唯一签名 有关调用过程上面介绍过

runtime 内部相关的结构对象

An opaque type that represents an Objective-C class.

Method

An opaque type that represents a method in a class definition.

Ivar

An opaque type that represents an instance variable.

Category

An opaque type that represents a category.

objc_property_t

An opaque type that represents an Objective-C declared property.

IMP

A pointer to the start of a method implementation.

SEL

Defines an opaque type that represents a method selector.

objc_method_description

Defines an Objective-C method.

objc_method_list

Contains an array of method definitions.

Deprecated

objc_cache

Performance optimization for method calls. Contains pointers to recently used methods.

objc_protocol_list

Represents a list of formal protocols.

objc_property_attribute_t

Defines a property attribute.

五、runtime使用场景

5.1. kvo

介于传统的kvo实现方式过于的死板,只能通过代理方法实现监听回调,因此,我自己用 runtime 写了一个NSOBject+ggzKVO的分类,主要实现 : 一层属性监听和多层属性监听 2、监听回调方法定义函数回调、block实现回调
实现原理:
1、创建目标类的派生类,将派生类的isa指针指向目标类;
2、将观察关系的信息保存在一个字典里,字典放在集成在NSObject的对象里;
3、重写父类的setter方法,在改变父类的值之前获取旧值,之后获取新的值,然后获取观察信息,获取当前key对应的对调函数或者block进行回调
说着简单,不如自己手动写一下

github-custom-runtime-kvo 连接 : https://github.com/ge123/test-runtime-kvo

5.2. kvc :
setValue forKey:(NSString*) key 查找顺序: 去查找对应的set<key>方法,如果没有找到如果本类的accessInstanceVariablesDirectly属性返回YES,则按 _<key>、_is<key> 、key、is<key>的顺序去查找,如果最后还是没找到,就回调setValue:forUndefinedKey方法;我们可以重写setValue:forUndefinedKey方法进行防止报错;
github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime-kvc

5.3. JSON转Model
github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime--

5.4. categary 实现添加属性成员变量
github-custom-runtime-kvo 连接 : https://github.com/ge123/runtime-categary--

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

推荐阅读更多精彩内容