给分类关联属性(objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects)

关联

关联是指把两个对象相互关联起来,使得其中的一个对象作为另外一个对象的一部分。    关联特性只有在Mac OS X V10.6以及以后的版本上才是可用的。

在类的定义之外为类增加额外的存储空间

    使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用。 关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(在垃圾自动回收环境下也不会导致资源不可回收)。

创建关联

    创建关联要使用到Objective-C的运行时函数:objc_setAssociatedObject来把一个对象与另外一个对象进行关联。该函数需要四个参数:源对象,关键字,关联的对象和一个关联策略。当然,此处的关键字和关联策略是需要进一步讨论的。

  ■  关键字是一个void类型的指针。每一个关联的关键字必须是唯一的。通常都是会采用静态变量来作为关键字。

  ■  关联策略表明了相关的对象是通过赋值,保留引用还是复制的方式进行关联的;还有这种关联是原子的还是非原子的。这里的关联策略和声明属性时的很类似。这种关联策略是通过使用预先定义好的常量来表示的。


为何创建关联

    我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用@property并不能在分类中正确创建实例变量和存取方法。

不过,通过 Objective-C 运行时中的关联对象,也就是 Associated Object,我们可以实现上述需求。


关联对象的应用

    关于关联对象的使用相信已经成为了一个老生常谈的问题了,不过为了保证这篇文章的完整性,笔者还是会在这里为各位介绍这部分的内容的。

分类中的 @property

@property可以说是一个 Objective-C 编程中的“宏”,它有元编程的思想。

@interfaceDKObject:NSObject@property(nonatomic,strong)NSString*property;@end

在使用上述代码时会做三件事:

生成实例变量_property

生成getter方法- property

生成setter方法- setProperty:

@implementationDKObject{NSString*_property;}- (NSString*)property {return_property;}- (void)setProperty:(NSString*)property {    _property = property;}@end

这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里,我们既然可以在类中使用@property生成一个属性,那么为什么在分类中不可以呢?

我们来做一个小实验:创建一个DKObject的分类Category,并添加一个属性categoryProperty:

@interfaceDKObject(Category)@property(nonatomic,strong)NSString*categoryProperty;@end

看起来还是很不错的,不过 Build 一下这个 Demo,会发现有这么一个警告:

objc-ao-warning-category-property

在这里的警告告诉我们categoryProperty属性的存取方法需要自己手动去实现,或者使用@dynamic在运行时实现这些方法。

换句话说,分类中的@property并没有为我们生成实例变量以及存取方法,而需要我们手动实现。

使用关联对象

Q:我们为什么要使用关联对象?

A:因为在分类中@property并不会自动生成实例变量以及存取方法,所以一般使用关联对象为已经存在的类添加『属性』

上一小节的内容已经给了我们需要使用关联对象的理由。在这里,我们会介绍 ObjC 运行时为我们提供的与关联对象有关的 API,并在分类中实现一个伪属性

#import"DKObject+Category.h"#import<objc/runtime.h>@implementationDKObject(Category)- (NSString*)categoryProperty {returnobjc_getAssociatedObject(self, _cmd);}- (void)setCategoryProperty:(NSString*)categoryProperty {    objc_setAssociatedObject(self,@selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}@end

这里的_cmd代指当前方法的选择子,也就是@selector(categoryProperty)。

我们使用了两个方法objc_getAssociatedObject以及objc_setAssociatedObject来模拟『属性』的存取方法,而使用关联对象模拟实例变量。

在这里有必要解释两个问题:

为什么向方法中传入@selector(categoryProperty)?

OBJC_ASSOCIATION_RETAIN_NONATOMIC是干什么的?

关于第一个问题,我们需要看一下这两个方法的原型:

idobjc_getAssociatedObject(idobject,constvoid*key);voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy);

@selector(categoryProperty)也就是参数中的key,其实可以使用静态指针static void *类型的参数来代替,不过在这里,笔者强烈推荐使用@selector(categoryProperty)作为key传入。因为这种方法省略了声明参数的代码,并且能很好地保证key的唯一性。

OBJC_ASSOCIATION_RETAIN_NONATOMIC又是什么呢?如果我们使用Command加左键查看它的定义:

typedefOBJC_ENUM(uintptr_t, objc_AssociationPolicy) {    OBJC_ASSOCIATION_ASSIGN =0,/**< Specifies a weak reference to the associated object. */OBJC_ASSOCIATION_RETAIN_NONATOMIC =1,/**< Specifies a strong reference to the associated object.

                                            *  The association is not made atomically. */OBJC_ASSOCIATION_COPY_NONATOMIC =3,/**< Specifies that the associated object is copied.

                                            *  The association is not made atomically. */OBJC_ASSOCIATION_RETAIN =01401,/**< Specifies a strong reference to the associated object.

                                            *  The association is made atomically. */OBJC_ASSOCIATION_COPY =01403/**< Specifies that the associated object is copied.

                                            *  The association is made atomically. */};

从这里的注释我们能看到很多东西,也就是说不同的objc_AssociationPolicy对应了不通的属性修饰符:

objc_AssociationPolicymodifier

OBJC_ASSOCIATION_ASSIGNassign

OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic, strong

OBJC_ASSOCIATION_COPY_NONATOMICnonatomic, copy

OBJC_ASSOCIATION_RETAINatomic, strong

OBJC_ASSOCIATION_COPYatomic, copy

而我们在代码中实现的属性categoryProperty就相当于使用了nonatomic和strong修饰符。

关于属性修饰符的区别,并不是这篇文章的主要内容,如果你需要了解它们的区别,Google是一个很好的选择。

到这里,我们已经完成了对关联对象应用的介绍,再来回顾一下小节的内容。

@property` 其实有元编程的思想,它能够为我们自动生成实例变量以及存取方法,而这三者构成了属性这个类似于语法糖的概念,为我们提供了更便利的点语法来访问属性:

self.property <=> [selfproperty]self.property = value <=> [selfsetProperty:value]

在分类中,因为类的实例变量的布局已经固定,使用@property已经无法向固定的布局中添加新的实例变量(这样做可能会覆盖子类的实例变量),所以我们需要使用关联对象以及两个方法来模拟构成属性的三个要素

如果你是一个 iOS 开发方面的新手,我相信这篇文章的前半部分对已经足够使用了,不过,如果你还对关联对象的实现非常感兴趣,也可以尝试阅读下面的内容。

关联对象的实现

    探索关联对象的实现一直是我想要做的一件事情,直到最近,我才有足够的时间来完成这篇文章,希望能够对各位读者有所帮助。

这一部分会从三个 objc 运行时的方法为入口来对关联对象的实现一探究竟,其中两个方法是上一部分使用到的方法:

voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy);idobjc_getAssociatedObject(idobject,constvoid*key);voidobjc_removeAssociatedObjects(idobject);

三个方法的作用分别是:

以键值对形式添加关联对象

根据key获取关联对象

移除所有关联对象

而接下来的内容自然就是围绕这三个方法进行的,我们会对它们的实现进行分析。

objc_setAssociatedObject

首先是objc_setAssociatedObject方法,这个方法的调用栈并不复杂:

voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy) └──voidobjc_setAssociatedObject_non_gc(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy)    └──void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy)

调用栈中的_object_set_associative_reference方法实际完成了设置关联对象的任务:

void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy) {    ObjcAssociation old_association(0,nil);idnew_value = value ? acquireValue(value, policy) :nil;    {        AssociationsManager manager;        AssociationsHashMap &associations(manager.associations());        ObjectAssociationMap *refs = i->second;        ...    }if(old_association.hasValue()) ReleaseValue()(old_association);}

在这里的实现省略了大多的实现代码,而且忽略了很多逻辑上的顺序,不过不要在意这里的代码能否执行。

我们需要注意其中的几个类和数据结构,因为在具体分析这个方法的实现之前,我们需要了解其中它们的作用:

AssociationsManager

AssociationsHashMap

ObjcAssociationMap

ObjcAssociation

AssociationsManager

AssociationsManager在源代码中的定义是这样的:

classAssociationsManager {staticspinlock_t _lock;staticAssociationsHashMap *_map;public:    AssociationsManager()  { _lock.lock(); }    ~AssociationsManager()  { _lock.unlock(); }        AssociationsHashMap &associations() {if(_map ==NULL)            _map = new AssociationsHashMap();return*_map;    }};spinlock_t AssociationsManager::_lock;AssociationsHashMap *AssociationsManager::_map =NULL;

它维护了spinlock_t和AssociationsHashMap的单例,初始化它的时候会调用lock.lock()方法,在析构时会调用lock.unlock(),而associations方法用于取得一个全局的AssociationsHashMap单例。

也就是说AssociationsManager通过持有一个自旋锁spinlock_t保证对AssociationsHashMap的操作是线程安全的,即每次只会有一个线程对 AssociationsHashMap 进行操作

如何存储 ObjcAssociation

ObjcAssociation就是真正的关联对象的类,上面的所有数据结构只是为了更好的存储它。

首先,AssociationsHashMap用与保存从对象的disguised_ptr_t到ObjectAssociationMap的映射:

classAssociationsHashMap : public unordered_map {public:void*operator new(size_t n) {return::malloc(n); }voidoperator delete(void*ptr) { ::free(ptr); }};

而ObjectAssociationMap则保存了从key到关联对象ObjcAssociation的映射,这个数据结构保存了当前对象对应的所有关联对象

classObjectAssociationMap : public std::map {public:void*operator new(size_t n) {return::malloc(n); }voidoperator delete(void*ptr) { ::free(ptr); }};

最关键的ObjcAssociation包含了policy以及value:

classObjcAssociation {    uintptr_t _policy;id_value;public:    ObjcAssociation(uintptr_t policy,idvalue) : _policy(policy), _value(value) {}    ObjcAssociation() : _policy(0), _value(nil) {}    uintptr_t policy()const{return_policy; }idvalue()const{return_value; }boolhasValue() {return_value !=nil; }};

举一个简单的例子来说明关联对象在内存中以什么形式存储的,以下面的代码为例:

intmain(intargc,constchar* argv[]) {@autoreleasepool{NSObject*obj = [NSObjectnew];        objc_setAssociatedObject(obj,@selector(hello),@"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);    }return0;}

这里的关联对象ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")在内存中是这么存储的:

objc-ao-associateobjcect

接下来我们可以重新回到对objc_setAssociatedObject方法的分析了。

在这里会将方法的执行分为两种情况:

new_value != nil设置/更新关联对象的值

new_value == nil删除一个关联对象

new_value != nil

先来分析在new_value != nil的情况下,该方法的执行是什么样的:

void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy) {    ObjcAssociation old_association(0,nil);idnew_value = value ? acquireValue(value, policy) :nil;    {        AssociationsManager manager;        AssociationsHashMap &associations(manager.associations());        disguised_ptr_t disguised_object = DISGUISE(object);        AssociationsHashMap::iterator i = associations.find(disguised_object);if(i != associations.end()) {            ObjectAssociationMap *refs = i->second;            ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {                old_association = j->second;                j->second = ObjcAssociation(policy, new_value);            }else{                (*refs)[key] = ObjcAssociation(policy, new_value);            }        }else{            ObjectAssociationMap *refs = new ObjectAssociationMap;            associations[disguised_object] = refs;            (*refs)[key] = ObjcAssociation(policy, new_value);            object->setHasAssociatedObjects();        }    }if(old_association.hasValue()) ReleaseValue()(old_association);}

使用old_association(0, nil)创建一个临时的ObjcAssociation对象(用于持有原有的关联对象,方便在方法调用的最后释放值)

调用acquireValue对new_value进行retain或者copy

staticidacquireValue(idvalue, uintptr_t policy) {switch(policy &0xFF) {caseOBJC_ASSOCIATION_SETTER_RETAIN:return((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);caseOBJC_ASSOCIATION_SETTER_COPY:return((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);    }returnvalue;}

初始化一个AssociationsManager,并获取唯一的保存关联对象的哈希表AssociationsHashMap

AssociationsManager manager;

AssociationsHashMap &associations(manager.associations());

先使用DISGUISE(object)作为 key 寻找对应的ObjectAssociationMap

如果没有找到,初始化一个ObjectAssociationMap,再实例化ObjcAssociation对象添加到 Map 中,并调用setHasAssociatedObjects方法,表明当前对象含有关联对象

ObjectAssociationMap *refs = new ObjectAssociationMap;

associations[disguised_object] = refs;

(*refs)[key] = ObjcAssociation(policy, new_value);

object->setHasAssociatedObjects();

如果找到了对应的ObjectAssociationMap,就要看key是否存在了,由此来决定是更新原有的关联对象,还是增加一个

ObjectAssociationMap *refs = i->second;ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {    old_association = j->second;    j->second = ObjcAssociation(policy, new_value);}else{    (*refs)[key] = ObjcAssociation(policy, new_value);}

最后的最后,如果原来的关联对象有值的话,会调用ReleaseValue()释放关联对象的值

structReleaseValue {voidoperator() (ObjcAssociation &association) {        releaseValue(association.value(), association.policy());    }};staticvoidreleaseValue(idvalue, uintptr_t policy) {if(policy & OBJC_ASSOCIATION_SETTER_RETAIN) {        ((id(*)(id, SEL))objc_msgSend)(value, SEL_release);    }}

到这里,该条件下的方法实现就结束了。

new_value == nil

如果new_value == nil,就说明我们要删除对应key的关联对象,实现如下:

void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy) {    ObjcAssociation old_association(0,nil);idnew_value = value ? acquireValue(value, policy) :nil;    {        AssociationsManager manager;        AssociationsHashMap &associations(manager.associations());        disguised_ptr_t disguised_object = DISGUISE(object);        AssociationsHashMap::iterator i = associations.find(disguised_object);if(i !=  associations.end()) {            ObjectAssociationMap *refs = i->second;            ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {                old_association = j->second;                refs->erase(j);            }        }    }if(old_association.hasValue()) ReleaseValue()(old_association);}

这种情况下方法的实现与前面的唯一区别就是,我们会调用erase方法,擦除ObjectAssociationMap中key对应的节点。

setHasAssociatedObjects()

其实上面的两种情况已经将objc_setAssociatedObject方法的实现分析得很透彻了,不过,这里还有一个小问题来等待我们解决,setHasAssociatedObjects()方法的作用是什么?

inlinevoidobjc_object::setHasAssociatedObjects() {if(isTaggedPointer())return; retry:    isa_t oldisa = LoadExclusive(&isa.bits);    isa_t newisa = oldisa;if(!newisa.indexed)return;if(newisa.has_assoc)return;    newisa.has_assoc =true;if(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))gotoretry;}

它会将isa结构体中的标记位has_assoc标记为true,也就是表示当前对象有关联对象,在这里我还想祭出这张图来介绍isa中的各个标记位都是干什么的。

objc-ao-isa-struct

如果想要了解关于 isa 的知识,可以阅读从 NSObject 的初始化了解 isa

objc_getAssociatedObject

我们既然已经对objc_setAssociatedObject的实现已经比较熟悉了,相信对于objc_getAssociatedObject的理解也会更加容易。

方法的调用栈和objc_setAssociatedObject非常相似:

idobjc_getAssociatedObject(idobject,constvoid*key)└──idobjc_getAssociatedObject_non_gc(idobject,constvoid*key);    └──id_object_get_associative_reference(idobject,void*key)

而_object_get_associative_reference相比于前面方法的实现更加简单。

id_object_get_associative_reference(idobject,void*key) {idvalue =nil;    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;    {        AssociationsManager manager;        AssociationsHashMap &associations(manager.associations());        disguised_ptr_t disguised_object = DISGUISE(object);        AssociationsHashMap::iterator i = associations.find(disguised_object);if(i != associations.end()) {            ObjectAssociationMap *refs = i->second;            ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {                ObjcAssociation &entry = j->second;                value = entry.value();                policy = entry.policy();if(policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);            }        }    }if(value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);    }returnvalue;}

代码中寻找关联对象的逻辑和objc_setAssociatedObject差不多:

获取静态变量AssociationsHashMap

以DISGUISE(object)为 key 查找AssociationsHashMap

以void *key为 key 查找ObjcAssociation

根据policy调用相应的方法

if(policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);if(value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {    ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);}

返回关联对象ObjcAssociation的值

objc_removeAssociatedObjects

关于最后的objc_removeAssociatedObjects方法,其实现也相对简单,这是方法的调用栈:

voidobjc_removeAssociatedObjects(idobject)└──void_object_remove_assocations(idobject)

这是简化版本的objc_removeAssociatedObjects方法实现:

voidobjc_removeAssociatedObjects(idobject) {if(object && object->hasAssociatedObjects()) {        _object_remove_assocations(object);    }}

为了加速移除对象的关联对象的速度,我们会通过标记位has_assoc来避免不必要的方法调用,在确认了对象和关联对象的存在之后,才会调用_object_remove_assocations方法移除对象上所有的关联对象:

void_object_remove_assocations(idobject) {    vector< ObjcAssociation,ObjcAllocator > elements;    {        AssociationsManager manager;        AssociationsHashMap &associations(manager.associations());if(associations.size() ==0)return;        disguised_ptr_t disguised_object = DISGUISE(object);        AssociationsHashMap::iterator i = associations.find(disguised_object);if(i != associations.end()) {            ObjectAssociationMap *refs = i->second;for(ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {                elements.push_back(j->second);            }            delete refs;            associations.erase(i);        }    }    for_each(elements.begin(), elements.end(), ReleaseValue());}

方法会将对象包含的所有关联对象加入到一个vector中,然后对所有的ObjcAssociation对象调用ReleaseValue()方法,释放不再被需要的值。

小结

关于应用

本来在这个系列的文章中并不会涉及关联对象这个话题,不过,有人问过我这么一个问题:在分类中到底能否实现属性?其实在回答这个问题之前,首先要知道到底属性是什么?而属性的概念决定了这个问题的答案。

如果你把属性理解为通过方法访问的实例变量,我相信这个问题的答案是不能,因为分类不能为类增加额外的实例变量

不过如果属性只是一个存取方法以及存储值的容器的集合,那么分类是可以实现属性的。

分类中对属性的实现其实只是实现了一个看起来像属性的接口而已

关于实现

关联对象又是如何实现并且管理的呢:

关联对象其实就是ObjcAssociation对象

关联对象由AssociationsManager管理并在AssociationsHashMap存储

对象的指针以及其对应ObjectAssociationMap以键值对的形式存储在AssociationsHashMap中

ObjectAssociationMap则是用于存储关联对象的数据结构

每一个对象都有一个标记位has_assoc指示对象是否含有关联对象


链接:https://www.jianshu.com/p/79479a09a8c0#%E5%85%B3%E8%81%94%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%BA%94%E7%94%A8

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

推荐阅读更多精彩内容