YYModel 阅读总结

简介(摘至官网

特性

  • 高性能: 模型转换性能接近手写解析代码。
  • 自动类型转换: 对象类型可以自动转换,详情见下方表格。
  • 类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
  • 无侵入性: 模型无需继承自其他基类。
  • 轻量: 该框架只有 5 个文件 (包括.h文件)。
  • 文档和单元测试: 文档覆盖率100%, 代码覆盖率99.6%。

基本使用

简单的 Model 与 JSON 相互转换

// JSON:
{
    "uid":123456,
    "name":"Harry",
    "created":"1965-07-31T00:00:00+0000"
}

// Model:
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end
@implementation User
@end

    
// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];
    
// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];

当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。

Model 属性名和 JSON 中的 Key 不相同

// JSON:
{
    "n":"Harry Pottery",
    "p": 256,
    "ext" : {
        "desc" : "A book written by J.K.Rowing."
    },
    "ID" : 100010
}

// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"name" : @"n",
             @"page" : @"p",
             @"desc" : @"ext.desc",
             @"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end

你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。

在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。

在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。

Model 包含其他 Model

// JSON
{
    "author":{
        "name":"J.K.Rowling",
        "birthday":"1965-07-31T00:00:00+0000"
    },
    "name":"Harry Potter",
    "pages":256
}

// Model: 什么都不用做,转换会自动完成
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end
    
@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author; //Book 包含 Author 属性
@end
@implementation Book
@end

容器类属性

@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end

@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
    return @{@"shadows" : [Shadow class],
             @"borders" : Border.class,
             @"attachments" : @"Attachment" };
}
@end

黑名单与白名单

@interface User
@property NSString *name;
@property NSUInteger age;
@end
    
@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
    return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
    return @[@"name"];
}
@end

数据校验与自定义转换

// JSON:
{
    "name":"Harry",
    "timestamp" : 1445534567
}
    
// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end

@implementation User
// 当 JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
    NSNumber *timestamp = dic[@"timestamp"];
    if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
    _createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
    return YES;
}
    
// 当 Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
    if (!_createdAt) return NO;
    dic[@"timestamp"] = @(n.timeIntervalSince1970);
    return YES;
}
@end

底层原理

YYModel 类结构

1305302-20180914153149933-1445616001.jpg

除掉YYModel.h之外,只剩下了YYClassInfo和NSObject+YYModel两个模块啦

  1. YYClassInfo功能主要是将Runtime层级中的一些结构体封装到NSObject中调用


    1305302-20180915103000006-951413839.jpg
  2. NSObject+YYModel功能是提供调用的接口以及实现具体的模型转换逻辑

前面已经讲到YYClassInfo主要功能是将Runtime层级的结构体封装到NSObject层级以便调用。下面是YYClassInfo与Runtime层级对比:


1305302-20180915103817595-987812236.png

详解

YYClassIvarInfo

YYClassIvarInfo && objc_ivar
// 类的属性描述
@interface YYClassIvarInfo : NSObject
// 成员变量
@property (nonatomic, assign, readonly) Ivar ivar;              ///< ivar opaque struct
// 变量名称
@property (nonatomic, strong, readonly) NSString *name;         ///< Ivar's name
// 成员变量地址偏移量
@property (nonatomic, assign, readonly) ptrdiff_t offset;       ///< Ivar's offset
// 成员变量类型编码类型
@property (nonatomic, strong, readonly) NSString *typeEncoding; ///< Ivar's type encoding
// 成员变量数据类型
@property (nonatomic, assign, readonly) YYEncodingType type;    ///< Ivar's type

/**
 Creates and returns an ivar info object.
 
 @param ivar ivar opaque struct
 @return A new object, or nil if an error occurs.
 */
// 初始化一个类成员变量描述
- (instancetype)initWithIvar:(Ivar)ivar;
@end

紧接着我们看一下Runtime的objc_ivar表示变量的结构体

struct objc_ivar {
    char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 变量名称
    char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 变量类型
    int ivar_offset OBJC2_UNAVAILABLE; // 变量偏移量
#ifdef __LP64__ // 如果已定义 __LP64__ 则表示正在构建 64 位目标
    int space OBJC2_UNAVAILABLE; // 变量空间
#endif
}

注:日常开发中,NSString类型的属性会用copy修饰,看上面YYClassIvarInfo中typeEncoding和name是用strong修饰。这是因为其内部先是通过Runtime方法拿到const char * 之后通过 stringWithUTF8String 方法之后转为 NSString 的。所以 NSString 这类属性在确定其不会在初始化之后出现被修改的情况下,使用 strong来修饰 做一次单纯的强引用在性能上是比 copy 要高的。

YYClassMethodInfo && objc_method

下面是YYClassMethodInfo

/**
 Method information.
 */
// 类方法的描述
@interface YYClassMethodInfo : NSObject
// 方法,实质上就是一个结构体
@property (nonatomic, assign, readonly) Method method;                  ///< method opaque struct
// 方法名
@property (nonatomic, strong, readonly) NSString *name;                 ///< method name
// 方法的选择子,实质上是可以跟 name 进行转换的
@property (nonatomic, assign, readonly) SEL sel;                        ///< method's selector
// 函数的实现
@property (nonatomic, assign, readonly) IMP imp;                        ///< method's implementation
// 参数的编码
@property (nonatomic, strong, readonly) NSString *typeEncoding;         ///< method's parameter and return types
// 返回值的编码
@property (nonatomic, strong, readonly) NSString *returnTypeEncoding;   ///< return value's type
// 所有的参数编码
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *argumentTypeEncodings; ///< array of arguments' type

/**
 Creates and returns a method info object.
 
 @param method method opaque struct
 @return A new object, or nil if an error occurs.
 */
// 初始化一个函数描述
- (instancetype)initWithMethod:(Method)method;
@end

YYClassMethodInfo则是对Rutime里面的objc_method的封装,紧接着我们看Runtime的objc_method结构体

struct objc_method {
    SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名称
    char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法类型
    IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法实现(函数指针)
}
YYClassPropertyInfo && property_t

YYClassPropertyInfo是对Runtime中property_t的封装

/**
 Property information.
 */
// 类属性的描述
@interface YYClassPropertyInfo : NSObject
// 属性
@property (nonatomic, assign, readonly) objc_property_t property; ///< property's opaque struct
// 属性名
@property (nonatomic, strong, readonly) NSString *name;           ///< property's name
// 编码类型 由 typeEncoding 转换而来
@property (nonatomic, assign, readonly) YYEncodingType type;      ///< property's type
// 编码类型
@property (nonatomic, strong, readonly) NSString *typeEncoding;   ///< property's encoding value
// 变量名
@property (nonatomic, strong, readonly) NSString *ivarName;       ///< property's ivar name
// 隶属的 class
@property (nullable, nonatomic, assign, readonly) Class cls;      ///< may be nil
// 遵守的协议
@property (nullable, nonatomic, strong, readonly) NSArray<NSString *> *protocols; ///< may nil
// get 方法
@property (nonatomic, assign, readonly) SEL getter;               ///< getter (nonnull)
// set 方法
@property (nonatomic, assign, readonly) SEL setter;               ///< setter (nonnull)

/**
 Creates and returns a property info object.
 
 @param property property opaque struct
 @return A new object, or nil if an error occurs.
 */
- (instancetype)initWithProperty:(objc_property_t)property;
@end

然后来看一下Runtime的property_t结构体

struct property_t {
    const char *name; // 名称
    const char *attributes; // 修饰
};
YYClassInfo && objc_class

YYClassInfo封装了Runtime的objc_class,下面看一下YYClassInfo

YYClassInfo

/**
 Class information for a class.
 */
// 类的描述
@interface YYClassInfo : NSObject
// 类
@property (nonatomic, assign, readonly) Class cls; ///< class object
// 父类
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object
// 元类
@property (nullable, nonatomic, assign, readonly) Class metaCls;  ///< class's meta class object
// 这个类是否是元类
@property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class
// 类名称
@property (nonatomic, strong, readonly) NSString *name; ///< class name
// 父类信息
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info
// 成员变量的描述信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< ivars
// 方法的描述信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< methods
// 属性的描述信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< properties

/**
 If the class is changed (for example: you add a method to this class with
 'class_addMethod()'), you should call this method to refresh the class info cache.
 
 After called this method, `needUpdate` will returns `YES`, and you should call 
 'classInfoWithClass' or 'classInfoWithClassName' to get the updated class info.
 */
//
// 更新
- (void)setNeedUpdate;

/**
 If this method returns `YES`, you should stop using this instance and call
 `classInfoWithClass` or `classInfoWithClassName` to get the updated class info.
 
 @return Whether this class info need update.
 */
// 是否更新
- (BOOL)needUpdate;

/**
 Get the class info of a specified Class.
 
 @discussion This method will cache the class info and super-class info
 at the first access to the Class. This method is thread-safe.
 
 @param cls A class.
 @return A class info, or nil if an error occurs.
 */
// 通过 class 初始化一个类描述对象
+ (nullable instancetype)classInfoWithClass:(Class)cls;

/**
 Get the class info of a specified Class.
 
 @discussion This method will cache the class info and super-class info
 at the first access to the Class. This method is thread-safe.
 
 @param className A class name.
 @return A class info, or nil if an error occurs.
 */
// 通过 类名 初始化一个描述对象
+ (nullable instancetype)classInfoWithClassName:(NSString *)className;

@end

objc_class

// objc.h
typedef struct objc_class *Class;
 
// runtime.h
struct objc_class {
    Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa 指针
 
#if !__OBJC2__
    Class _Nullable super_class OBJC2_UNAVAILABLE; // 父类(超类)指针
    const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名
    long version OBJC2_UNAVAILABLE; // 版本
    long info OBJC2_UNAVAILABLE; // 信息
    long instance_size OBJC2_UNAVAILABLE; // 初始尺寸
    struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表
    struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 缓存
    struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 协议列表
#endif
 
} OBJC2_UNAVAILABLE;

注解:下面是Runtime关于class的知识,下图是 《iOS 进阶》对oc对象模型的解释


1305302-20180915150609417-1293696256.jpg

下面是对应的讲解。


1305302-20180915150813320-283185146.png
YYClassInfo 的初始化
+ (instancetype)classInfoWithClass:(Class)cls {
    // 如果父类不存在,则结束调用
    if (!cls) return nil;
    // 类缓存
    static CFMutableDictionaryRef classCache;
    // 元类缓存
    static CFMutableDictionaryRef metaCache;
    static dispatch_once_t onceToken;
    // 信号量
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        // 初始化类缓存
        classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        // 初始化元类缓存
        metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        // 初始化信号量
        lock = dispatch_semaphore_create(1);
    });
    // 上锁
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    // 从元类缓存中查找类描述对象,如果是元类,如果是元类,就从元类缓存中找;如果cls 不是元类,从类缓存中找
    YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
    // 默认为NO 第一次加载 info 的时候会进入判断条件
    if (info && info->_needUpdate) {
        [info _update];
    }
    dispatch_semaphore_signal(lock);
    // 如果没有查找到
    if (!info) {
        // 实例化
        info = [[YYClassInfo alloc] initWithClass:cls];
        if (info) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            // 根据是否是元类属性区分存储
            CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
            dispatch_semaphore_signal(lock);
        }
    }
    return info;
}
下面总结一下初始化主要步骤:
  1. 首先创建单例缓存,类缓存和元类缓存
  2. 使用dispatch_semaphore 保证缓存线程安全
  3. 初始化操作之前首先缓存中查找是否已经向缓存中注册过的当前要初始化的YYClassInfo
  4. 如果查找缓存对象,需要判断对象是否需要更新以及其他相关操作
  5. 如果没有找到缓存对象,就开始初始化
  6. 初始化成功之后,向缓存中注册YYClassInfo实例

NSObject+YYModel

1305302-20180915153501929-876137726.jpg

NSObject+YYModel在YYModel主要任务是利用YYClassInfo层级封装的类来执行JSON模型之间的转换逻辑。下面是NSObject+YYModel讲述的主要内容:

  1. 类型编码的解析
  2. 数据结构的定义
  3. 递归模型的转换
  4. 接口相关的代码
数据结构的定义

NSObject+YYModel重新定义了两个类,来使用 YYClassInfo 中的封装。


1305302-20180915161006355-1364949982.png

_YYModelPropertyMeta

/// A property info in object model.
// 对象模型的属性信息
@interface _YYModelPropertyMeta : NSObject {
    @package
    // 属性的名称
    NSString *_name;             ///< property's name
    // 属性的类型
    YYEncodingType _type;        ///< property's type
    // 属性的基础类型
    YYEncodingNSType _nsType;    ///< property's Foundation type
    // 是否是数字类型
    BOOL _isCNumber;             ///< is c number type
    // 类
    Class _cls;                  ///< property's class, or nil
    // 泛型类
    Class _genericCls;           ///< container's generic class, or nil if threr's no generic class
    // get 方法
    SEL _getter;                 ///< getter, or nil if the instances cannot respond
    // set 方法
    SEL _setter;                 ///< setter, or nil if the instances cannot respond
    // KVO 兼容属性
    BOOL _isKVCCompatible;       ///< YES if it can access with key-value coding
    // 如果结构可以用键控归档器/非归档器编码,则可以
    BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver
    BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary:
    
    /*
     property->key:       _mappedToKey:key     _mappedToKeyPath:nil            _mappedToKeyArray:nil
     property->keyPath:   _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil
     property->keys:      _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath    _mappedToKeyArray:keys(array)
     */
    // 映射 key
    NSString *_mappedToKey;      ///< the key mapped to
    // 映射 keyPath,如果没有映射到 keyPath 则返回 nil
    NSArray *_mappedToKeyPath;   ///< the key path mapped to (nil if the name is not key path)
    // key 或者 keyPath 的数组,如果没有映射多个键的话则返回 nil
    NSArray *_mappedToKeyArray;  ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
    // 属性信息,详见上文 YYClassPropertyInfo && property_t 章节
    YYClassPropertyInfo *_info;  ///< property's info
    // 如果有多个属性映射到同一个 key 则指向下一个模型属性元
    _YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key.
}
@end

_YYModelMeta

// 对象模型的类信息
@interface _YYModelMeta : NSObject {
    @package
    // 类描述
    YYClassInfo *_classInfo;
    /// Key:mapped key and key path, Value:_YYModelPropertyMeta.
    // 属性映射器,包括key和keypath
    NSDictionary *_mapper;
    /// Array<_YYModelPropertyMeta>, all property meta of this model.
    // 类属性
    NSArray *_allPropertyMetas;
    /// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
    // 类中的所欲keypath 属性
    NSArray *_keyPathPropertyMetas;
    /// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
    // 所有key
    NSArray *_multiKeysPropertyMetas;
    /// The number of mapped key (and key path), same to _mapper.count.
    // key 的个数
    NSUInteger _keyMappedCount;
    /// Model class type.
    // 类型
    YYEncodingNSType _nsType;
    
    BOOL _hasCustomWillTransformFromDictionary;
    BOOL _hasCustomTransformFromDictionary;
    BOOL _hasCustomTransformToDictionary;
    BOOL _hasCustomClassFromDictionary;
}
@end
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容