iOS Objective-C KVO 詳解

iOS Objective-C KVO 詳解

1. KVO

KVOKey-Value Observing是蘋果提供給開發者的一套鍵值觀察的API,KVO是一種機制,它允許將其他對象的指定屬性的更改通知給對象。KVO是建立在KVC的基礎上的,對于KVC的原理及應用可以查看我的上一篇文章。下面我們來詳細的介紹KVO

1.1 KVO 可以觀察什么屬性?

根據KVO官方文檔的定義,我們可以知道可觀察的屬性分為以下三種:

  • attributes: 簡單屬性,比如基本數據類型,字符串布爾值等,諸如NSNumber和其他一些不可變類型,比如NSColor也被認為是簡單屬性。
  • to-one relationships: 一對一關系,一個屬性的值取決于另一個值。比如一個人的全名姓氏名字組成,其中任一一個改變都會影響全名的改變;其實下載進度也跟這個類似,下載量和總量的任一改變都會改變下載進度。
  • to-many relationships: 一對多關系,在KVO中不支持多對多的關系鍵值路徑。一對多關系主要是集合對象屬性,通常就是NSArray或者NSSet等,但是涉及改變就是他們的可變類型。比如有一個部門,有一個員工數組,員工薪資屬性,部門有個總工資屬性,我想監聽總工資的變化,其實就是監聽員工數組的改變,再其改變后可以通過KVC的數組操作符計算總工資的變化,然后手動調用willchange去觸發總工資改變的監聽

對于一對一和一對多的關系可以查看蘋果官方文檔,進一步了解和示例代碼的查看。

1.2 KVO 的三個步驟

example.jpg

舉個例子,如上圖所示Person對象有個Account屬性,而Account對象又有balanceinterestRate兩個屬性。現在我們想實現一個功能:當余額和利率變化的時候需要通知到用戶,其實用戶可以通過輪詢的方式定期去查詢Account對象中的balanceinterestRate,但是這種方式不僅不及時而且效率低,消耗大,更好的方式是使用KVO,使Person對象像收到通知一樣能及時的知道余額和利率的變動。

另外要實現KVO的前提是被觀察對象時符合KVO機制的,一般來說,繼承于NSObject根類的對象及其屬性都自動符合KVO機制。當然我們也可以自己去實現,使其同樣符合KVO機制,這就是Manual Change Notification(手動變更通知),所以KVO包含Automatic Change Notification(自動變更通知)和Manual Change Notification(手動變更通知)兩種機制。

KVO合規性官方文檔

  • 首先是注冊觀察者
注冊觀察者.jpg

將觀察者實例Person與觀察實例Account注冊在一起。Person對每個觀察到的鍵路徑向Account發送一個addObserver:forKeyPath:options:context:消息,將自己命名為觀察者。這里observer(監聽者)、keyPath(被監聽者)、options(監聽策略)、context(上下文)。

  • 被觀察者觸發回調
被觀察者觸發回調.jpg

為了接收Account的變更通知,Person需要實現observeValueForKeyPath:ofObject:change:context:方法。Account將在任何改變的時候想Person發送該消息,Person可以根據通知做出相應的措施。

  • 移除觀察
移除觀察.jpg

最后,當不需要監聽的時候就可以通過removeObserver:forKeyPath:方法移除監聽,但是移除必須在監聽者對象銷毀前執行。

1.3 KVO三個方法解析

1.3.1 注冊觀察者

- (void)addObserver:(NSObject *)observer
                     forKeyPath:(NSString *)keyPath 
                     options:(NSKeyValueObservingOptions)options 
                     context:(nullable void *)context;
  • observer: 觀察者,一般都是self
  • keyPath: 被觀察者的屬性
  • options: NSKeyValueObservingOptions的組合,它指定觀察通知中會回調什么值
  • context: 上下文,這里是一個nullable void *類型的參數,我們通常會傳nil,其實應該傳NULL,官方文檔也說應該傳NULL。其實這里我們可以傳一個void *類型的指針,用來區分相同path的不同對象的觀察。傳值示例:static void *PersonNameContext = &PersonNameContext;

NSKeyValueObservingOptions:的四個枚舉值

  1. NSKeyValueObservingOptionNew: 表明通知中的更改字典應該提供新的屬性值,如果有的話。
  2. NSKeyValueObservingOptionOld: 表明通知中的更改字典應該包含舊的屬性值,如果有的話。
  3. NSKeyValueObservingOptionInitial: 在屬性發生變化后立即通知觀察者,這個過程甚至早于觀察者注冊是時候。如果在注冊的時候配置了 NSKeyValueObservingOptionNew,那么在通知的更改字典中也會包含 NSKeyValueChangeNewKey,但是不會包括 NSKeyValueChangeOldKey。(在初始通知中,觀察到的屬性值可能是舊的,但是對于觀察者來說是新的)其實簡單來說就是這個枚舉值會在屬性變化前先觸發一次 observeValueForKeyPath 回調。
  4. NSKeyValueObservingOptionPrior: 這個會先后連續出發兩次 observeValueForKeyPath 回調。同時在回調中的可變字典中會有一個布爾值的 key - notificationIsPrior 來標識屬性值是變化前還是變化后的。如果是變化后的回調,那么可變字典中就只有 new 的值了,如果同時制定了 NSKeyValueObservingOptionNew 的話。如果你需要啟動手動 KVO 的話,你可以指定這個枚舉值然后通過 willChange 實例方法來觀察屬性值。在出發 observeValueForKeyPath 回調后再去調用 willChange 可能就太晚了。

下面我們來驗證一下NSKeyValueObservingOptions幾個key會有什么樣的結果。

初始實現代碼:

static void *PersonNameContext = &PersonNameContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person  = [LGPerson new];
    self.person.name = @"nameA";
    
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
  
    if (context == PersonNameContext) {
        NSLog(@"person name change %@ - %@",self, change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name  = @"nameB";
}

NSKeyValueObservingOptionNew:

new.jpg

NSKeyValueObservingOptionOld:

old.jpg

NSKeyValueObservingOptionInitial:

Initial.jpg

NSKeyValueObservingOptionInitial會觸發兩次回調,第一次是在屬性改變前,第二次是在屬性改變后。但是并沒有返回任何的舊值和新值。其實第一次是在我們調用addObserver:forKeyPath:后就打印了的。這與name是否賦初始值沒有關系,有沒有初值都會打印。

Initial | New | Old:

Initial | New | Old.jpg

此時還是觸發了兩次回調,只不過第一次返回的新值其實就是舊值,就是我們初始化時的值,第二次返回即包含了新值,也包含了舊值。其實我們包含new在第二次就會返回新值,包含old就會返回舊值,如果不包含就不會返回。如果不包含new第一次就不會返回新值。

NSKeyValueObservingOptionPrior:

Prior.jpg

這是也是觸發了兩次回調,不過這兩次回調是在值改變后觸發的,并且第一次多返回了一個notificationIsPrior值。

Prior | New | Old:

Prior | New | Old.jpg

此時還是觸發了兩次回調,同樣在第一次回調中包含notificationIsPrior值。并且第一次回調中多了舊值,第二次回調中即包含舊值也包含新值。同樣我們包含new在第二次就會返回新值,包含old就會返回舊值,如果不包含就不會返回。如果不包含old第一次就不會返回舊值。

1.3.2 觀察者接收通知

- (void)observeValueForKeyPath:(nullable NSString *)keyPath 
                    ofObject:(nullable id)object 
                    change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change 
                    context:(nullable void *)context;

除了change其他參數跟上面注冊觀察時的相同。

change包含五個key,如下:

key value 描述
NSKeyValueChangeKindKey NSNumber類型 1:Setting,2:Insertion,3:Removal,4:Replacement
NSKeyValueChangeNewKey id 變化后的新值
NSKeyValueChangeOldKey id 變化后的舊值
NSKeyValueChangeIndexesKey NSIndexSet 插入、刪除或替換的對象的索引
NSKeyValueChangeNotificationIsPriorKey NSNumber boolValue Option為Prior時標識屬性值是變化前和還是變化后的

NSKeyValueChangeKindKey對應的枚舉:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

1.3.3 移除觀察

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

移除和注冊時一一對應的,有兩個方法一個是帶context參數的,另一個是不帶的。在觀察者生命周期結束前,一定要移除觀察,如果沒有移除,KVO機制會給一個不存在的對象發送變化回調消息導致野指針錯誤。另外也不能重復移除注冊,重復移除會導致crash,當然為了避免crash我們可以把移除放在@try里面去執行。

1.4 自動觀察與手動觀察

默認情況下,我們只需要按照上面的步驟就可以實現屬性的觀察,其實這是由系統完全控制的,屬于自動觀察。其實KVO還給我們提供了手動觀察的選項。

如果我們想要開啟手動觀察就要通過重寫類方法+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key,如果返回YES就是自動觀察,返回NO就是手動觀察,根據方法的我們還可以判斷key值對不同的key分別實現自動觀察手動觀察

// 自動開關
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

對于需要手動觀察key在改變前需要調用willChangeValueForKey方法,在改變后需要調用didChangeValueForKey方法,如果不調用,就不會觸發KVO的監聽。

示例代碼:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
  • 我們可以通過提前檢查是否已更改來最大程度的減少發送不必要的通知:

官方示例:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}
  • 如果單個操作導致更改多個鍵,則必須嵌套更改通知

官方示例:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
  • 對于有序的一對多關系,不僅必須指定已更改的鍵,還必須指定更改的類型和所涉及對象的索引。
    • 改變的類型的鍵值是一個 NSKeyValueChange 類型的枚舉值,有三個分別是:NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement
    • 受影響對象的索引作為NSIndexSet對象傳遞。

示例代碼:

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

1.5 Registering Dependent Keys(注冊從屬關系的鍵值)

在許多情況下,一個屬性的值取決于另一對象中一個或多個其他屬性的值。如果一個屬性的值發生更改,則派生屬性的值也應標記為更改。如何確保為這些從屬屬性發布鍵值觀察通知取決于關系的基數。 這個在上面已經有所提到,這里在通過舉例進行詳細的說明。

1.5.1 一對一關系

要自動觸發一對一關系的通知,您應該重寫 keyPathsForValuesAffectingValueForKey:或實現遵循其定義的用于注冊從屬鍵的模式的合適方法。

例如,一個人的全名取決于名字和姓氏。返回全名的方法可以編寫如下:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

fullName當firstName或lastName屬性更改時,必須通知觀察該屬性的應用程序,因為它們會影響屬性的值。

第一種方法是我們通過重寫keyPathsForValuesAffectingValueForKey:指定fullName的屬性取決于lastNamefirstName屬性。通常我們應該調用super并返回一個集合,該集合包括這樣做所導致的集合中的其他任何成員免受干擾。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

第二種方法是通過實現遵循命名約定的類方法keyPathsForValuesAffecting<Key>來實現相同的結果,其中<Key>是依賴值的屬性名稱(首字母大寫)。實現代碼如下:

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

對于分類我們只能以第二種方法進行實現因為我們不能再分類中覆蓋keyPathsForValuesAffectingValueForKey:的實現。

1.5.2 一對多關系

keyPathsForValuesAffectingValueForKey:方法不支持包含多對多關系的鍵路徑。那么對于這種關系的鍵值路徑我們該如何處理呢?

例如我們有個Department(部門),他又一個employees(員工數組)對象,部門跟員工有很多關系,但是Employee(員工)具有salary(薪資)屬性,這時我們希望部門有個totalSalary(總工資)屬性,那么這個屬性取決于員工數組中所有員工的薪資,我們也不能使用keyPathsForValuesAffectingTotalSalaryemployees.salary作為鍵返回。

此時我們可以使用鍵值觀察將父項(在此示例中為Department)注冊為所有子項(在此示例中為employees)的相關屬性的觀察者。您必須作為觀察者添加和刪除父對象,因為要在關系中添加或刪除子對象。在該observeValueForKeyPath:ofObject:change:context:方法中,您將響應更改來更新從屬值,如以下代碼片段所示:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

另外如果您使用的是Core Data,則可以將父項注冊到應用程序的通知中心,作為其托管對象上下文的觀察者。父母應以類似于觀察鍵值的方式響應孩子發布的相關變更通知。

2. KVO 底層原理探索

由于KVO的實現并沒有開源,我們首先看看官方文檔是怎么說的:

Automatic key-value observing is implemented using a technique called isa-swizzling. 【譯:】自動鍵值觀察使用的是一種叫做isa-swizzling的技術。

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.【譯:】isa指針,顧名思義,指向的是對象所屬的類,這個類維護了一個哈希表,這個哈希表實質上包含指向該類實現的方法的指針以及其他數據。

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.【譯:】在位對象的屬性注冊觀察者時,將修改觀察對象的isa指針,指向中間類而不是真實的類,因為isa的值不一定反映的是實例的實際的類。

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.【譯:】所以我們永遠不要依靠isa指針來確定類成員,所以我們應該使用class方法確定對象實例的類。

2.1 中間類(派生類)

根據官方文檔的內容我們可以知道,在KVO的底層實現中會生成一個中間類,此時我們實例對象的isa就指向了這個中間類,那么我們就來驗證一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    NSLog(@"注冊KVO前%@---%s", NSStringFromClass([self.person class]), object_getClassName(self.person));
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"注冊KVO后%@---%s", NSStringFromClass([self.person class]), object_getClassName(self.person));
}

打印結果如下:

打印結果.jpg
打印對象的isa.jpg

通過打印結果可以看出在注冊KVO觀察后通過Objective-C方法打印的的類名仍然是LGPerson,但是在注冊后通過Runtime API打印的確有不同了,所以說Objective-C方法對class方法進行了封裝,讓我們在開發過程中對中間類無感知,但是底層確實是實現了一個中間類就是NSKVONotifying_xxx。其實我們也可以通過打印對象的isa來驗證,至此我們就驗證了官方文檔所說的內容。

那么這個中間類跟我們的類有什么關系呢?我們不妨打印一下類和它的子類來看看。

打印類實現代碼:

NSLog(@"注冊KVO前");
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"注冊KVO后");
[self printClasses:[LGPerson class]];
- (void)printClasses:(Class)cls{
    
    /// 注冊類的總數
    int count = objc_getClassList(NULL, 0);
    /// 創建一個數組, 其中包含給定對象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    /// 獲取所有已注冊的類
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
打印結果.jpg

可以看到在LGPerson的子類中有這個中間類,所以說這個中間類是類的子類。

2.2 KVO 觀察

我們知道KVO是觀察屬性的變化,那么屬性的本質是成員變量+getter+settergetter是取值的,并不會修改值,值的變化發生在setter和給成員變量賦值兩種情況。那么我們分別測試一下這兩種情況哪一種會觸發KVO的觀察。

聲明代碼:

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end

驗證代碼和結果:

驗證代碼和結果.jpg

通過上圖我們可以看到直接給實例變量賦值并不會觸發KVO的監聽,但是直接給屬性賦值就觸發了KVO的監聽,其實給屬性賦值就是調用setter方法,所以說KVO底層是觀察的setter方法。

2.3 中間類都有哪些方法

我們分別打印原始類和中間類中的方法進行查看:

實現代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    
    NSLog(@"原始類中的方法");
    [self printClassAllMethod:[LGPerson class]];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"派生類中的方法");
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
}

printClassAllMethod 代碼:

- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

打印結果:

查看方法.jpg

我們可以看到中間類中有屬性的setter方法,class方法,dealloc方法以及_isKVOA方法。這里的setter方法是重寫了原始類的方法,其余的都是重寫的NSObject方法。

  • 對于重寫setter應該是在setter方法中觸發監聽回調,已經給原始類中屬性賦值
  • 對于重寫class,這里也就驗證了我們在上面打印class時為什么都是原始類的名稱。這樣是為了隱藏中間類的存在,讓開發者在使用過程中保持一致性。
  • 對于重寫dealloc應該是移除監聽時需要處理一些邏輯
  • 對于重寫_isKVOA方法應該是返回是否是KVO的值

2.3 isa 何時指回原始類

其實這很容易想到,當我們移除所有觀察后就意味著我們不需要觀察了,此時在指向中間類也就沒什么意義了。下面我們進行驗證。

驗證代碼:

- (void)dealloc{
    NSLog(@"移除觀察前%@",object_getClass(self.person));
    [self.person removeObserver:self forKeyPath:@"nickName"];
    NSLog(@"移除觀察后%@",object_getClass(self.person));
    [self printClasses:[LGPerson class]];
}

打印結果:

代碼打印.jpg
lldb驗證.jpg

我們通過代碼和lldb進行了驗證在移除觀察后isa即指回了原始的類。另外我們也驗證了指回后是否銷毀中間類,顯然中間類并沒有被銷毀。其實這也很正常,因為創建一個類還是非常耗費性能的,雖然移除了觀察,但是也不能保證不再重新開始觀察,既然創建了就讓它留著吧,如果下次繼續開始監聽就不用重新創建了,也就提高了性能。

3. 自定義KVO

至此我們就基本分析完畢了KVO,那么我們可以自己來實現以下。

擱置了!!!

4.總結

  1. KVO是蘋果提供給開發者的一套鍵值觀察的API
  2. KVO由注冊觀察者,監聽通知,移除觀察三個步驟組成
  3. 有自動觀察和手動觀察兩種模式
  4. 對于可變集合需要通過mutableXXXValueForKey的相關方法觸發更改
  5. 我們還可以注冊從屬關系的鍵值觀察,KVO支持一對一和一對多兩種
  6. KVO本質是isa-swizzling技術,通過生成中間類(派生類)來實現屬性的觀察
  7. 中間類會重寫屬性的setter方法以及重寫class方法,dealloc方法和_isKVOA方法
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372