iOS底層原理總結(jié) - 探尋KVO本質(zhì)

本篇主要是對(duì)小碼哥底層視頻學(xué)習(xí)的總結(jié)缘薛。方便日后復(fù)習(xí)乒躺。
上一篇《iOS底層原理總結(jié) - 探尋Class的本質(zhì)》:
http://www.reibang.com/p/748bc1d63184

本篇學(xué)習(xí)總結(jié):

  • 探尋KVO本質(zhì)
  • KVO底層方法調(diào)用順序
  • 代碼求證KVO實(shí)現(xiàn)機(jī)制
  • 探尋KVC本質(zhì)
  • KVC賦值和查找順序

好了衰猛,帶著問題,我們一一開始閱讀吧 ??

一.探尋KVO本質(zhì)

1.面試題:iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO示损?(KVO的本質(zhì)是什么馁痴?)

看到這個(gè)面試題,我們就從頭開始說一下什么是KVO,KVO的全稱 Key-Value Observing簿晓,俗稱“鍵值監(jiān)聽”眶拉,可以用于監(jiān)聽某個(gè)對(duì)象屬性值的改變。
先來一段代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    self.person2 = [[MJPerson alloc]init];
    self.person2.age = 2;
    // 給person1對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 10;
    self.person2.age = 20;
    
}
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
 
}
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)憔儿,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結(jié)果如下:
監(jiān)聽到<MJPerson: 0x2830605c0>的age屬性值改變了 - {
    kind = 1;
    new = 10;//新賦值數(shù)據(jù)
    old = 1;//舊賦值數(shù)據(jù) NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
} - 123 //添加監(jiān)聽時(shí)的上下文

上述代碼可以看出忆植,p1添加observer后,一旦age屬性值發(fā)生改變谒臼,會(huì)立刻通知observer朝刊,執(zhí)行observerobserveValueForKeyPath方法。

我們嘗試探尋KVO底層實(shí)現(xiàn)原理蜈缤,我們通過斷點(diǎn)方式探尋拾氓。
KVO底層原理分析

p1和p2的age屬性值發(fā)生改變,自然會(huì)調(diào)用-setAge方法底哥,我們把斷點(diǎn)打在-setAge方法這里咙鞍,斷點(diǎn)確實(shí)走到了這里,我們發(fā)現(xiàn)p1對(duì)象和p2對(duì)象調(diào)用同樣的setter方法趾徽,但是我們發(fā)現(xiàn)p1除了調(diào)用setter方法之外還會(huì)另外執(zhí)行observerobserveValueForKeyPath方法续滋,說明KVO在運(yùn)行時(shí)獲取對(duì)p1對(duì)象做了一些改變,使得p1對(duì)象在調(diào)用setage方法的時(shí)候可能做了一些額外的操作孵奶,所以問題出在對(duì)象身上疲酌,兩個(gè)對(duì)象在內(nèi)存中肯定不一樣,兩個(gè)對(duì)象可能本質(zhì)上并不一樣。接下來來探索KVO內(nèi)部是怎么實(shí)現(xiàn)的朗恳。

KVO底層原理實(shí)現(xiàn)
我們分別將斷點(diǎn)打到添加監(jiān)聽之前跟之后湿颅,

監(jiān)聽之前isa指針.png

監(jiān)聽之后isa指針.png

通過上面?zhèn)z圖我們會(huì)發(fā)現(xiàn),p1對(duì)象執(zhí)行addObserver操作之后僻肖,p1實(shí)例對(duì)象的isa指針指向NSKVONotifyin_Person類對(duì)象肖爵,而p2實(shí)例對(duì)象的isa指針還是指向MJPerson類對(duì)象
那么我們先來觀察p2對(duì)象在內(nèi)存中時(shí)如何存儲(chǔ)的臀脏,然后對(duì)比p2觀察p1

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 10;
    self.person2.age = 20;
    
}

首先我們知道,p2實(shí)例對(duì)象在調(diào)用-setAge方法的時(shí)候冀自,會(huì)通過p2實(shí)例對(duì)象中的isa指針找到MJPerson類對(duì)象揉稚,然后去類對(duì)象的方法列表中查找-setAge方法,進(jìn)而找到方法對(duì)應(yīng)的實(shí)現(xiàn)熬粗,如下圖所示:

p2實(shí)例對(duì)象調(diào)用setage方法路徑.png

但是剛才我們發(fā)現(xiàn)p1對(duì)象添加監(jiān)聽者之后搀玖,isa指針指向了NSKVONotifyin_Person類對(duì)象,NSKVONotifyin_Person類對(duì)象其實(shí)是MJPerson的類對(duì)象的子類驻呐,NSKVONotifyin_Person類對(duì)象的supercalss指針指向MJPerson的類對(duì)象灌诅。NSKVONotifyin_Person類對(duì)象是運(yùn)行時(shí)通過runtime生成的,那么p1實(shí)例對(duì)象調(diào)用setage方法含末,肯定是通過p1實(shí)例對(duì)象中的isa指針找到NSKVONotifyin_Person類對(duì)象猜拾,在NSKVONotifyin_Person類對(duì)象找到setage方法及實(shí)現(xiàn)。

小碼哥將NSKVONotifyin_Person類對(duì)象中的setage方法實(shí)現(xiàn)邏輯告訴我們了:

在調(diào)用NSKVONotifyin_Person類對(duì)象中的setage方法佣盒,其實(shí)是調(diào)用了Foundation框架中C語言函數(shù)_NSsetIntValueAndNotify挎袜,_NSsetIntValueAndNotify方法內(nèi)部的邏輯,首先是調(diào)用willChangeValueForKey告訴將要改變某一個(gè)屬性的值肥惭,然后調(diào)用父類的setage方法對(duì)成員變量賦值盯仪,最后調(diào)用didChangeValueForKey告訴已經(jīng)改變了某一個(gè)屬性的值,didChangeValueForKey中調(diào)用監(jiān)聽器的監(jiān)聽方法蜜葱,最終來到監(jiān)聽者的observeValueForKeyPath方法中全景。

到此,整個(gè)KVO調(diào)用底層流程就搞清楚了,我們需要用代碼求證一下步驟

二.代碼求證KVO實(shí)現(xiàn)流程

1.給p1添加observer后牵囤,在runtime時(shí)期動(dòng)態(tài)創(chuàng)建MJPerson類對(duì)象的子類即NSKVONotifyin_Person類對(duì)象爸黄,修改p1的isa指針指向?yàn)?strong>NSKVONotifyin_Person類對(duì)象,前面已經(jīng)有圖印證了這點(diǎn)奔浅。
2.為了驗(yàn)證p1添加observer前后調(diào)用-setAge方法后實(shí)現(xiàn)有什么區(qū)別馆纳,我們通過打印方法實(shí)現(xiàn)的地址來看一下p1和p2的-setAge的方法實(shí)現(xiàn)的地址在添加KVO前后有什么變化。

監(jiān)聽之前的IMP地址.png

監(jiān)聽之后的IMP地址.png

我們只是看實(shí)現(xiàn)的內(nèi)存地址汹桦,好像并不能看出什么鲁驶,前面不是說p1對(duì)象添加observer后,調(diào)用-setAge方法的時(shí)候舞骆,其實(shí)調(diào)用_NSsetIntValueAndNotify方法了嗎钥弯,方法名怎么看呢径荔?我們?cè)诮K端打印一下方法實(shí)現(xiàn),如下圖:
終端打印方法實(shí)現(xiàn).png

這一步也印證了咱們前面總結(jié)的
Foundation框架中會(huì)根據(jù)屬性的類型脆霎,調(diào)用不同的方法总处。例如我們之前定義的int類型的age屬性,那么我們看到Foundation框架中調(diào)用的_NSsetIntValueAndNotify函數(shù)睛蛛。那么我們把a(bǔ)ge的屬性類型變?yōu)閐ouble重新打印一遍
image.png

補(bǔ)充一點(diǎn)Founddation框架知識(shí):
我們發(fā)現(xiàn)調(diào)用的函數(shù)變?yōu)榱薩NSSetDoubleValueAndNotify鹦马,那么這說明Foundation框架中有許多此類型的函數(shù),通過屬性的不同類型調(diào)用不同的函數(shù)忆肾。
那么我們可以推測(cè)Foundation框架中還有很多例如_NSSetBoolValueAndNotify荸频、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify客冈、_NSSetLongValueAndNotify等等函數(shù)旭从。
我們可以找到Foundation框架文件,通過命令行查詢關(guān)鍵字找到相關(guān)函數(shù)

相關(guān)函數(shù).png

用一張圖來總結(jié)一下吧:


NSKVONotifying_MJPerson偽代碼.png
三.NSKVONotifyin_Person內(nèi)部結(jié)構(gòu)是怎樣的场仲?

首先我們知道和悦,NSKVONotifyin_Person作為MJPerson 類對(duì)象的子類,其superclass指針指向MJPerson類對(duì)象渠缕,并且NSKVONotifyin_Person內(nèi)部一定對(duì)-setAge方法做了單獨(dú)的實(shí)現(xiàn)鸽素,那么NSKVONotifyin_PersonMJPerson* 類的差別可能就在于其內(nèi)存儲(chǔ)的對(duì)象方法及實(shí)現(xiàn)不同。
我們通過runtime分別打印MJPerson* 類對(duì)象和NSKVONotifyin_Person類對(duì)象內(nèi)存儲(chǔ)的對(duì)象方法褐健。
先上代碼:

//MJPerson類
@interface MJPerson : NSObject
@property (assign, nonatomic) int age;
@end


//ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];
    p1.age = 1.0;
    Person *p2 = [[Person alloc] init];
    p1.age = 2.0;
    // self 監(jiān)聽 p1的 age屬性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

    [self printMethods: object_getClass(p2)];
    [self printMethods: object_getClass(p1)];

    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void) printMethods:(Class)cls
{
    unsigned int count ;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString: methodName];
        [methodNames appendString:@" "];
        
    }
    
    NSLog(@"%@",methodNames);
    free(methods);
}

打印結(jié)果如下圖:


runtime 打印結(jié)果.png

上述兩個(gè)類對(duì)象方法列表不一樣付鹿,我們直接看下圖:


NSKVONotifyin_Person內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序.png

這里有兩點(diǎn)要說明,一是-setAge方法內(nèi)部調(diào)用Foundation框架中的_NSSetIntValueAndNotify方法蚜迅,這點(diǎn)前面已經(jīng)講完了舵匾,二是重寫class方法,NSKVONotifyin_Person重寫class方法是為了隱藏NSKVONotifyin_Person。不被外界所看到。我們?cè)趐1添加過KVO監(jiān)聽之后镜遣,分別打印p1和p2對(duì)象的class可以發(fā)現(xiàn)他們都返回Person

NSLog(@"p1 = %@,p2 = %@",[self.person1 class],[self.person2 class]);
//打印結(jié)果如下:
 p1 = MJPerson,p2 = MJPerson

如果NSKVONotifyin_Person不重寫class方法遥倦,那么當(dāng)對(duì)象要調(diào)用class對(duì)象方法的時(shí)候就會(huì)一直向上找來到NSObject伞鲫,而NSObject的class的實(shí)現(xiàn)大致為返回自己isa指向的類,返回p1的isa指向的類那么打印出來的類就是NSKVONotifyin_Person,但是apple不希望將NSKVONotifyin_Person類暴露出來,并且不希望我們知道NSKVONotifyin_Person內(nèi)部實(shí)現(xiàn)蹋辅,所以在內(nèi)部重寫了class類,直接返回Person類挫掏,所以外界在調(diào)用p1的class對(duì)象方法時(shí)侦另,是Person類。這樣p1給外界的感覺p1還是Person類,并不知道NSKVONotifyin_Person子類的存在

那么我們可以猜測(cè)NSKVONotifyin_Person內(nèi)重寫的class內(nèi)部實(shí)現(xiàn)大致為

- (Class) class {
     // 得到類對(duì)象褒傅,在找到類對(duì)象父類
     return class_getSuperclass(object_getClass(self));
}

驗(yàn)證didChangeValueForKey:內(nèi)部會(huì)調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法
我們?cè)赑erson類中重寫willChangeValueForKey:和didChangeValueForKey:方法弃锐,模擬他們的實(shí)現(xiàn)

- (void)setAge:(int)age
{
    NSLog(@"setAge:");
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}
調(diào)用順序.png
四.探尋KVC本質(zhì)

先說一下KVC:Key Value Coding(鍵值編碼),獲取或者給對(duì)象的某個(gè)屬性進(jìn)行賦值殿托,常用方法如下:

- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

key 跟keypath的區(qū)別就是:key 是單純當(dāng)前調(diào)用對(duì)象的某個(gè)特定屬性名字霹菊,keypath既可以是某個(gè)特定屬性名字,也可以是某個(gè)路徑下查找的名字

五.KVC賦值和查找順序
KVC賦值順序.png
KVC取值順序.png

總結(jié)一下本篇面試題:

  • 1.iOS 用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO支竹?(KVO本質(zhì)是什么旋廷?)

答:當(dāng)一個(gè)對(duì)象使用了KVO監(jiān)聽,iOS系統(tǒng)會(huì)修改這個(gè)對(duì)象的isa指針礼搁,改為指向一個(gè)全新的通過runtime動(dòng)態(tài)創(chuàng)建的子類柳洋,子類擁有自己的setter方法實(shí)現(xiàn),setter方法實(shí)現(xiàn)內(nèi)部會(huì)調(diào)用Foundation框架的_NSSetIntValueAndNotify方法叹坦,在_NSSetIntValueAndNotify 方法里面調(diào)用willChangeValueForKey方法,然后調(diào)用父類的setter方法對(duì)成員變量賦值卑雁,最后調(diào)用didChangeValueForKey方法募书,最終通知監(jiān)聽者調(diào)用observeValueForKeyPath 方法,
這里我們要特別說明一點(diǎn):?jiǎn)渭兘o成員變量賦值测蹲,沒有調(diào)用willChangeValueForKey和didChangeValueForKey方法莹捡,最終也不會(huì)調(diào)用observeValueForKeyPath方法

  • 2.面試題:手動(dòng)能否觸發(fā)實(shí)現(xiàn)一個(gè)KVO?

答案是可以的扣甲,只要知道了KVO底層調(diào)用順序篮赢,就可以寫出來,以代碼為例說明:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    // 給person1對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    [self.person1 willChangeValueForKey:@"age"];
    self.person1.age = 10000;
    [self.person1 didChangeValueForKey:@"age"];
    
}

//- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
//{
//    self.person1.age = 10;
//}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// observeValueForKeyPath:ofObject:change:context:
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)琉挖,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
//打印結(jié)果如下:
監(jiān)聽到<MJPerson: 0x280a4c890>的age屬性值改變了 - {
    kind = 1;
    new = 10000;
    old = 1;
} - (null)

  • 3.面試題:KVC改變屬性值回引起KVO變化嗎启泣?

答:可以的,直接來看代碼及打印吧

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    // 給person1對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
//    [self.person1 willChangeValueForKey:@"age"];
//    [self.person1 setValue:@(101) forKey:@"age"];
//    [self.person1 didChangeValueForKey:@"age"];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.person1 setValue:@(102) forKey:@"age"];
}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// observeValueForKeyPath:ofObject:change:context:
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)示辈,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}

本篇學(xué)習(xí)先記錄到此寥茫,感謝閱讀,如有錯(cuò)誤矾麻,不吝賜教纱耻。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市险耀,隨后出現(xiàn)的幾起案子弄喘,更是在濱河造成了極大的恐慌,老刑警劉巖甩牺,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蘑志,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)卖漫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門费尽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人羊始,你說我怎么就攤上這事旱幼。” “怎么了突委?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵柏卤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我匀油,道長(zhǎng)缘缚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任敌蚜,我火速辦了婚禮桥滨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘弛车。我一直安慰自己齐媒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布纷跛。 她就那樣靜靜地躺著喻括,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贫奠。 梳的紋絲不亂的頭發(fā)上唬血,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音唤崭,去河邊找鬼拷恨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛浩姥,可吹牛的內(nèi)容都是我干的挑随。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼勒叠,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼兜挨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起眯分,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤拌汇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后弊决,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體噪舀,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡魁淳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了与倡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片界逛。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖纺座,靈堂內(nèi)的尸體忽然破棺而出息拜,到底是詐尸還是另有隱情,我是刑警寧澤净响,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布少欺,位于F島的核電站,受9級(jí)特大地震影響馋贤,放射性物質(zhì)發(fā)生泄漏赞别。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一配乓、第九天 我趴在偏房一處隱蔽的房頂上張望仿滔。 院中可真熱鬧,春花似錦犹芹、人聲如沸堤撵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洞豁,卻和暖如春盐固,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丈挟。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工刁卜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曙咽。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓蛔趴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親例朱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孝情,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344