iOS底层原理 - Category实现原理(一)

通过探索Category底层原理回答以下问题

  1. Category是否可以添加方法、属性、成员变量?Category是否可以遵守Protocol?
  2. Category的本质是什么,在底层是怎么存储的?
  3. Category的实现原理是什么,Catagory中的方法是如何调用到的?
  4. Category中是否有Load方法,load方法是什么时候调用的?
  5. load、initialize的区别

Category可以直接添加 属性、成员变量吗?

首先我们创建一个类MGCPerson,为该类添加一个分类MGCPerson (Sport),并尝试为该分类添加实例方法、类方法、属性、成员变量,观察是否报错
创建MGCPerson

// MGCPerson.h
@interface MGCPerson : NSObject

@property (nonatomic, assign) NSInteger height;
@property (nonatomic, assign) NSInteger weight;
@property (nonatomic, copy) NSString    *name;
@property (nonatomic, strong) NSDate    *brithday;

- (void)life;
+ (void)life;

@end

// MGCPerson.m
@implementation MGCPerson

- (void)life
{
    NSLog(@"MGCPerson : - (void)life");
}

+ (void)life
{
    NSLog(@"MGCPerson : + (void)life");
}

@end

添加分类MGCPerson (Sport),并尝试为该分类添加实例方法、类方法、属性、成员变量

Category-IVarError.png

通过操作我们发现,可以在Category中直接添加实例方法、类方法、属性以及准守协议,但是当尝试在Category中添加成员变量时会报错Instance variables may not be placed in categories,即不能直接在分类中添加成员变量。接下来我们继续探讨Category的本质,分析为什么不能在Category中直接添加成员变量

Category的底层数据结构

首先我们为MGCPerson创建两个分类MGCPerson (Sport)MGCPerson (Eat)
MGCPerson (Sport)

// MGCPerson+Sport.h
@interface MGCPerson (Sport) <NSCopying, NSCoding>

@property (nonatomic, assign) NSInteger stepCount;
@property (nonatomic, copy) NSDate      *sportStartTime;
@property (nonatomic, copy) NSDate      *sportEndTime;

- (void)sport;
+ (void)sport;
- (void)run;
+ (void)run;

@end

// MGCPerson+Sport.m
@implementation MGCPerson (Sport)

- (void)sport
{
    NSLog(@"MGCPerson (Sport) : - (void)sport");
}

+ (void)sport
{
    NSLog(@"MGCPerson (Sport) : + (void)sport");
}

- (void)run
{
    NSLog(@"MGCPerson (Sport) : - (void)run");
}

+ (void)run
{
    NSLog(@"MGCPerson (Sport) : + (void)run");
}

- (void)life
{
    NSLog(@"MGCPerson (Sport) : - (void)life");
}

+ (void)life
{
    NSLog(@"MGCPerson (Sport) : + (void)life");
}

@end

MGCPerson (Eat)

// MGCPerson+Eat.h
@interface MGCPerson (Eat)

+ (void)eat;
- (void)eat;

@end

// MGCPerson+Eat.m
@implementation MGCPerson (Eat)

- (void)eat
{
    NSLog(@"MGCPerson (Eat) : - (void)eat");
}

+ (void)eat
{
    NSLog(@"MGCPerson (Eat) : + (void)eat");
}

- (void)life
{
    NSLog(@"MGCPerson (Eat) : - (void)life");
}

+ (void)life
{
    NSLog(@"MGCPerson (Eat) : + (void)life");
}

@end

我们知道OC是基于C/C++实现的,接下来我们将通过命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件名将分类MGCPerson (Sport)MGCPerson (Eat)转化成.cpp文件,窥探Category的底层实现
分析.cpp文件思路
第一步: 因为我们分析的是.m转化称的.cpp文件,所以我们首先尝试在.cpp文件中查找关键字@implementation MGCPerson (Sport),通过检索,我们找到了对应方法的实现

// @implementation MGCPerson (Sport)

static void _I_MGCPerson_Sport_sport(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_0);
}


static void _C_MGCPerson_Sport_sport(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_1);
}


static void _I_MGCPerson_Sport_run(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_2);
}


static void _C_MGCPerson_Sport_run(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_3);
}


static void _I_MGCPerson_Sport_life(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_4);
}


static void _C_MGCPerson_Sport_life(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_5);
}

// @end

对函数名进行简单分析,得出命名规则


Category-MethodNameRule.png

第二步: 检索函数名_I_MGCPerson_Sport_sport_C_MGCPerson_Sport_sport进一步分析

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    3,
    {{(struct objc_selector *)"sport", "v16@0:8", (void *)_I_MGCPerson_Sport_sport},
    {(struct objc_selector *)"run", "v16@0:8", (void *)_I_MGCPerson_Sport_run},
    {(struct objc_selector *)"life", "v16@0:8", (void *)_I_MGCPerson_Sport_life}}
};

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_CLASS_METHODS_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    3,
    {{(struct objc_selector *)"sport", "v16@0:8", (void *)_C_MGCPerson_Sport_sport},
    {(struct objc_selector *)"run", "v16@0:8", (void *)_C_MGCPerson_Sport_run},
    {(struct objc_selector *)"life", "v16@0:8", (void *)_C_MGCPerson_Sport_life}}
};

分析可知:上述代码创建了_method_list_t类型的结构体变量_OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport_OBJC_$_CATEGORY_CLASS_METHODS_MGCPerson_$_Sport分别用于存储实例方法列表和类方法列表。

struct _method_list_t {
    unsigned int entsize;  // sizeof(struct _objc_method) 方法所占内存大小
    unsigned int method_count; // 方法个数
    struct _objc_method method_list[3]; // 方法列表
};
struct _objc_method {
    struct objc_selector * _cmd; // SEL,可以理解为函数名字符串
    const char *method_type; // 函数类型(包括返回值类型、参数类型)
    void  *_imp; // 指向函数实现
};

这里简单说下_objc_method中的method_type,详细介绍后续在探究runtime消息机制时再继续扒。method_type其实可以看做用字符缩写来表达的函数类型字符串,比如v@:i就是返回类型为void,第一个参数为id类型,第二个参数为指针类型,第三个参数为int类型的函数,如- (void)addStepCount:(int)count(我们知道iOS中方法调用会默认传入隐式参数 方法调用者:self 和 方法名:_cmd。这也是为什么我们可以在方法内部访问selfcmd的原因)

第三步:继续检索_OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport,继续分析

static struct _category_t _OBJC_$_CATEGORY_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "MGCPerson",
    0, // &OBJC_CLASS_$_MGCPerson,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MGCPerson_$_Sport,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MGCPerson_$_Sport,
    (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MGCPerson_$_Sport,
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MGCPerson_$_Sport,
};

分析可知:上述代码创建了一个_category_t类型的结构体变量 _OBJC_$_CATEGORY_MGCPerson_$_Sport,并且传入了类方法列表、实例方法列表、协议类别、属性列表,具备了Category的所有信息,没错Category在底层就是_category_t类型,下边我们检索结构体类型_category_t,看下_category_t的定义

struct _category_t {
    const char *name; // 类名(MGCPerson)
    struct _class_t *cls;  // 类对象指针(指向MGCPerson类对象)
    const struct _method_list_t *instance_methods; // 实例方法列表
    const struct _method_list_t *class_methods; // 类方法列表
    const struct _protocol_list_t *protocols; // 协议列表
    const struct _prop_list_t *properties; // 属性列表
};

到此我们已经清楚的看到了Category在底层的存储结构,并且可以看到底层并没有存储成员变量,这也就是为什么直接添加成员变量会报错的原因。
上边我们已经分析了_category_t_method_list_t,下边我们再看下_protocol_list_t_prop_list_t
先看_protocol_list_t,同样的在.cpp文件中检索_protocol_list_t

static struct /*_protocol_list_t*/ {
    long protocol_count;  // Note, this is 32/64 bit 协议个数
    struct _protocol_t *super_protocols[2]; // 协议数组(2是数组长度)
} _OBJC_CATEGORY_PROTOCOLS_$_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    2,
    &_OBJC_PROTOCOL_NSCopying, // 协议变量地址
    &_OBJC_PROTOCOL_NSCoding  // 协议变量地址
};

分析可知:上述代码定义了_protocol_list_t类型的结构体变量_OBJC_CATEGORY_PROTOCOLS_$_MGCPerson_$_Sport用于存储协议列表

struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
    0,
    "NSCopying",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};

struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
    0,
    "NSCoding",
    0,
    (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
    0,
    0,
    0,
    0,
    sizeof(_protocol_t),
    0,
    (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};

分析可知:上述代码定义了两个_protocol_t类型的协议变量_OBJC_PROTOCOL_NSCopying_OBJC_PROTOCOL_NSCoding

struct _protocol_t {
    void * isa;  // NULL
    const char *protocol_name; // 协议名称
    const struct _protocol_list_t * protocol_list; // super protocols 父协议
    const struct method_list_t *instance_methods; // 协议中实例方法列表
    const struct method_list_t *class_methods; // 协议中类方法列表
    const struct method_list_t *optionalInstanceMethods;// 协议中可选实现的实例方法列表
    const struct method_list_t *optionalClassMethods;// 协议中可选实现的类方法列表
    const struct _prop_list_t * properties; // 协议中的属性列表
    const unsigned int size;  // sizeof(struct _protocol_t) // 协议所占内存大小
    const unsigned int flags;  // = 0
    const char ** extendedMethodTypes;
};

再来看下_prop_list_t

static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t) 属性数组所占内存大小
    unsigned int count_of_properties; // 属性个数
    struct _prop_t prop_list[3]; // 属性数组
} _OBJC_$_PROP_LIST_MGCPerson_$_Sport __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    3,
    {{"stepCount","Tq,N"},
    {"sportStartTime","T@\"NSDate\",C,N"}, // 属性名、 类型名、copy、nonatomic
    {"sportEndTime","T@\"NSDate\",C,N"}} // 属性名、 类型名、copy、nonatomic
};

分析可知:上述代码创建了一个_prop_t类型的结构体变量_OBJC_$_PROP_LIST_MGCPerson_$_Sport用于存放属性列表

struct _prop_t {
    const char *name; // 属性名
    const char *attributes; // 属性描述,就是@property后边的一些关键字
};

到此我们已经了解了Category的底层数据结构,以及可以在Category中添方法、协议、属性,但是不能添加成员变量,那么Category中的属性和类中的属性是否一样的呢?如果不一样,怎么实现和类中添加属性一样的效果呢?接下来我们将探讨这个问题

Category中的属性

我们已经为MGCPerson添加了heightweightnamebrithday属性,为MGCPerson+Sport添加了stepCountsportStartTimesportEndTime属性

并且我们知道在类中添加一个属性,系统为我们做了三件事
@property (nonatomic, copy) NSString *name;

  • 创建了一个成员变量_name
  • 生成了settergetter方法的声明
  • 生成了settergetter方法的实现

那么在Category添加属性系统是否也会为我们做这三件事呢?

  • Category添加属性系统不会自动添加成员变量
    根据上边的学习我们知道,_category_t 中并没有存储成员变量
  • Category添加属性系统会生成settergetter方法的声明
    尝试在MGCPerson+Sport.h中直接敲对应的settergetter,发现是有提示的,说明会生成settergetter方法的声明
  • Category添加属性系统不会生成settergetter方法的实现
    下边我们比下MGCPerson.cppMGCPerson+Sport.cppp 文件
// @implementation MGCPerson

static void _I_MGCPerson_life(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_d3d3d6_mi_0);
}

static void _C_MGCPerson_life(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_d3d3d6_mi_1);
}

static NSInteger _I_MGCPerson_height(MGCPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_height)); }
static void _I_MGCPerson_setHeight_(MGCPerson * self, SEL _cmd, NSInteger height) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_height)) = height; }

static NSInteger _I_MGCPerson_weight(MGCPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_weight)); }
static void _I_MGCPerson_setWeight_(MGCPerson * self, SEL _cmd, NSInteger weight) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_MGCPerson$_weight)) = weight; }

static NSString * _Nonnull _I_MGCPerson_name(MGCPerson * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_MGCPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_MGCPerson_setName_(MGCPerson * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct MGCPerson, _name), (id)name, 0, 1); }

static NSDate * _Nonnull _I_MGCPerson_brithday(MGCPerson * self, SEL _cmd) { return (*(NSDate * _Nonnull *)((char *)self + OBJC_IVAR_$_MGCPerson$_brithday)); }
static void _I_MGCPerson_setBrithday_(MGCPerson * self, SEL _cmd, NSDate * _Nonnull brithday) { (*(NSDate * _Nonnull *)((char *)self + OBJC_IVAR_$_MGCPerson$_brithday)) = brithday; }
// @end
// @implementation MGCPerson (Sport)

static void _I_MGCPerson_Sport_sport(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_0);
}


static void _C_MGCPerson_Sport_sport(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_1);
}


static void _I_MGCPerson_Sport_run(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_2);
}


static void _C_MGCPerson_Sport_run(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_3);
}


static void _I_MGCPerson_Sport_life(MGCPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_4);
}


static void _C_MGCPerson_Sport_life(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gp_d5d9dd_x6kzfb5mn22zbqsg40000gn_T_MGCPerson_Sport_bea66e_mi_5);
}

// @end

可以看到,MGCPerson自动生成了对应属性的settergetter的实现方法,但是MGCPerson+Sport却没有生成对应属性的settergetter的实现方法,综上所述在Category添加属性系统仅仅只生成了settergetter方法的声明

如何使Category中的属性与类中的属性具备同样的效果(关联对象)

通过上边的学习,我们知道想达到与类添加属性一样的效果,我们需要做两件事

  • 添加一个存储属性值的变量
  • 实现settergetter方法
    可以通过runtime中的关联对象方法来实现
/ **
  *使用给定的键和关联策略为给定的对象设置关联值。
  *
  * @param object 关联的源对象。
  * @param key 关联的键。
  * @param value 与对象的键相关联的值。 传递nil清除现有关联。
  * @param policy 关联的策略
  *
  * /

objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)

// 关联策略,和@property后的关键字对应
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,       
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3, 
    OBJC_ASSOCIATION_RETAIN = 01401, 
    OBJC_ASSOCIATION_COPY = 01403
};
- (void)setSportEndTime:(NSDate *)sportEndTime
{
    objc_setAssociatedObject(self, @selector(sportEndTime), sportEndTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDate *)sportEndTime
{
    return objc_getAssociatedObject(self, _cmd);
}

系统是如何管理关联对象的

去官网下载runtime源码,搜索objc_setAssociatedObject方法,这里不做过多分析,简单说下结论,后续会单开一篇扒关联对象的实现原理。
runtime用四个类来管理关联对象,AssociationsManagerAssociationsHashMapAssociationsMapObjectAssociation,四个类之间的关系如下图

管理关联对象的类之间的关系.png

  • 对象并不是存储在被关联对象本身内存中
  • 关联对象存储在全局的统一的一个AssociationsManager中
  • 设置关联对象为nil,就相当于是移除关联对象

小尾巴

到此我们已经解决了开篇中的第1、2个问题,后续问题见iOS底层原理 - Category实现原理(二)

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

推荐阅读更多精彩内容