FBKVOController 源碼閱讀理解

FBKVOController 源碼閱讀理解

簡介

蘋果原生API提供的KVO有一些顯而易見的缺點(diǎn)场躯。

  • 添加和移除觀察者要配對出現(xiàn);
  • 移除一個未添加的觀察者纬霞,程序會crash;
  • 添加觀察者恩够,移除觀察者引有,通知回調(diào)蠢挡,三塊兒代碼過于分散;

那么,有沒有改良版的KVO呢化撕?FBKVOControllerFacebook開源的代碼几晤,主要是對我們經(jīng)常使用的 KVO機(jī)制進(jìn)行了額外的一層封裝,源碼簡單,設(shè)計感好植阴。其中最亮眼的特色是提供了一個block回調(diào)讓我們進(jìn)行處理蟹瘾,避免KVO的相關(guān)代碼四處散落。

使用

[observer.KVOControllerNonRetaining observe:object keyPath:@"keyPath"
 options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull 
 object, NSDictionary<NSString *,id> * _Nonnull change) {

}];

使用非常簡單掠手,提供了block回調(diào)憾朴,而且并不需要考慮remove observer的事情。同時還可以以數(shù)組形式喷鸽,同時對一個被觀察者object的多個不同成員變量進(jìn)行KVO众雷。

閱讀

KVOController一共只有兩個文件NSObject+FBKVOControllerFBKVOController

NSObject+FBKVOController中的代碼和邏輯非常簡單,通過Category的形式結(jié)合Runtime的特性报腔,通過objc_setAssociatedObject株搔,并支持懶加載的形式剖淀,給所有NSObject類添加了兩個FBKVOController類型的屬性纯蛾。

FBKVOController中定義了三個類,FBKVOController纵隔、_FBKVOSharedController_FBKVOInfo翻诉。因為代碼中直接使用的是FBKVOController,我們先看它捌刮。

初始化
// 在FBKVOController.h中
@property (nullable, nonatomic, weak, readonly) id observer;
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    // observer 本身會持有 FBKVOController,而如果FBKVOController再持有observer,那么必須使用weak
    _observer = observer;
      
    // 定義 NSMapTable key的內(nèi)存管理策略
    // retainObserved : 是否對 NSMapTable中的key(key是Observed-被觀察者)進(jìn)行retain操作
    // 在默認(rèn)情況碰煌,傳入的參數(shù) retainObserved = YES
    NSPointerFunctionsOptions keyOptions = retainObserved ? 
    NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : 
    NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
      
    //創(chuàng)建NSMapTable :key 為 id 類型,value 為 NSMutableSet<_FBKVOInfo *> 類型
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    
    // C語言中的互斥鎖绅作,一般在開發(fā)跨平臺的框架時使用
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

我在看源碼的時候比價陌生的是NSMapTable芦圾,于是查閱了一波資料,推薦一篇文章NSMapTable: 不只是一個能放weak指針的 NSDictionary俄认。

簡單來說个少,NSMapTableNSDictionary類似。

NSDictionaryKey的內(nèi)存策略是固定為copy,因此key應(yīng)該是小且高效的眯杏,以至于復(fù)制的時候不會對CPU 和內(nèi)存造成負(fù)擔(dān)夜焦。NSDictionary中真的只適合將值類型的對象作為key(如簡短字符串和數(shù)字)。當(dāng)keyobject時岂贩,copy的開銷可能比較大茫经!并不適合自己的模型類來做對象到對象的映射

NSMapTable可以自主控制key -> value的內(nèi)存管理策略,在這里只能使用相對比較靈活的 NSMapTable萎津。

注冊observe

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:
(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  // Debug模式下:不滿足條件的注冊卸伞,會產(chǎn)生斷言直接crash
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters
   observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    
  // 非Debug模式下:不滿足條件的注冊,直接返回
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath 
  options:options block:block];

  // observe object with info
  [self _observe:object info:info];
}

NSAssert()預(yù)處理宏捕獲錯誤锉屈,它與NSLog一樣荤傲,如果使用過多, 也會影響程序運(yùn)行。不用擔(dān)心部念,Xcode已經(jīng)幫我們設(shè)置好了弃酌,在debug模式下放心使用,Xcode已經(jīng)默認(rèn)將release環(huán)境下的斷言取消了, 免除了忘記關(guān)閉斷言造成的程序不穩(wěn)定儡炼。

在這里創(chuàng)建了一個_FBKVOInfo對象妓湘,使用調(diào)用者傳入的參數(shù)(除了object)和 self進(jìn)行初始化。_FBKVOInfo是一個模型類乌询,負(fù)責(zé)將記錄這些數(shù)據(jù)榜贴。

Snip20180426_8.png

接上段代碼的最后一句[self _observe:object info:info];

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);
    
  // _objectInfosMap : 初始化時創(chuàng)建的 NSMapTable
  // 其結(jié)構(gòu)是以 被觀察者 object 為 key唬党。并不像我們常用的 NSDictionary 那樣是以 NSString 為 key
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  // 使用 NSSet 的 member 方法判斷是否已存在
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  // 如果沒有 關(guān)于這個 object(被觀察者)的相關(guān)信息鹃共,則創(chuàng)建 NSMutableSet,并添加到 NSMapTable 中
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] observe:object info:info];
}

NSMapTable通過keyobject:被觀察的對象)驶拱,來查找對應(yīng)的valueNSMutableSet:存放_FBKVOInfo類型的info)霜浴,然后從NSMutableSet中查找是否已經(jīng)保存了相同_FBKVOInfo。如果已經(jīng)存在了相同的_FBKVOInfo蓝纲,那么就可以return不做操作阴孟;如果不存在該_FBKVOInfo,查看是否存在valueNSMutableSet)税迷,不存在則創(chuàng)建NSMutableSet永丝,與keyobject)映射添加到NSMapTable中。之后再將傳入的_FBKVOInfo添加到NSMutableSet中箭养。

避免添加重復(fù)的keypath

當(dāng)我在查看的時候比較在意的是NSSet這個方法:

// 判斷集合是否包含對象object
- (nullable ObjectType)member:(ObjectType)object;

它用來查看NSSet中是否已經(jīng)包含了相同的object慕嚷。之前的代碼中我們知道,傳入相同的keyPath也會創(chuàng)建不同的_FBKVOInfo毕泌,那么是如何做到避免了相同的keyPath重復(fù)添加的喝检?

通過重寫 - (NSUInteger)hash; 以及 - (BOOL)isEqual:(id)anObject; 這兩個方法,來告訴NSSet“相等”的含義懈词。

為了優(yōu)化判等的效率, 基于hashNSSetNSDictionary在判斷成員是否相等時, 會這樣做
Step 1: 集成成員的hash值是否和目標(biāo)hash值相等, 如果相同進(jìn)入Step 2, 如果不等, 直接判斷不相等
Step 2: 在hash值相同的情況下, 再進(jìn)行對象判等(- (BOOL)isEqual:), 作為判等的結(jié)果

hash值是對象判等的必要非充分條件

數(shù)據(jù)結(jié)構(gòu)

觀察者observe持有FBKVOController蛇耀,同時FBKVOController又弱引用(weak)了observe ,而FBKVOController擁有成員變量NSMapTable坎弯,NSMapTable以被觀察者(object)為key纺涤,NSMutableSetvalue,在NSMutableSet中,存儲了不同info抠忘。如圖:

Snip20180426_9.png
_FBKVOSharedController

_FBKVOSharedController 是單例類撩炊,其職責(zé)是:接收并轉(zhuǎn)發(fā)KVO通知,通過FBKVOController框架添加的KVO都由_FBKVOSharedController來處理。

- (instancetype)init
{
  self = [super init];
  if (nil != self) {
    NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
    _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
    if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
      _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
    } else {
      // silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
    }
#endif
    pthread_mutex_init(&_mutex, NULL);
  }
  return self;
}

初始化方法中有一個NSHashTable崎脉,同NSMapTable一樣拧咳,接觸不是很多。NSHashTable效仿了NSSet囚灼,但提供了比NSSet更多的操作選項骆膝,尤其是在對弱引用關(guān)系的支持上,NSHashTable在對象/內(nèi)存處理時更加的靈活灶体。相較于NSSet阅签,NSHashTable具有以下特性:

  1. NSSet(NSMutableSet)持有其元素的強(qiáng)引用,同時這些元素是使用hash值及isEqual:方法來做hash檢測及判斷是否相等的蝎抽。
  1. NSHashTable是可變的政钟,它沒有不可變版本。
  2. 它可以持有元素的弱引用,而且在對象被銷毀后能正確地將其移除养交。而這一點(diǎn)在NSSet是做不到的精算。
  3. 它的成員可以在添加時被拷貝。
  4. 它的成員可以使用指針來標(biāo)識是否相等及做hash檢測碎连。
  5. 它可以包含任意指針灰羽,其成員沒有限制為對象。我們可以配置一個NSHashTable實例來操作任意的指針破花,而不僅僅是對象谦趣。

在初始化中使用了NSPointerFunctionsWeakMemory,簡單來說就是定義NSHashTable中的元素采用弱引用內(nèi)存管理策略疲吸。當(dāng)里面存放的_FBKVOInfo銷毀時座每,NSHashTable自動將它移除。猜想由于在FBKVOController中摘悴,已經(jīng)通過NSMutableSet對_FBKVOInfo持有了一個強(qiáng)引用峭梳,那么這里采用弱引用特性的集合類型,給自己省去了很多麻煩的操作蹂喻,交由系統(tǒng)完成葱椭。

繼續(xù)追蹤之注冊observe的方法,最后一句代碼[[_FBKVOSharedController sharedController] observe:object info:info];

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }
  // register info
  // 注意:在 _FBKVOController 類中的 NSMutableSet 已經(jīng)強(qiáng)引用了 info
  // 這里是為了弱引用 info口四,才使用 NSHashTable孵运,當(dāng) info dealloc 時,同時會從容器中刪除
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  // add observer
  // _FBKVOSharedController 是實際的觀察者, 隨后會進(jìn)行轉(zhuǎn)發(fā)蔓彩。
  // context 是 void * 無類型指針治笨,是 info 的指針
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
    
  // 如果 state 是原始狀態(tài),則改為正在觀察的狀態(tài)赤嚼,表明是在正在觀察的狀態(tài)
  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // 這里是做容錯的處理,避免意外情況,與移除觀察者的邏輯相關(guān)
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

在這段代碼中旷赖,思路非常清晰,將所有本該在觀察者中寫的邏輯更卒,改為統(tǒng)一由_FBKVOSharedController中進(jìn)行注冊等孵,由_FBKVOSharedController這個單例類來注冊和接收。

有些特殊的是context參數(shù)使用的是(void *)info的指針蹂空,這樣可以保證context的唯一性,同時會將info傳遞給回調(diào)方法俯萌,也是為了做容錯處理,讓代碼更加嚴(yán)謹(jǐn)上枕。在修改_state狀態(tài)時咐熙,也考慮到了在移除觀察者方法中存在的某個漏洞,在這里進(jìn)行安全的移除姿骏。注釋的清晰糖声,嚴(yán)謹(jǐn)?shù)倪壿嫞?xì)心的設(shè)計,吾輩要多多學(xué)習(xí)蘸泻。

實現(xiàn)observeValueForKeyPath:ofObject:Change:context來接收通知:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, id> *)change
                       context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;

  {
    // lookup context in registered infos, taking out a strong reference only if it exists
    // 這里就很巧妙啊,通過注冊觀察時,將info傳過來,然后在NSHashTable中查看是否存在這個info
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

    // take strong reference to controller
    // 從這里拿到了FBKVOController
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      // take strong reference to observer
      // 從這里拿到了observer
      id observer = controller.observer;
      if (nil != observer) {

        // dispatch custom block or action, fall back to default action
        if (info->_block) {
          NSDictionary<NSString *, id> *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            // 字典合并琉苇,并重新拷貝一份,
            // 包含信息有:1悦施、改變了哪個值 mChange 2并扇、 原先的 change 字典
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
          }
          info->_block(observer, object, changeWithKeyPath);
        } else if (info->_action) {
          // 忽略警告!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          // 默認(rèn)情況 調(diào)用觀察者的原生函數(shù)B盏穷蛹!
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}
  1. context傳遞的是info,在NSHashTable中昼汗,查看是否還存在這個info肴熏。因為在NSHashTable是弱引用,所以它有可能已經(jīng)被釋放或者已經(jīng)被移除顷窒。
  2. 在傳遞參數(shù)和對象初始化賦值成員變量的時候蛙吏,考慮到安全性,像block鞋吉,NSString等這些要進(jìn)行copy操作鸦做。
移除

不用像使用原生的KVO考慮移除的問題,當(dāng)被觀察者object銷毀時谓着,注冊的觀察者也就不會再收到回調(diào)泼诱。
有些場景需要我們手動移除注冊,FBKVOController也提供了相關(guān)的方法赊锚。

/**
 移除被觀察的對象的某個屬性
 */
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;

/**
 移除被觀察對象的所有
 */
- (void)unobserve:(nullable id)object;

/**
 移除所有
 */
- (void)unobserveAll;

在實現(xiàn)中有這樣一段修改_state的代碼:

if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;

根據(jù)_state來進(jìn)行移除,并修改了_state的狀態(tài)治筒,這樣就和之前注冊observe時的一段移除邏輯相對應(yīng),如果在_FBKVOSharedController中還未注冊成功改抡,就被移除掉的話矢炼,那么_state狀態(tài)值是_FBKVOInfoStateNotObserving,那么在注冊時就會將這個注冊移除掉阿纤。

自釋放

FBKVOController是如何做到自釋放的句灌?可以歸納為四個字——動態(tài)屬性。其為觀察者綁定動態(tài)屬性self.KVOController欠拾,動態(tài)綁定的KVOController會隨著觀察者的釋放而釋放胰锌,KVOController在自己的dealloc函數(shù)中移除KVO監(jiān)聽,巧妙的將觀察者的remove轉(zhuǎn)移到其動態(tài)屬性的dealloc函數(shù)中藐窄。

注意

其還是有一定的局限性——對象無法監(jiān)聽自己的屬性资昧,如果你的代碼是這樣的:

[self.KVOController observe:self keyPath:@"date"
options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) {
    // to do
}];

很遺憾,循環(huán)引用的問題又出現(xiàn)荆忍,因為FBKVOController中的NSMapTable對象會retain key對象:

[_objectInfosMap setObject:infos forKey:object];

NSObject+FBKVOController中格带,動態(tài)添加了兩個屬性

@interface NSObject (FBKVOController)
// 會對被觀察者強(qiáng)引用
@property (nonatomic, strong) FBKVOController *KVOController;
// 會對被觀察者弱引用
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

當(dāng)在使用KVOController時撤缴,如果不手動取消對被觀察者的注冊,那么只有在observeFBKVOController被釋放時叽唱,被觀察者object才會被釋放掉屈呕。

總結(jié)

FBKVOController對于喜好使用kvo的工程師來說,是一個好的棺亭,精簡的開發(fā)框架虎眨。源碼優(yōu)雅,可讀性高镶摘,利于自己維護(hù)嗽桩。

優(yōu)點(diǎn)如下:

  1. 提供了干凈的block的回調(diào),避免了處理這個函數(shù)的邏輯散落的到處都是凄敢。
  2. 不用擔(dān)心remove問題碌冶,不用再在dealloc中寫remove代碼。當(dāng)然贡未,如果你需要在其他時機(jī)進(jìn)行remove observer,你大可放心的remove种樱,不會出現(xiàn)因為沒有添加而crash的問題。

缺點(diǎn):

  1. 對象無法監(jiān)聽自己的屬性俊卤,否則會出現(xiàn)循環(huán)引用。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末害幅,一起剝皮案震驚了整個濱河市消恍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌以现,老刑警劉巖狠怨,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異邑遏,居然都是意外死亡佣赖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門记盒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憎蛤,“玉大人,你說我怎么就攤上這事纪吮×┟剩” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵碾盟,是天一觀的道長棚辽。 經(jīng)常有香客問我,道長冰肴,這世上最難降的妖魔是什么屈藐? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任榔组,我火速辦了婚禮,結(jié)果婚禮上联逻,老公的妹妹穿的比我還像新娘瓷患。我一直安慰自己,他們只是感情好遣妥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布擅编。 她就那樣靜靜地躺著,像睡著了一般箫踩。 火紅的嫁衣襯著肌膚如雪爱态。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天境钟,我揣著相機(jī)與錄音锦担,去河邊找鬼。 笑死慨削,一個胖子當(dāng)著我的面吹牛洞渔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缚态,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼磁椒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了玫芦?” 一聲冷哼從身側(cè)響起浆熔,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桥帆,沒想到半個月后医增,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡老虫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年叶骨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祈匙。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡忽刽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菊卷,到底是詐尸還是另有隱情缔恳,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布洁闰,位于F島的核電站歉甚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扑眉。R本人自食惡果不足惜纸泄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一赖钞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聘裁,春花似錦雪营、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至镣陕,卻和暖如春谴餐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呆抑。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工岂嗓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹊碍。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓厌殉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親侈咕。 傳聞我的和親對象是個殘疾皇子公罕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容