‘iOS開發(fā)’幫你全解iOS通知機制(輕松過面)

簡述

本文主要是針對iOS通知機制的全面解析储玫,從接口到原理面面俱到岩睁。同時也解決了之前寫的文章阿里藕筋、字節(jié):一套高效的iOS面試題中關(guān)于通知的問題翅雏,相信看完此文再也不怕面試官問我任何通知相關(guān)問題了

由于蘋果沒有對相關(guān)源碼開放圈驼,所以以GNUStep源碼為基礎(chǔ)進行研究,GNUStep雖然不是蘋果官方的源碼枚荣,但很具有參考意義碗脊,根據(jù)實現(xiàn)原理來猜測和實踐,更重要的還可以學(xué)習(xí)觀察者模式的架構(gòu)設(shè)計

問題列表

先把之前的問題列出來,詳細(xì)讀完本文之后衙伶,你會找到答案

  1. 實現(xiàn)原理(結(jié)構(gòu)設(shè)計祈坠、通知如何存儲的、name&observer&SEL之間的關(guān)系等)
  2. 通知的發(fā)送時同步的矢劲,還是異步的
  3. NSNotificationCenter接受消息和發(fā)送消息是在一個線程里嗎赦拘?如何異步發(fā)送消息
  4. NSNotificationQueue是異步還是同步發(fā)送?在哪個線程響應(yīng)
  5. NSNotificationQueuerunloop的關(guān)系
  6. 如何保證通知接收的線程在主線程
  7. 頁面銷毀時不移除通知會崩潰嗎
  8. 多次添加同一個通知會是什么結(jié)果芬沉?多次移除通知呢
  9. 下面的方式能接收到通知嗎躺同?為什么
// 發(fā)送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
復(fù)制代碼

關(guān)鍵類結(jié)構(gòu)

NSNotification

用于描述通知的類,一個NSNotification對象就包含了一條通知的信息丸逸,所以當(dāng)創(chuàng)建一個通知時通常包含如下屬性:

@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */

- (NSString*) name; // 通知的name
- (id) object; // 攜帶的對象
- (NSDictionary*) userInfo; // 配置信息

@end
復(fù)制代碼

作為一個開發(fā)者蹋艺,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:761407670 進群密碼000黄刚,不管你是小白還是大牛歡迎入駐 捎谨,分享BAT,阿里面試題、面試經(jīng)驗憔维,討論技術(shù)涛救, 大家一起交流學(xué)習(xí)成長!

提供逆向安防业扒、Swift检吆、算法、架構(gòu)設(shè)計程储、多線程蹭沛,網(wǎng)絡(luò)進階,還有底層虱肄、音視頻致板、Flutter等資料

image.png

一般用于發(fā)送通知時使用交煞,常用api如下:

- (void)postNotification:(NSNotification *)notification;
復(fù)制代碼

NSNotificationCenter

這是個單例類咏窿,負(fù)責(zé)管理通知的創(chuàng)建和發(fā)送,屬于最核心的類了素征。而NSNotificationCenter類主要負(fù)責(zé)三件事

  1. 添加通知
  2. 發(fā)送通知
  3. 移除通知

核心API如下:

// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 發(fā)送通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 刪除通知
- (void)removeObserver:(id)observer;

復(fù)制代碼

NSNotificationQueue

功能介紹

通知隊列集嵌,用于異步發(fā)送消息,這個異步并不是開啟線程御毅,而是把通知存到雙向鏈表實現(xiàn)的隊列里面根欧,等待某個時機觸發(fā)時調(diào)用NSNotificationCenter的發(fā)送接口進行發(fā)送通知,這么看NSNotificationQueue最終還是調(diào)用NSNotificationCenter進行消息的分發(fā)

另外NSNotificationQueue是依賴runloop的端蛆,所以如果線程的runloop未開啟則無效凤粗,至于為什么依賴runloop下面會解釋

NSNotificationQueue主要做了兩件事:

  1. 添加通知到隊列
  2. 刪除通知

核心API如下:

// 把通知添加到隊列中,NSPostingStyle是個枚舉今豆,下面會介紹
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 刪除通知嫌拣,把滿足合并條件的通知從隊列中刪除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

復(fù)制代碼

隊列的合并策略和發(fā)送時機

把通知添加到隊列等待發(fā)送柔袁,同時提供了一些附加條件供開發(fā)者選擇,如:什么時候發(fā)送通知异逐、如何合并通知等捶索,系統(tǒng)給了如下定義

// 表示通知的發(fā)送時機
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空閑時發(fā)送通知
    NSPostASAP = 2, // 盡快發(fā)送,這種情況稍微復(fù)雜灰瞻,這種時機是穿插在每次事件完成期間來做的
    NSPostNow = 3 // 立刻發(fā)送或者合并通知完成之后發(fā)送
};
// 通知合并的策略腥例,有些時候同名通知只想存在一個,這時候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0, // 默認(rèn)不合并
    NSNotificationCoalescingOnName = 1, // 只要name相同酝润,就認(rèn)為是相同通知
    NSNotificationCoalescingOnSender = 2  // object相同
};
復(fù)制代碼

GSNotificationObserver

這個類是GNUStep源碼中定義的燎竖,它的作用是代理觀察者,主要用來實現(xiàn)接口:addObserverForName:object: queue: usingBlock:時用到要销,即要實現(xiàn)在指定隊列回調(diào)block底瓣,那么GSNotificationObserver對象保存了queueblock信息,并且作為觀察者注冊到通知中心蕉陋,等到接收通知時觸發(fā)了響應(yīng)方法捐凭,并在響應(yīng)方法中把block拋到指定queue中執(zhí)行,定義如下:

@implementation GSNotificationObserver
{
    NSOperationQueue *_queue; // 保存?zhèn)魅氲年犃?    GSNotificationBlock _block; // 保存?zhèn)魅氲腷lock
}
- (id) initWithQueue: (NSOperationQueue *)queue 
               block: (GSNotificationBlock)block
{
......初始化操作
}

- (void) dealloc
{
....
}
// 響應(yīng)接收通知的方法凳鬓,并在指定隊列中執(zhí)行block
- (void) didReceiveNotification: (NSNotification *)notif
{
    if (_queue != nil)
    {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];

        [_queue addOperation: op];
    }
    else
    {
        CALL_BLOCK(_block, notif);
    }
}

@end
復(fù)制代碼

存儲容器

上面介紹了一些類的功能茁肠,但是要想實現(xiàn)通知中心的邏輯必須設(shè)計一套合理的存儲結(jié)構(gòu),對于通知的存儲基本上圍繞下面幾個結(jié)構(gòu)體來做(大致了解下缩举,后面章節(jié)會用到)垦梆,后面會詳細(xì)介紹具體邏輯的

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation       *wildcard;  /* 鏈表結(jié)構(gòu)仅孩,保存既沒有name也沒有object的通知 */
  GSIMapTable       nameless;   /* 存儲沒有name但是有object的通知 */
  GSIMapTable       named;      /* 存儲帶有name的通知托猩,不管有沒有object  */
    ...
} NCTable;

// Observation 存儲觀察者和響應(yīng)結(jié)構(gòu)體,基本的存儲單元
typedef struct  Obs {
  id        observer;   /* 觀察者辽慕,接收通知的對象  */
  SEL       selector;   /* 響應(yīng)方法     */
  struct Obs    *next;      /* Next item in linked list.    */
  ...
} Observation;

復(fù)制代碼

注冊通知

正式開始“注冊通知”的深入研究京腥,注冊通知有幾個常用方法,但只需要研究典型的一兩個就夠了溅蛉,原理都是一樣的

目前只介紹NSNotificationCenter的注冊流程公浪,NSNotificationQueue的方式在下面章節(jié)單獨拎出來解釋

接口1

直接看源碼(精簡版便于理解)

/*
observer:觀察者,即通知的接收者
selector:接收到通知時的響應(yīng)方法
name: 通知name
object:攜帶對象
*/
- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name 
                object: (id)object {
  // 前置條件判斷
  ......

  // 創(chuàng)建一個observation對象船侧,持有觀察者和SEL欠气,下面進行的所有邏輯就是為了存儲它
  o = obsNew(TABLE, selector, observer);

/*======= case1: 如果name存在 =======*/
  if (name) {
    //-------- NAMED是個宏,表示名為named字典镜撩。以name為key预柒,從named表中獲取對應(yīng)的mapTable
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
      if (n == 0) { // 不存在,則創(chuàng)建 
          m = mapNew(TABLE); // 先取緩存,如果緩存沒有則新建一個map
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          ...
      }
      else { // 存在則把值取出來 賦值給m
          m = (GSIMapTable)n->value.ptr;
      }
    //-------- 以object為key宜鸯,從字典m中取出對應(yīng)的value人灼,其實value被MapNode的結(jié)構(gòu)包裝了一層,這里不追究細(xì)節(jié)
      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
      if (n == 0) {// 不存在顾翼,則創(chuàng)建 
          o->next = ENDOBS;
          GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
      }
      else {
          list = (Observation*)n->value.ptr;
          o->next = list->next;
          list->next = o;
      }
    }
/*======= case2:如果name為空投放,但object不為空 =======*/
  else if (object) {
    // 以object為key,從nameless字典中取出對應(yīng)的value适贸,value是個鏈表結(jié)構(gòu)
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      // 不存在則新建鏈表灸芳,并存到map中
      if (n == 0) { 
          o->next = ENDOBS;
          GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
      }
      else { // 存在 則把值接到鏈表的節(jié)點上
        ...
      }
    }
/*======= case3:name 和 object 都為空 則存儲到wildcard鏈表中 =======*/
  else {
      o->next = WILDCARD;
      WILDCARD = o;
  }
}
復(fù)制代碼

邏輯說明

從上面介紹的存儲容器中我們了解到NCTable結(jié)構(gòu)體中核心的三個變量以及功能:wildcardnamed拜姿、nameless烙样,在源碼中直接用宏定義表示了:WILDCARDNAMELESS蕊肥、NAMED谒获,下面邏輯會用到

建議如果看文字說明覺得復(fù)雜不好理解,就看看下節(jié)介紹的存儲關(guān)系圖

case1: 存在name(無論object是否存在)

  1. 注冊通知壁却,如果通知的name存在批狱,則以name為key從named字典中取出值n(這個n其實被MapNode包裝了一層,便于理解這里直接認(rèn)為沒有包裝)展东,這個n還是個字典赔硫,各種判空新建邏輯不討論
  2. 然后以object為key,從字典n中取出對應(yīng)的值盐肃,這個值就是Observation類型(后面簡稱obs)的鏈表爪膊,然后把剛開始創(chuàng)建的obs對象o存儲進去

數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

這里就回答了上述問題列表的問題1的一部分,現(xiàn)在梳理下存儲關(guān)系

如果注冊通知時傳入name砸王,那么會是一個雙層的存儲結(jié)構(gòu)

  1. 找到NCTable中的named表推盛,這個表存儲了還有name的通知
  2. name作為key,找到value谦铃,這個value依然是一個map
  3. map的結(jié)構(gòu)是以object作為key耘成,obs對象為value,這個obs對象的結(jié)構(gòu)上面已經(jīng)解釋荷辕,主要存儲了observer & SEL

case2: 只存在object

  1. object為key凿跳,從nameless字典中取出value,此value是個obs類型的鏈表
  2. 把創(chuàng)建的obs類型的對象o存儲到鏈表中

數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

只存在object時存儲只有一層疮方,那就是objectobs對象之間的映射

case3: 沒有name和object

這種情況直接把obs對象存放在了Observation *wildcard 鏈表結(jié)構(gòu)中

接口2

源碼

接口功能: 此接口實現(xiàn)的功能是在接收到通知時,在指定隊列queue執(zhí)行block

// 這個api使用頻率較低茧彤,怎么實現(xiàn)在指定隊列回調(diào)block的骡显,值得研究
- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    // 創(chuàng)建一個臨時觀察者
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];
    // 調(diào)用了接口1的注冊方法
    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}
復(fù)制代碼

邏輯說明

這個接口依賴于接口1,只是多了一層代理觀察者GSNotificationObserver,在關(guān)鍵類結(jié)構(gòu)中已經(jīng)介紹了它惫谤,設(shè)計思路值得學(xué)習(xí)

  1. 創(chuàng)建一個GSNotificationObserver類型的對象observer壁顶,并把queueblock保存下來
  2. 調(diào)用接口1進行通知的注冊
  3. 接收到通知時會響應(yīng)observerdidReceiveNotification:方法,然后在didReceiveNotification:中把block拋給指定的queue去執(zhí)行

小結(jié)

  1. 從上述介紹可以總結(jié)溜歪,存儲是以nameobject為維度的若专,即判定是不是同一個通知要從nameobject區(qū)分,如果他們都相同則認(rèn)為是同一個通知蝴猪,后面包括查找邏輯调衰、刪除邏輯都是以這兩個為維度的,問題列表中的第九題也迎刃而解了
  2. 理解數(shù)據(jù)結(jié)構(gòu)的設(shè)計是整個通知機制的核心自阱,其他功能只是在此基礎(chǔ)上擴展了一些邏輯
  3. 存儲過程并沒有做去重操作嚎莉,這也解釋了為什么同一個通知注冊多次則響應(yīng)多次

發(fā)送通知

源碼

發(fā)送通知的核心邏輯比較簡單,基本上就是查找和調(diào)用響應(yīng)方法沛豌,核心函數(shù)如下

// 發(fā)送通知
- (void) postNotificationName: (NSString*)name
               object: (id)object
             userInfo: (NSDictionary*)info
{
// 構(gòu)造一個GSNotification對象趋箩, GSNotification繼承了NSNotification
  GSNotification    *notification;
  notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
  notification->_name = [name copyWithZone: [self zone]];
  notification->_object = [object retain];
  notification->_info = [info retain];

  // 進行發(fā)送操作
  [self _postAndRelease: notification];
}
//發(fā)送通知的核心函數(shù),主要做了三件事:查找通知加派、發(fā)送叫确、釋放資源
- (void) _postAndRelease: (NSNotification*)notification {
    //step1: 從named、nameless芍锦、wildcard表中查找對應(yīng)的通知
    ...
    //step2:執(zhí)行發(fā)送启妹,即調(diào)用performSelector執(zhí)行響應(yīng)方法,從這里可以看出是同步的
    [o->observer performSelector: o->selector
                    withObject: notification];
    //step3: 釋放資源
    RELEASE(notification);
}

復(fù)制代碼

邏輯說明

其實上述代碼注釋說的很清晰了醉旦,主要做了三件事

  1. 通過name & object 查找到所有的obs對象(保存了observersel)饶米,放到數(shù)組中
  2. 通過performSelector:逐一調(diào)用sel,這是個同步操作
  3. 釋放notification對象

小結(jié)

從源碼邏輯可以看出發(fā)送過程的概述:從三個存儲容器中:named车胡、nameless檬输、wildcard去查找對應(yīng)的obs對象,然后通過performSelector:逐一調(diào)用響應(yīng)方法匈棘,這就完成了發(fā)送流程

核心點:

  1. 同步發(fā)送
  2. 遍歷所有列表丧慈,即注冊多次通知就會響應(yīng)多次

刪除通知

這里源碼太長而且基本上都是查找刪除邏輯,不一一列舉主卫,感興趣的去下載源碼看下吧
要注意的點:

  1. 查找時仍然以nameobject為維度的逃默,再加上observer做區(qū)分
  2. 因為查找時做了這個鏈表的遍歷,所以刪除時會把重復(fù)的通知全都刪除掉
// 刪除已經(jīng)注冊的通知
- (void) removeObserver: (id)observer
           name: (NSString*)name
                 object: (id)object {
  if (name == nil && object == nil && observer == nil)
      return;
      ...
}

- (void) removeObserver: (id)observer
{
  if (observer == nil)
    return;

  [self removeObserver: observer name: nil object: nil];
}
復(fù)制代碼

異步通知

上面介紹的NSNotificationCenter都是同步發(fā)送的簇搅,而這里介紹關(guān)于NSNotificationQueue的異步發(fā)送完域,從線程的角度看并不是真正的異步發(fā)送,或可稱為延時發(fā)送瘩将,它是利用了runloop的時機來觸發(fā)的

入隊

下面為精簡版的源碼吟税,看源碼的注釋凹耙,基本上能明白大致邏輯

  1. 根據(jù)coalesceMask參數(shù)判斷是否合并通知
  2. 接著根據(jù)postingStyle參數(shù),判斷通知發(fā)送的時機肠仪,如果不是立即發(fā)送則把通知加入到隊列中:_asapQueue肖抱、_idleQueue

核心點:

  1. 隊列是雙向鏈表實現(xiàn)
  2. 當(dāng)postingStyle值是立即發(fā)送時,調(diào)用的是NSNotificationCenter進行發(fā)送的异旧,所以NSNotificationQueue還是依賴NSNotificationCenter進行發(fā)送
/*
* 把要發(fā)送的通知添加到隊列意述,等待發(fā)送
* NSPostingStyle 和 coalesceMask在上面的類結(jié)構(gòu)中有介紹
* modes這個就和runloop有關(guān)了,指的是runloop的mode
*/ 
- (void) enqueueNotification: (NSNotification*)notification
        postingStyle: (NSPostingStyle)postingStyle
        coalesceMask: (NSUInteger)coalesceMask
            forModes: (NSArray*)modes
{
    ......
  // 判斷是否需要合并通知
  if (coalesceMask != NSNotificationNoCoalescing) {
      [self dequeueNotificationsMatching: notification
                coalesceMask: coalesceMask];
  }
  switch (postingStyle) {
      case NSPostNow: {
        ...
        // 如果是立馬發(fā)送吮蛹,則調(diào)用NSNotificationCenter進行發(fā)送
         [_center postNotification: notification];
         break;
      }
      case NSPostASAP:
        // 添加到_asapQueue隊列荤崇,等待發(fā)送
        add_to_queue(_asapQueue, notification, modes, _zone);
        break;

      case NSPostWhenIdle:
        // 添加到_idleQueue隊列,等待發(fā)送
        add_to_queue(_idleQueue, notification, modes, _zone);
        break;
    }
}
復(fù)制代碼

發(fā)送通知

這里截取了發(fā)送通知的核心代碼匹涮,這個發(fā)送通知邏輯如下:

  1. runloop觸發(fā)某個時機天试,調(diào)用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,這兩個方法最終都調(diào)用了notify()方法
  2. notify()所做的事情就是調(diào)用NSNotificationCenterpostNotification:進行發(fā)送通知
static void notify(NSNotificationCenter *center, 
                   NSNotificationQueueList *list,
                   NSString *mode, NSZone *zone)
{
    ......
    // 循環(huán)遍歷發(fā)送通知
    for (pos = 0; pos < len; pos++)
    {
      NSNotification    *n = (NSNotification*)ptr[pos];

      [center postNotification: n];
      RELEASE(n);
    }
    ......  
}
// 發(fā)送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_asapQueue,
        mode,
        item->queue->_zone);
}
// 發(fā)送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_idleQueue,
        mode,
        item->queue->_zone);
}

復(fù)制代碼

小結(jié)

對于NSNotificationQueue總結(jié)如下

  1. 依賴runloop然低,所以如果在其他子線程使用NSNotificationQueue喜每,需要開啟runloop
  2. 最終還是通過NSNotificationCenter進行發(fā)送通知,所以這個角度講它還是同步的
  3. 所謂異步雳攘,指的是非實時發(fā)送而是在合適的時機發(fā)送带兜,并沒有開啟異步線程

主線程響應(yīng)通知

異步線程發(fā)送通知則響應(yīng)函數(shù)也是在異步線程,如果執(zhí)行UI刷新相關(guān)的話就會出問題吨灭,那么如何保證在主線程響應(yīng)通知呢刚照?

其實也是比較常見的問題了,基本上解決方式如下幾種:

  1. 使用addObserverForName: object: queue: usingBlock方法注冊通知喧兄,指定在mainqueue上響應(yīng)block
  2. 在主線程注冊一個machPort无畔,它是用來做線程通信的,當(dāng)在異步線程收到通知吠冤,然后給machPort發(fā)送消息浑彰,這樣肯定是在主線程處理的,具體用法去網(wǎng)上資料很多拯辙,蘋果官網(wǎng)也有

總結(jié)

本文寫的內(nèi)容比較多郭变,以GNUStep源碼為基礎(chǔ)進行研究,全面闡述了通知的存儲涯保、發(fā)送碗旅、異步發(fā)送等原理曙搬,對研究學(xué)習(xí)有很大幫助

最后推薦個我的高級iOS交流群:761407670 進群密碼000,有一個共同的圈子很重要柿究,結(jié)識人脈僚纷!里面都是iOS開發(fā)梢睛,全棧發(fā)展逛裤,歡迎入駐,共同進步狈蚤!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末困肩,一起剝皮案震驚了整個濱河市划纽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锌畸,老刑警劉巖勇劣,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異潭枣,居然都是意外死亡比默,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門盆犁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來命咐,“玉大人,你說我怎么就攤上這事谐岁〈椎欤” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵伊佃,是天一觀的道長窜司。 經(jīng)常有香客問我,道長航揉,這世上最難降的妖魔是什么塞祈? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮帅涂,結(jié)果婚禮上议薪,老公的妹妹穿的比我還像新娘。我一直安慰自己媳友,他們只是感情好斯议,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著庆锦,像睡著了一般捅位。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搂抒,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天艇搀,我揣著相機與錄音,去河邊找鬼求晶。 笑死焰雕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的芳杏。 我是一名探鬼主播矩屁,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼辟宗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吝秕?” 一聲冷哼從身側(cè)響起泊脐,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎烁峭,沒想到半個月后容客,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡约郁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年缩挑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鬓梅。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡供置,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绽快,到底是詐尸還是另有隱情芥丧,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布谎僻,位于F島的核電站娄柳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏艘绍。R本人自食惡果不足惜赤拒,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诱鞠。 院中可真熱鬧挎挖,春花似錦、人聲如沸航夺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阳掐。三九已至始衅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缭保,已是汗流浹背汛闸。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留艺骂,地道東北人诸老。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像钳恕,于是被迫代替她去往敵國和親别伏。 傳聞我的和親對象是個殘疾皇子蹄衷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344