如何優(yōu)雅地使用 KVO

KVO 作為 iOS 中一種強(qiáng)大并且有效的機(jī)制弥奸,為 iOS 開發(fā)者們提供了很多的便利;我們可以使用 KVO 來檢測對象屬性的變化捺癞、快速做出響應(yīng),這能夠?yàn)槲覀冊陂_發(fā)強(qiáng)交互咖耘、響應(yīng)式應(yīng)用以及實(shí)現(xiàn)視圖和模型的雙向綁定時(shí)提供大量的幫助翘簇。

但是在大多數(shù)情況下,除非遇到不用 KVO 無法解決的問題儿倒,筆者都會盡量避免它的使用版保,這并不是因?yàn)?KVO 有性能問題或者使用場景不多,總重要的原因是 KVO 的使用是在是太 ** 麻煩了夫否。

trouble

使用 KVO 時(shí)彻犁,既需要進(jìn)行注冊成為某個(gè)對象屬性的觀察者,還要在合適的時(shí)間點(diǎn)將自己移除凰慈,再加上需要覆寫一個(gè)又臭又長的方法汞幢,并在方法里判斷這次是不是自己要觀測的屬性發(fā)生了變化,每次想用 KVO 解決一些問題的時(shí)候微谓,作者的第一反應(yīng)就是頭疼森篷,這篇文章會為各位為 KVO 所苦的開發(fā)者提供一種更優(yōu)雅的解決方案。

使用 KVO

不過在介紹如何優(yōu)雅地使用 KVO 之前豺型,我們先來回憶一下仲智,在通常情況下,我們是如何使用 KVO 進(jìn)行鍵值觀測的姻氨。

首先钓辆,我們有一個(gè) Fizz 類,其中包含一個(gè) number 屬性肴焊,它在初始化時(shí)會自動(dòng)被賦值為 @0

// Fizz.h
@interface Fizz : NSObject

@property (nonatomic, strong) NSNumber *number;

@end

// Fizz.m
@implementation Fizz

- (instancetype)init {
    if (self = [super init]) {
        _number = @0;
    }
    return self;
}

@end

我們想在 Fizz 對象中的 number 對象發(fā)生改變時(shí)獲得通知得到的和的值前联,這時(shí)我們就要祭出 -addObserver:forKeyPath:options:context 方法來監(jiān)控 number 屬性的變化:

Fizz *fizz = [[Fizz alloc] init];
[fizz addObserver:self
       forKeyPath:@"number"
          options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
          context:nil];
fizz.number = @2;

在將當(dāng)前對象 self注冊成為 fizz 的觀察者之后,我們需要在當(dāng)前對象中覆寫 -observeValueForKeyPath:ofObject:change:context: 方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"number"]) {
        NSLog(@"%@", change);
    }
}

在大多數(shù)情況下我們只需要對比 keyPath 的值娶眷,就可以知道我們到底監(jiān)控的是哪個(gè)對象似嗤,但是在更復(fù)雜的業(yè)務(wù)場景下,使用 context 上下文以及其它輔助手段才能夠幫助我們更加精準(zhǔn)地確定被觀測的對象届宠。

但是當(dāng)上述代碼運(yùn)行時(shí)双谆,雖然可以成功打印出 change 字典壳咕,但是卻會發(fā)生崩潰,你會在控制臺中看到下面的內(nèi)容:

2017-02-26 23:44:19.666 KVOTest[15888:513229] {
    kind = 1;
    new = 2;
    old = 0;
}
2017-02-26 23:44:19.720 KVOTest[15888:513229] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x60800001dd20 of class Fizz was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x60800003d320> (
<NSKeyValueObservance 0x608000057310: Observer: 0x7fa098f07590, Key path: number, Options: <New: YES, Old: YES, Prior: NO> Context: 0x0, Property: 0x608000057400>
)'

這是因?yàn)?fizz 對象沒有被其它對象引用顽馋,在脫離 viewDidLoad 作用于之后就被回收了,然而在 -dealloc 時(shí)幌羞,并沒有移除觀察者寸谜,所以會造成崩潰。

我們可以使用下面的代碼來驗(yàn)證上面的結(jié)論是否正確:

// Fizz.h
@interface Fizz : NSObject

@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, weak) NSObject *observer;

@end

// Fizz.m
@implementation Fizz

- (instancetype)init {
    if (self = [super init]) {
        _number = @0;
    }
    return self;
}

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

@end

Fizz 類的接口中添加一個(gè) observer 弱引用來持有對象的觀察者属桦,并在對象 -dealloc 時(shí)將它移除熊痴,重新運(yùn)行這段代碼,就不會發(fā)生崩潰了聂宾。

not-crash-with-remove-observer-when-deallo

由于沒有移除觀察者導(dǎo)致崩潰使用 KVO 時(shí)經(jīng)常會遇到的問題之一果善,解決辦法其實(shí)有很多,我們在這里簡單介紹一個(gè)系谐,使用當(dāng)前對象持有被觀測的對象巾陕,并在當(dāng)前對象 -dealloc 時(shí),移除觀察者:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.fizz = [[Fizz alloc] init];
    [self.fizz addObserver:self
                forKeyPath:@"number"
                   options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                   context:nil];
    self.fizz.number = @2;
}

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

這也是我們經(jīng)常使用來避免崩潰的辦法纪他,但是在筆者看來也是非常的不優(yōu)雅鄙煤,除了上述的崩潰問題,使用 KVO 的過程也非常的別扭和痛苦:

  1. 需要手動(dòng)移除觀察者茶袒,且移除觀察者的時(shí)機(jī)必須合適梯刚;
  2. 注冊觀察者的代碼和事件發(fā)生處的代碼上下文不同,傳遞上下文是通過 void * 指針薪寓;
  3. 需要覆寫 -observeValueForKeyPath:ofObject:change:context: 方法亡资,比較麻煩;
  4. 在復(fù)雜的業(yè)務(wù)邏輯中向叉,準(zhǔn)確判斷被觀察者相對比較麻煩锥腻,有多個(gè)被觀測的對象和屬性時(shí),需要在方法中寫大量的 if 進(jìn)行判斷植康;

雖然上述幾個(gè)問題并不影響 KVO 的使用旷太,不過這也足夠成為筆者盡量不使用 KVO 的理由了。

優(yōu)雅地使用 KVO

如何優(yōu)雅地解決上一節(jié)提出的幾個(gè)問題呢销睁?我們在這里只需要使用 Facebook 開源的 KVOController 框架就可以優(yōu)雅地解決這些問題了供璧。

如果想要實(shí)現(xiàn)同樣的業(yè)務(wù)需求,當(dāng)使用 KVOController 解決上述問題時(shí)冻记,只需要以下代碼就可以達(dá)到與上一節(jié)中完全相同的效果:

[self.KVOController observe:self.fizz
                    keyPath:@"number"
                    options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString    *,id> * _Nonnull change) {
                          NSLog(@"%@", change);
                      }];

我們可以在任意對象上獲得 KVOController 對象睡毒,然后調(diào)用它的實(shí)例方法 -observer:keyPath:options:block: 就可以檢測某個(gè)對象對應(yīng)的屬性了,該方法傳入的參數(shù)還是非常容易理解的冗栗,在 block 中也可以獲得所有與 KVO 有關(guān)的參數(shù)演顾。

使用 KVOController 進(jìn)行鍵值觀測可以說完美地解決了在使用原生 KVO 時(shí)遇到的各種問題供搀。

  1. 不需要手動(dòng)移除觀察者;
  2. 實(shí)現(xiàn) KVO 與事件發(fā)生處的代碼上下文相同钠至,不需要跨方法傳參數(shù)葛虐;
  3. 使用 block 來替代方法能夠減少使用的復(fù)雜度,提升使用 KVO 的體驗(yàn)棉钧;
  4. 每一個(gè) keyPath 會對應(yīng)一個(gè)屬性屿脐,不需要在 block 中使用 if 判斷 keyPath

KVOController 的實(shí)現(xiàn)

KVOController 其實(shí)是對 Cocoa 中 KVO 的封裝宪卿,它的實(shí)現(xiàn)其實(shí)也很簡單的诵,整個(gè)框架中只有兩個(gè)實(shí)現(xiàn)文件,先來簡要看一下 KVOController 如何為所有的 NSObject 對象都提供 -KVOController 屬性的吧佑钾。

分類和 KVOController 的初始化

KVOController 不止為 Cocoa Touch 中所有的對象提供了 -KVOController 屬性還提供了另一個(gè) KVOControllerNonRetaining 屬性西疤,實(shí)現(xiàn)方法就是分類和 ObjC Runtime。

@interface NSObject (FBKVOController)

@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

@end

從名字可以看出 KVOControllerNonRetaining 在使用時(shí)并不會持有被觀察的對象休溶,與它相比 KVOController 就會持有該對象了代赁。

對于 KVOControllerKVOControllerNonRetaining 屬性來說,其實(shí)現(xiàn)都非常簡單邮偎,對運(yùn)行時(shí)非常熟悉的讀者都應(yīng)該知道使用關(guān)聯(lián)對象就可以輕松實(shí)現(xiàn)這一需求管跺。

- (FBKVOController *)KVOController {
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey);
  if (nil == controller) {
    controller = [FBKVOController controllerWithObserver:self];
    self.KVOController = controller;
  }
  return controller;
}

- (void)setKVOController:(FBKVOController *)KVOController {
  objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (FBKVOController *)KVOControllerNonRetaining {
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
  if (nil == controller) {
    controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
    self.KVOControllerNonRetaining = controller;
  }
  return controller;
}

- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining {
  objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

兩者的 setter 方法都只是使用 objc_setAssociatedObject 按照鍵值簡單地存一下,而 getter 中不同的其實(shí)也就是對于 FBKVOController 的初始化了禾进。

easy

到這里這個(gè)整個(gè) FBKVOController 框架中的兩個(gè)實(shí)現(xiàn)文件中的一個(gè)就介紹完了豁跑,接下來要看一下其中的另一個(gè)文件中的類 KVOController

KVOController 的初始化

KVOController 是整個(gè)框架中提供 KVO 接口的類泻云,作為 KVO 的管理者艇拍,其必須持有當(dāng)前對象所有與 KVO 有關(guān)的信息,而在 KVOController 中宠纯,用于存儲這個(gè)信息的數(shù)據(jù)結(jié)構(gòu)就是 NSMapTable卸夕。

KVOControlle

為了使 KVOController 達(dá)到線程安全,它還必須持有一把 pthread_mutex_t 鎖婆瓜,用于在操作 _objectInfosMap 時(shí)使用快集。

再回到上一節(jié)提到的初始化問題,NSObject 的屬性 FBKVOControllerKVOControllerNonRetaining 的區(qū)別在于前者會持有觀察者廉白,使其引用計(jì)數(shù)加一个初。

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved {
  self = [super init];
  if (nil != self) {
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

在初始化方法中使用各自的方法對 KVOController 對象持有的所有實(shí)例變量進(jìn)行初始化,KVOControllerKVOControllerNonRetaining 的區(qū)別就體現(xiàn)在生成的 NSMapTable 實(shí)例時(shí)傳入的是 NSPointerFunctionsStrongMemory 還是 NSPointerFunctionsWeakMemory 選項(xiàng)猴蹂。

KVO 的過程

使用 KVOController 實(shí)現(xiàn)鍵值觀測時(shí)院溺,大都會調(diào)用實(shí)例方法 -observe:keyPath:options:block 來注冊成為某個(gè)對象的觀察者,監(jiān)控屬性的變化:

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block {
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  [self _observe:object info:info];
}

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

這個(gè)方法中就涉及到另外一個(gè)私有的數(shù)據(jù)結(jié)構(gòu) _FBKVOInfo磅轻,這個(gè)類中包含著所有與 KVO 有關(guān)的信息:

_FBKVOInfo

_FBKVOInfoKVOController 中充當(dāng)?shù)淖饔脙H僅是一個(gè)數(shù)據(jù)結(jié)構(gòu)珍逸,我們主要用它來存儲整個(gè) KVO 過程中所需要的全部信息逐虚,其內(nèi)部沒有任何值得一看的代碼,需要注意的是谆膳,_FBKVOInfo 覆寫了 -isEqual: 方法用于對象之間的判等以及方便 NSMapTable 的存儲叭爱。

如果再有點(diǎn)別的什么特別作用的就是,其中的 state 表示當(dāng)前的 KVO 狀態(tài)漱病,不過在本文中不會具體介紹涤伐。

typedef NS_ENUM(uint8_t, _FBKVOInfoState) {
  _FBKVOInfoStateInitial = 0,
  _FBKVOInfoStateObserving,
  _FBKVOInfoStateNotObserving,
};

observe 的過程

在使用 -observer:keyPath:options:block: 監(jiān)聽某一個(gè)對象屬性的變化時(shí),該過程的核心調(diào)用棧其實(shí)還是比較簡單:

KVOController-Observe-Stack

我們從棧底開始簡單分析一下整個(gè)封裝 KVO 的過程缨称,其中棧底的方法,也就是我們上面提到的 -observer:keyPath:options:block: 初始化了一個(gè)名為 _FBKVOInfo 的對象:

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block {
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  [self _observe:object info:info];
}

在創(chuàng)建了 _FBKVOInfo 之后執(zhí)行了另一個(gè)私有方法 -_observe:info:

- (void)_observe:(id)object info:(_FBKVOInfo *)info {
  pthread_mutex_lock(&_lock);
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    pthread_mutex_unlock(&_lock);
    return;
  }

  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }
  [infos addObject:info];
  pthread_mutex_unlock(&_lock);

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

這個(gè)私有方法通過自身持有的 _objectInfosMap 來判斷當(dāng)前對象祝迂、屬性以及各種上下文是否已經(jīng)注冊在表中存在了睦尽,在這個(gè) _objectInfosMap 中保存著對象以及與對象有關(guān)的 _FBKVOInfo 集合:

objectInfosMap

在操作了當(dāng)前 KVOController 持有的 _objectInfosMap 之后,才會執(zhí)行私有的 _FBKVOSharedController 類的實(shí)例方法 -observe:info:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info {
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

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

_FBKVOSharedController 才是最終調(diào)用 Cocoa 中的 -observe:forKeyPath:options:context: 方法開始對屬性的監(jiān)聽的地方型雳;同時(shí)当凡,在整個(gè)應(yīng)用運(yùn)行時(shí),只會存在一個(gè) _FBKVOSharedController 實(shí)例:

+ (instancetype)sharedController {
  static _FBKVOSharedController *_controller = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _controller = [[_FBKVOSharedController alloc] init];
  });
  return _controller;
}

這個(gè)唯一的 _FBKVOSharedController 實(shí)例會在 KVO 的回調(diào)方法中將事件分發(fā)給 KVO 的觀察者纠俭。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, id> *)change
                       context:(nullable void *)context {
    _FBKVOInfo *info;
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);

    FBKVOController *controller = info->_controller;
    id observer = controller.observer;

    if (info->_block) {
        NSDictionary<NSString *, id> *changeWithKeyPath = change;
        if (keyPath) {
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
        }
        info->_block(observer, object, changeWithKeyPath);
    } else if (info->_action) {
        [observer performSelector:info->_action withObject:change withObject:object];
    } else {
        [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
    }
}

在這個(gè) -observeValueForKeyPath:ofObject:change:context: 回調(diào)方法中沿量,_FBKVOSharedController 會根據(jù) KVO 的信息 _KVOInfo 選擇不同的方式分發(fā)事件,如果觀察者沒有傳入 block 或者選擇子冤荆,就會調(diào)用觀察者 KVO 回調(diào)方法朴则。

KVOSharedControlle

上圖就是在使用 KVOController 時(shí),如果一個(gè) KVO 事件觸發(fā)之后钓简,整個(gè)框架是如何對這個(gè)事件進(jìn)行處理以及回調(diào)的乌妒。

如何 removeObserver

在使用 KVOController 時(shí),我們并不需要手動(dòng)去處理 KVO 觀察者的移除外邓,因?yàn)樗械?KVO 事件都由私有的 _KVOSharedController 來處理撤蚊;

KVOController-Unobserve-Stack

當(dāng)每一個(gè) KVOController 對象被釋放時(shí),都會將它自己持有的所有 KVO 的觀察者交由 _KVOSharedController-unobserve:infos: 方法處理:

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos {
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

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

該方法會遍歷所有傳入的 _FBKVOInfo损话,從其中取出 keyPath 并將 _KVOSharedController 移除觀察者侦啸。

除了在 KVOController 析構(gòu)時(shí)會自動(dòng)移除觀察者,我們也可以通過它的實(shí)例方法 -unobserve:keyPath: 操作達(dá)到相同的效果丧枪;不過在調(diào)用這個(gè)方法時(shí)光涂,我們能夠得到一個(gè)不同的調(diào)用棧:

KVOController-Unobserve-Object-Stack

功能的實(shí)現(xiàn)過程其實(shí)都是類似的,都是通過 -removeObserver:forKeyPath:context: 方法移除觀察者:

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info {
  pthread_mutex_lock(&_mutex);
  [_infos removeObject:info];
  pthread_mutex_unlock(&_mutex);

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

不過由于這個(gè)方法的參數(shù)并不是一個(gè)數(shù)組豪诲,所以并不需要使用 for 循環(huán)顶捷,而是只需要將該 _FBKVOInfo 對應(yīng)的 KVO 事件移除就可以了。

總結(jié)

KVOController 對于 Cocoa 中 KVO 的封裝非常的簡潔和優(yōu)秀屎篱,我們只需要調(diào)用一個(gè)方法就可以完成一個(gè)對象的鍵值觀測服赎,同時(shí)不需要處理移除觀察者等問題葵蒂,能夠降低我們出錯(cuò)的可能性。

在筆者看來 KVOController 中唯一不是很優(yōu)雅的地方就是重虑,需要寫出 object.KVOController 才可以執(zhí)行 KVO践付,如果能將 KVOController 換成更短的形式可能看起來更舒服一些:

[self.kvo observer:keyPath:options:block:];

不過這并不是一個(gè)比較大的問題,同時(shí)也只是筆者自己的看法缺厉,況且不影響 KVOController 的使用永高,所以各位讀者也無須太過介意。

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/kvocontroller

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末提针,一起剝皮案震驚了整個(gè)濱河市命爬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辐脖,老刑警劉巖饲宛,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嗜价,居然都是意外死亡艇抠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門久锥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來家淤,“玉大人,你說我怎么就攤上這事瑟由⌒踔兀” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵错妖,是天一觀的道長绿鸣。 經(jīng)常有香客問我,道長暂氯,這世上最難降的妖魔是什么潮模? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮痴施,結(jié)果婚禮上擎厢,老公的妹妹穿的比我還像新娘。我一直安慰自己辣吃,他們只是感情好动遭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著神得,像睡著了一般厘惦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上哩簿,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天宵蕉,我揣著相機(jī)與錄音酝静,去河邊找鬼。 笑死羡玛,一個(gè)胖子當(dāng)著我的面吹牛别智,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稼稿,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼薄榛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了让歼?” 一聲冷哼從身側(cè)響起敞恋,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谋右,沒想到半個(gè)月后耳舅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡倚评,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了馏予。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片天梧。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖霞丧,靈堂內(nèi)的尸體忽然破棺而出呢岗,到底是詐尸還是另有隱情,我是刑警寧澤蛹尝,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布后豫,位于F島的核電站,受9級特大地震影響突那,放射性物質(zhì)發(fā)生泄漏挫酿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一愕难、第九天 我趴在偏房一處隱蔽的房頂上張望早龟。 院中可真熱鬧,春花似錦猫缭、人聲如沸葱弟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽芝加。三九已至,卻和暖如春射窒,著一層夾襖步出監(jiān)牢的瞬間藏杖,已是汗流浹背将塑。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留制市,地道東北人抬旺。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像祥楣,于是被迫代替她去往敵國和親开财。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

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