iOS - 關(guān)于 KVO 的一些總結(jié)

KVO 大綱

目錄

  • 1. 什么是 KVO
  • 2. KVO 的基本使用
    ?2.1 注冊(cè)方法
    ?2.2 監(jiān)聽(tīng)方法
    ?2.3 移除方法
    ?2.4 使用示例
    ?2.5 實(shí)際應(yīng)用
    ?2.6 KVO 觸發(fā)監(jiān)聽(tīng)方法的方式
    ??2.6.1 自動(dòng)觸發(fā)
    ??2.6.2 手動(dòng)觸發(fā)
  • 3. KVO 的進(jìn)階使用
    ?3.1 observationInfo 屬性
    ?3.2 context 的使用
    ?3.3 KVO 監(jiān)聽(tīng)集合對(duì)象
    ?3.4 KVO 的自動(dòng)觸發(fā)控制
    ?3.5 KVO 的手動(dòng)觸發(fā)
    ?3.6 KVO 新舊值相等時(shí)不觸發(fā)
    ?3.7 KVO 手動(dòng)觀察集合屬性
    ?3.8 KVO 的依賴觀察
    ??3.8.1 一對(duì)一關(guān)系
    ??3.8.2 一對(duì)多關(guān)系
  • 4. KVO 的使用注意
    ?4.1 移除觀察者的注意點(diǎn)
    ?4.2 防止多次注冊(cè)和移除相同的 KVO
    ?4.3 其它注意點(diǎn)
  • 5. KVO 的實(shí)現(xiàn)原理
    ?5.1 isa-swizzling
    ?5.2 KVO 動(dòng)態(tài)生成的子類(lèi)都有哪些方法
  • 6. FBKVOController
    ?6.1 系統(tǒng) KVO 的缺點(diǎn)
    ?6.2 FBKVOController 的介紹
    ?6.3 FBKVOController 的優(yōu)點(diǎn)
    ?6.4 FBKVOController 的使用
    ?6.5 FBKVOController 的解析
  • 參考

1. 什么是 KVO

  • KVO的全稱是Key-Value Observing胰苏,俗稱“鍵值觀察/監(jiān)聽(tīng)”蝶缀,是蘋(píng)果提供的一套事件通知機(jī)制袁波,允許一個(gè)對(duì)象觀察/監(jiān)聽(tīng)另一個(gè)對(duì)象指定屬性值的改變。當(dāng)被觀察對(duì)象屬性值發(fā)生改變時(shí)球及,會(huì)觸發(fā)KVO的監(jiān)聽(tīng)方法來(lái)通知觀察者。KVO是在MVC應(yīng)用程序中的各層之間進(jìn)行通信的一種特別有用的技術(shù)台舱。
  • KVONSNotification都是iOS中觀察者模式的一種實(shí)現(xiàn)尚骄。
  • KVO可以監(jiān)聽(tīng)單個(gè)屬性的變化,也可以監(jiān)聽(tīng)集合對(duì)象的變化毙死。監(jiān)聽(tīng)集合對(duì)象變化時(shí)燎潮,需要通過(guò)KVCmutableArrayValueForKey:等可變代理方法獲得集合代理對(duì)象,并使用代理對(duì)象進(jìn)行操作扼倘,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)确封,會(huì)觸發(fā)KVO的監(jiān)聽(tīng)方法。集合對(duì)象包含NSArrayNSSet再菊。
  • KVOKVC有著密切的關(guān)系爪喘,如果想要深入了解KVO,建議先學(xué)習(xí)KVC袄简。
    傳送門(mén):iOS - 關(guān)于 KVC 的一些總結(jié)

2. KVO 的基本使用

KVO使用三部曲:添加/注冊(cè)KVO監(jiān)聽(tīng)、實(shí)現(xiàn)監(jiān)聽(tīng)方法以接收屬性改變通知泛啸、 移除KVO監(jiān)聽(tīng)绿语。

  1. 調(diào)用方法addObserver:forKeyPath:options:context:給被觀察對(duì)象添加觀察者;
  2. 在觀察者類(lèi)中實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法以接收屬性改變的通知消息;
  3. 當(dāng)觀察者不需要再監(jiān)聽(tīng)時(shí)吕粹,調(diào)用removeObserver:forKeyPath:方法將觀察者移除种柑。需要注意的是,至少需要在觀察者銷(xiāo)毀之前匹耕,調(diào)用此方法聚请,否則可能會(huì)導(dǎo)致Crash

2.1 注冊(cè)方法

/*
 ** target:  被觀察對(duì)象
 ** observer:觀察者對(duì)象
 ** keyPath: 被觀察對(duì)象的屬性的關(guān)鍵路徑稳其,不能為nil
 ** options: 觀察的配置選項(xiàng)驶赏,包括觀察的內(nèi)容(枚舉類(lèi)型):
           NSKeyValueObservingOptionNew:觀察新值
           NSKeyValueObservingOptionOld:觀察舊值
           NSKeyValueObservingOptionInitial:觀察初始值,如果想在注冊(cè)觀察者后既鞠,立即接收一次回調(diào)煤傍,可以加入該枚舉值
           NSKeyValueObservingOptionPrior:分別在值改變前后觸發(fā)方法(即一次修改有兩次觸發(fā))
 ** context: 可以傳入任意數(shù)據(jù)(任意類(lèi)型的對(duì)象或者C指針),在監(jiān)聽(tīng)方法中可以接收到這個(gè)數(shù)據(jù)嘱蛋,是KVO中的一種傳值方式
             如果傳的是一個(gè)對(duì)象蚯姆,必須在移除觀察之前持有它的強(qiáng)引用,否則在監(jiān)聽(tīng)方法中訪問(wèn)context就可能導(dǎo)致Crash
 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
 options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

2.2 監(jiān)聽(tīng)方法

如果對(duì)象被注冊(cè)成為觀察者洒敏,則該對(duì)象必須能響應(yīng)以下監(jiān)聽(tīng)方法龄恋,即該對(duì)象所屬類(lèi)中必須實(shí)現(xiàn)監(jiān)聽(tīng)方法。當(dāng)被觀察對(duì)象屬性發(fā)生改變時(shí)就會(huì)調(diào)用監(jiān)聽(tīng)方法凶伙。如果沒(méi)有實(shí)現(xiàn)就會(huì)導(dǎo)致Crash郭毕。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/*
 ** keyPath:被觀察對(duì)象的屬性的關(guān)鍵路徑
 ** object: 被觀察對(duì)象
 ** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,屬性值更改的詳細(xì)信息镊靴,根據(jù)注冊(cè)方法中options參數(shù)傳入的枚舉來(lái)返回
             key為 NSKeyValueChangeKey 枚舉類(lèi)型
             {
                 1.NSKeyValueChangeKindKey:存儲(chǔ)本次改變的信息(change字典中默認(rèn)包含這個(gè)key)
                 {
                     對(duì)應(yīng)枚舉類(lèi)型 NSKeyValueChange
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting     = 1,
                         NSKeyValueChangeInsertion   = 2,
                         NSKeyValueChangeRemoval     = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
                     如果是對(duì)被觀察對(duì)象屬性(包括集合)進(jìn)行賦值操作铣卡,kind 字段的值為 NSKeyValueChangeSetting
                     如果被觀察的是集合對(duì)象,且進(jìn)行的是(插入偏竟、刪除煮落、替換)操作,則會(huì)根據(jù)集合對(duì)象的操作方式來(lái)設(shè)置 kind 字段的值
                         插入:NSKeyValueChangeInsertion
                         刪除:NSKeyValueChangeRemoval
                         替換:NSKeyValueChangeReplacement
                 }    
                 2.NSKeyValueChangeNewKey:存儲(chǔ)新值(如果options中傳入NSKeyValueObservingOptionNew踊谋,change字典中就會(huì)包含這個(gè)key)
                 3.NSKeyValueChangeOldKey:存儲(chǔ)舊值(如果options中傳入NSKeyValueObservingOptionOld蝉仇,change字典中就會(huì)包含這個(gè)key)
                 4.NSKeyValueChangeIndexesKey:如果被觀察的是集合對(duì)象,且進(jìn)行的是(插入殖蚕、刪除轿衔、替換)操作,則change字典中就會(huì)包含這個(gè)key
                     這個(gè)key的value是一個(gè)NSIndexSet對(duì)象睦疫,包含更改關(guān)系中的索引
                 5.NSKeyValueChangeNotificationIsPriorKey:如果options中傳入NSKeyValueObservingOptionPrior害驹,則在改變前通知的change字典中會(huì)包含這個(gè)key。
                     這個(gè)key對(duì)應(yīng)的value是NSNumber包裝的YES蛤育,我們可以這樣來(lái)判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
             }
 ** context:注冊(cè)方法中傳入的context
 */
}

2.3 移除方法

在調(diào)用注冊(cè)方法后宛官,KVO并不會(huì)對(duì)觀察者進(jìn)行強(qiáng)引用葫松,所以需要注意觀察者的生命周期。至少需要在觀察者銷(xiāo)毀之前底洗,調(diào)用以下方法移除觀察者腋么,否則如果在觀察者被釋放后,再次觸發(fā)KVO監(jiān)聽(tīng)方法就會(huì)導(dǎo)致Crash亥揖。

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

2.4 使用示例

以下使用KVOperson對(duì)象添加觀察者為當(dāng)前viewController珊擂,監(jiān)聽(tīng)person對(duì)象的name屬性值的改變。當(dāng)name值改變時(shí)费变,觸發(fā)KVO的監(jiān)聽(tīng)方法摧扇。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"keyPath:%@",keyPath);
    NSLog(@"object:%@",object);
    NSLog(@"change:%@",change);
    NSLog(@"context:%@",context);
}

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

keyPath:name
object:<HTPerson: 0x600003ae4340>
change:{ kind = 1; new = "\U70b9\U51fb"; old = "<null>"; }
context:(null)

2.5 實(shí)際應(yīng)用

KVO主要用來(lái)做鍵值觀察操作,想要一個(gè)值發(fā)生改變后通知另一個(gè)對(duì)象胡控,則用KVO實(shí)現(xiàn)最為合適扳剿。斯坦福大學(xué)的iOS教程中有一個(gè)很經(jīng)典的案例,通過(guò)KVOModelController之間進(jìn)行通信昼激。如圖所示:

斯坦福大學(xué) KVO示例

2.6 KVO觸發(fā)監(jiān)聽(tīng)方法的方式

KVO觸發(fā)分為自動(dòng)觸發(fā)和手動(dòng)觸發(fā)兩種方式庇绽。

2.6.1 自動(dòng)觸發(fā)

① 如果是監(jiān)聽(tīng)對(duì)象特定屬性值的改變,通過(guò)以下方式改變屬性值會(huì)觸發(fā)KVO

  • 使用點(diǎn)語(yǔ)法
  • 使用setter方法
  • 使用KVCsetValue:forKey:方法
  • 使用KVCsetValue:forKeyPath:方法

② 如果是監(jiān)聽(tīng)集合對(duì)象的改變橙困,需要通過(guò)KVCmutableArrayValueForKey:等方法獲得代理對(duì)象瞧掺,并使用代理對(duì)象進(jìn)行操作,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)凡傅,會(huì)觸發(fā)KVO辟狈。集合對(duì)象包含NSArrayNSSet

2.6.2 手動(dòng)觸發(fā)

① 普通對(duì)象屬性或是成員變量使用:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

NSArray對(duì)象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

NSSet對(duì)象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

3. KVO 的進(jìn)階使用

3.1 observationInfo 屬性

  • observationInfo屬性是NSKeyValueObserving.h文件中系統(tǒng)通過(guò)分類(lèi)給NSObject添加的屬性夏跷,所以所有繼承于NSObject的對(duì)象都含有該屬性哼转;
  • 可以通過(guò)observationInfo屬性查看被觀察對(duì)象的全部觀察信息,包括observer槽华、keyPath壹蔓、optionscontext等猫态。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

3.2 context 的使用

注冊(cè)方法addObserver:forKeyPath:options:context:中的context可以傳入任意數(shù)據(jù)佣蓉,并且可以在監(jiān)聽(tīng)方法中接收到這個(gè)數(shù)據(jù)。

  • context作用:標(biāo)簽-區(qū)分亲雪,可以更精確的確定被觀察對(duì)象屬性勇凭,用于繼承、 多監(jiān)聽(tīng)义辕;也可以用來(lái)傳值虾标。
    ??KVO只有一個(gè)監(jiān)聽(tīng)回調(diào)方法observeValueForKeyPath:ofObject:change:context:,我們通常情況下可以在注冊(cè)方法中指定contextNULL灌砖,并在監(jiān)聽(tīng)方法中通過(guò)objectkeyPath來(lái)判斷觸發(fā)KVO的來(lái)源璧函。
    ??但是如果存在繼承的情況贞让,比如現(xiàn)在有 Person 類(lèi)和它的兩個(gè)子類(lèi) Teacher 類(lèi)和 Student 類(lèi),person柳譬、teacher 和 student 實(shí)例對(duì)象都對(duì) account 對(duì)象的 balance 屬性進(jìn)行觀察。問(wèn)題:
    ??① 當(dāng) balance 發(fā)生改變時(shí)续镇,應(yīng)該由誰(shuí)來(lái)處理呢美澳?
    ??② 如果都由 person 來(lái)處理,那么在 Person 類(lèi)的監(jiān)聽(tīng)方法中又該怎么判斷是自己的事務(wù)還是子類(lèi)對(duì)象的事務(wù)呢摸航?
    ??這時(shí)候通過(guò)使用context就可以很好地解決這個(gè)問(wèn)題制跟,在注冊(cè)方法中為context設(shè)置一個(gè)獨(dú)一無(wú)二的值,然后在監(jiān)聽(tīng)方法中對(duì)context值進(jìn)行檢驗(yàn)即可酱虎。

  • 蘋(píng)果的推薦用法:用context來(lái)精確的確定被觀察對(duì)象屬性雨膨,使用唯一命名的靜態(tài)變量的地址作為context的值《链可以為整個(gè)類(lèi)設(shè)置一個(gè)context聊记,然后在監(jiān)聽(tīng)方法中通過(guò)objectkeyPath來(lái)確定被觀察屬性,這樣存在繼承的情況就可以通過(guò)context來(lái)判斷恢暖;也可以為每個(gè)被觀察對(duì)象屬性設(shè)置不同的context排监,這樣使用context就可以精確的確定被觀察對(duì)象屬性。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}
  • context優(yōu)點(diǎn):嵌套少杰捂、性能高舆床、更安全、擴(kuò)展性強(qiáng)嫁佳。
  • context注意點(diǎn):
    ① 如果傳的是一個(gè)對(duì)象挨队,必須在移除觀察之前持有它的強(qiáng)引用,否則在監(jiān)聽(tīng)方法中訪問(wèn)context就可能導(dǎo)致Crash蒿往;
    ② 空傳NULL而不應(yīng)該傳nil盛垦。

3.3 KVO 監(jiān)聽(tīng)集合對(duì)象

KVO可以監(jiān)聽(tīng)單個(gè)屬性的變化,也可以監(jiān)聽(tīng)集合對(duì)象的變化熄浓。監(jiān)聽(tīng)集合對(duì)象變化時(shí)情臭,需要通過(guò)KVCmutableArrayValueForKey:等方法獲得代理對(duì)象,并使用代理對(duì)象進(jìn)行操作赌蔑,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)俯在,會(huì)觸發(fā)KVO的監(jiān)聽(tīng)方法。集合對(duì)象包含NSArrayNSSet娃惯。
(注意:如果直接對(duì)集合對(duì)象進(jìn)行操作改變跷乐,不會(huì)觸發(fā)KVO。)

示例代碼及輸出如下:

觀察者 viewController 對(duì)被觀察對(duì)象 person 的 mArray 屬性進(jìn)行監(jiān)聽(tīng)趾浅。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    self.person.mArray = [NSMutableArray arrayWithCapacity:5];
    [self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self.person.mArray addObject:@"2"]; //如果直接對(duì)數(shù)組進(jìn)行操作愕提,不會(huì)觸發(fā)KVO
    NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
    [array addObject:@"1"];
    [array replaceObjectAtIndex:0 withObject:@"2"];
    [array removeObjectAtIndex:0];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    /*  change 字典的值為:
        {
            indexes:對(duì)應(yīng)的值為數(shù)組操作的詳細(xì)信息馒稍,包括索引等
            kind:   對(duì)應(yīng)的值為數(shù)組操作的方式:
                     2:代表插入操作
                     3:代表刪除操作
                     4:代表替換操作
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting = 1,
                         NSKeyValueChangeInsertion = 2,
                         NSKeyValueChangeRemoval = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
            new/old:如果是插入操作,則字典中只會(huì)有new字段浅侨,對(duì)應(yīng)的值為插入的元素纽谒,前提條件是options中傳入了(NSKeyValueObservingOptionNew)
                     如果是刪除操作,則字典中只會(huì)有old字段如输,對(duì)應(yīng)的值為刪除的元素鼓黔,前提條件是options中傳入了(NSKeyValueObservingOptionOld)
                     如果是替換操作,則字典中new和old字段都可以存在不见,對(duì)應(yīng)的值為替換后的元素和替換前的元素澳化,前提條件是options中傳入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)

        如: indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
            kind = 2; 
            new =     (
                1
            );
        }
     */  
    NSLog(@"%@",change);  
}

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

{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = (2); }

3.4 KVO 的自動(dòng)觸發(fā)控制

??可以在被觀察對(duì)象的類(lèi)中重寫(xiě)+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法來(lái)控制KVO的自動(dòng)觸發(fā)。
??如果我們只允許外界觀察 person 的 name 屬性稳吮,可以在 Person 類(lèi)如下操作缎谷。這樣外界就只能觀察 name 屬性,即使外界注冊(cè)了對(duì) person 對(duì)象其它屬性的監(jiān)聽(tīng)灶似,那么在屬性發(fā)生改變時(shí)也不會(huì)觸發(fā)KVO列林。

// 返回值代表允不允許觸發(fā) KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"name"]) {
        automatic = YES;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

??也可以實(shí)現(xiàn)遵循命名規(guī)則為+ (BOOL)automaticallyNotifiesObserversOf<Key>的方法來(lái)單一控制屬性的KVO自動(dòng)觸發(fā),<Key>為屬性名(首字母大寫(xiě))酪惭。

+ (BOOL)automaticallyNotifiesObserversOfName
{
    return NO;
}

注意:

  • 第一個(gè)方法的優(yōu)先級(jí)高于第二個(gè)方法席纽。如果實(shí)現(xiàn)了automaticallyNotifiesObserversForKey:方法,并對(duì)<Key>做了處理撞蚕,則系統(tǒng)就不會(huì)再調(diào)用該<Key>automaticallyNotifiesObserversOf<Key>方法润梯。
  • options指定的NSKeyValueObservingOptionInitial觸發(fā)的KVO通知,是無(wú)法被automaticallyNotifiesObserversForKey:阻止的甥厦。

3.5 KVO 的手動(dòng)觸發(fā)

使用場(chǎng)景:

  • 使用KVO監(jiān)聽(tīng)成員變量值的改變纺铭;
  • 在某些需要控制監(jiān)聽(tīng)過(guò)程的場(chǎng)景下。比如:為了盡量減少不必要的觸發(fā)通知操作刀疙,或者當(dāng)多個(gè)更改同時(shí)具備的時(shí)候才調(diào)用屬性改變的監(jiān)聽(tīng)方法舶赔。

??由于KVO的本質(zhì),重寫(xiě)setter方法來(lái)達(dá)到可以通知所有觀察者對(duì)象的目的谦秧,所以只有通過(guò)setter方法或KVC方法去修改屬性變量值的時(shí)候竟纳,才會(huì)觸發(fā)KVO,直接修改成員變量不會(huì)觸發(fā)KVO疚鲤。
??當(dāng)我們要使用KVO監(jiān)聽(tīng)成員變量值改變的時(shí)候锥累,可以通過(guò)在為成員變量賦值的前后手動(dòng)調(diào)用willChangeValueForKey:didChangeValueForKey:兩個(gè)方法來(lái)手動(dòng)觸發(fā)KVO,如:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person willChangeValueForKey:@"age"];
    self.person->_age = 18;
    [self.person didChangeValueForKey:@"age"];
}

??NSKeyValueObservingOptionPrior(分別在值改變前后觸發(fā)方法集歇,即一次修改有兩次觸發(fā))的兩次觸發(fā)分別在willChangeValueForKey:didChangeValueForKey:的時(shí)候進(jìn)行的桶略。
??如果注冊(cè)方法中options傳入NSKeyValueObservingOptionPrior,那么可以通過(guò)只調(diào)用willChangeValueForKey:來(lái)觸發(fā)改變前的那次KVO,可以用于在屬性值即將更改前做一些操作际歼。

3.6 KVO 新舊值相等時(shí)不觸發(fā)

??有時(shí)候我們可能會(huì)有這樣的需求惶翻,KVO監(jiān)聽(tīng)的屬性值修改前后相等的時(shí)候,不觸發(fā)KVO的監(jiān)聽(tīng)方法鹅心,可以結(jié)合KVO的自動(dòng)觸發(fā)控制和手動(dòng)觸發(fā)來(lái)實(shí)現(xiàn)吕粗。
??例如:對(duì) person 對(duì)象的 name 屬性注冊(cè)了KVO監(jiān)聽(tīng),我們希望在對(duì) name 屬性賦值時(shí)做一個(gè)判斷旭愧,如果新值和舊值相等溯泣,則不觸發(fā)KVO,可以在 Person 類(lèi)中如下這樣實(shí)現(xiàn)榕茧,將 name 屬性值改變的KVO觸發(fā)方式由自動(dòng)觸發(fā)改為手動(dòng)觸發(fā)。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = YES;
    if ([key isEqualToString:@"name"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)setName:(NSString *)name
{
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    } 
}

3.7 KVO 手動(dòng)觀察集合屬性

有些情況下我們想手動(dòng)觀察集合屬性客给,下面以觀察數(shù)組為例用押。
關(guān)鍵方法:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

需要注意的是,根據(jù)KVCNSMutableArray 搜索模式
傳送門(mén):iOS - 關(guān)于 KVC 的一些總結(jié)

  • 至少要實(shí)現(xiàn)一個(gè)插入和一個(gè)刪除方法靶剑,否則不會(huì)觸發(fā)KVO蜻拨。如
    插入方法:insertObject:in<Key>AtIndex:insert<Key>:atIndexes:
    刪除方法:removeObjectFrom<Key>AtIndex:remove<Key>AtIndexes:
  • 可以不實(shí)現(xiàn)替換方法,但是如果不實(shí)現(xiàn)替換方法桩引,執(zhí)行替換操作時(shí)缎讼,KVO會(huì)把它當(dāng)成先刪除后添加,即會(huì)觸發(fā)兩次KVO坑匠。第一次觸發(fā)的KVOchange字典的old鍵的值為替換前的元素血崭,第二次觸發(fā)的KVOchange字典的new鍵的值為替換后的元素,前提條件是注冊(cè)方法中的options傳入對(duì)應(yīng)的枚舉值厘灼。
  • 如果實(shí)現(xiàn)替換方法夹纫,則執(zhí)行替換操作只會(huì)觸發(fā)一次KVO,并且change字典會(huì)同時(shí)包含newold设凹,前提條件是注冊(cè)方法中的options傳入對(duì)應(yīng)的枚舉值舰讹。
    替換方法:replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:
  • 建議實(shí)現(xiàn)替換方法以提高性能。

示例代碼如下:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"mArray"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray insertObjects:array atIndexes:indexes];

    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray removeObjectsAtIndexes:indexes];

    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray replaceObjectsAtIndexes:indexes withObjects:array];

    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}

3.8 KVO 的依賴觀察

3.8.1 一對(duì)一關(guān)系

??有些情況下闪朱,一個(gè)屬性的改變依賴于別的一個(gè)或多個(gè)屬性的改變月匣,也就是說(shuō)當(dāng)別的屬性改了,這個(gè)屬性也會(huì)跟著改變奋姿。
??比如我們想要對(duì) Download 類(lèi)中的 downloadProgress 屬性進(jìn)行KVO監(jiān)聽(tīng)锄开,該屬性的改變依賴于 writtenData 和 totalData 屬性的改變。觀察者監(jiān)聽(tīng)了 downloadProgress 称诗,當(dāng) writtenData 和 totalData 屬性值改變時(shí)院刁,觀察者也應(yīng)該被通知。以下有兩種方法可以解決這個(gè)問(wèn)題粪狼。

  1. 重寫(xiě)以下方法來(lái)指明 downloadProgress 屬性依賴于 writtenData 和 totalData:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"writtenData",@"totalData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  1. 實(shí)現(xiàn)一個(gè)遵循命名規(guī)則為keyPathsForValuesAffecting<Key>的類(lèi)方法退腥,<Key>是依賴于其他值的屬性名(首字母大寫(xiě)):
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
    return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}

注意:以上兩個(gè)方法可以同時(shí)存在任岸,且都會(huì)調(diào)用,但是最終結(jié)果會(huì)以keyPathsForValuesAffectingValueForKey:為準(zhǔn)狡刘。

3.8.2 一對(duì)多關(guān)系

??以上方法在觀察集合屬性時(shí)就不管用了享潜。例如,假如你有一個(gè) Department 類(lèi)嗅蔬,它有一個(gè)裝有 Employee 類(lèi)的實(shí)例對(duì)象的數(shù)組剑按,Employee 類(lèi)有 salary 屬性。你希望 Department 類(lèi)有一個(gè) totalSalary 屬性來(lái)計(jì)算所有員工的薪水澜术,也就是在這個(gè)關(guān)系中 Department 的 totalSalary 依賴于所有 Employee 實(shí)例對(duì)象的 salary 屬性艺蝴。以下有兩種方法可以解決這個(gè)問(wèn)題。

  1. 你可以用KVO將 parent(比如 Department )作為所有 children(比如 Employee )相關(guān)屬性的觀察者鸟废。你必須在把 child 添加或刪除到 parent 時(shí)把 parent 作為 child 的觀察者添加或刪除猜敢。在observeValueForKeyPath:ofObject:change:context:方法中我們可以針對(duì)被依賴項(xiàng)的變更來(lái)更新依賴項(xiàng)的值:
#import "Department.h"

static void *totalSalaryContext = &totalSalaryContext;

@interface Department ()
@property (nonatomic,strong)NSArray<Employee *> *employees;
@property (nonatomic,strong)NSNumber *totalSalary;

@end


@implementation Department

- (instancetype)initWithEmployees:(NSArray *)employees
{
    self = [super init];
    if (self) {
        self.employees = [employees copy];
        for (Employee *em in self.employees) {
            [em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
        }
    }
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
}
 
- (void)setTotalSalary:(NSNumber *)totalSalary
{
    if (_totalSalary != totalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = totalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}

- (void)dealloc
{
    for (Employee *em in self.employees) {
        [em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
    }
}

@end
  1. 使用iOS中觀察者模式的另一種實(shí)現(xiàn)方式:通知 (NSNotification) 。

4. KVO的使用注意

4.1 移除觀察者的注意點(diǎn)

  • 在調(diào)用KVO注冊(cè)方法后盒延,KVO并不會(huì)對(duì)觀察者進(jìn)行強(qiáng)引用缩擂,所以需要注意觀察者的生命周期。至少需要在觀察者銷(xiāo)毀之前添寺,調(diào)用KVO移除方法移除觀察者胯盯,否則如果在觀察者被釋放后,再次觸發(fā)KVO監(jiān)聽(tīng)方法就會(huì)導(dǎo)致Crash计露。
  • KVO的注冊(cè)方法和移除方法應(yīng)該是成對(duì)的博脑,如果重復(fù)調(diào)用移除方法,就會(huì)拋出異常NSRangeException并導(dǎo)致程序Crash票罐。
  • 蘋(píng)果官方推薦的方式是趋厉,在觀察者初始化期間(init或者viewDidLoad的時(shí)候)注冊(cè)為觀察者,在釋放過(guò)程中(dealloc時(shí))調(diào)用移除方法胶坠,這樣可以保證它們是成對(duì)出現(xiàn)的君账,是一種比較理想的使用方式。

4.2 防止多次注冊(cè)和移除相同的KVO

??有時(shí)候我們難以避免多次注冊(cè)和移除相同的KVO沈善,或者移除了一個(gè)未注冊(cè)的觀察者乡数,從而產(chǎn)生可能會(huì)導(dǎo)致Crash的風(fēng)險(xiǎn)。
??三種解決方案:黑科技防止多次添加刪除KVO出現(xiàn)的問(wèn)題

  • 利用 @try @catch(只能針對(duì)刪除多次KVO的情況下)
    NSObject增加一個(gè)分類(lèi)闻牡,然后利用Runtime API交換系統(tǒng)的removeObserver方法净赴,在里面添加@try @catch
  • 利用 模型數(shù)組 進(jìn)行存儲(chǔ)記錄罩润;
  • 利用 observationInfo 里私有屬性玖翅。

4.3 其它注意點(diǎn)

  • 如果對(duì)象被注冊(cè)成為觀察者,則該對(duì)象必須能響應(yīng)監(jiān)聽(tīng)方法,即該對(duì)象所屬類(lèi)中必須實(shí)現(xiàn)監(jiān)聽(tīng)方法金度。當(dāng)被觀察對(duì)象屬性發(fā)生改變時(shí)就會(huì)調(diào)用監(jiān)聽(tīng)方法应媚。如果沒(méi)有實(shí)現(xiàn)就會(huì)導(dǎo)致Crash。所以KVO三部曲缺一不可猜极。
  • keyPath傳入的是一個(gè)字符串乾忱,為避免寫(xiě)錯(cuò)址貌,可以使用NSStringFromSelector(@selector(propertyName))贸诚,將屬性的getter方法SEL轉(zhuǎn)換成字符串秧了,在編譯階段對(duì)keyPath進(jìn)行檢驗(yàn)。
  • 如果注冊(cè)方法中context傳的是一個(gè)對(duì)象受扳,必須在移除觀察之前持有它的強(qiáng)引用携龟,否則在監(jiān)聽(tīng)方法中訪問(wèn)context就可能導(dǎo)致Crash
  • 如果是監(jiān)聽(tīng)集合對(duì)象的改變桦卒,需要通過(guò)KVCmutableArrayValueForKey:等方法獲得代理對(duì)象立美,并使用代理對(duì)象進(jìn)行操作,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)方灾,會(huì)觸發(fā)KVO建蹄。如果直接對(duì)集合對(duì)象進(jìn)行操作改變,不會(huì)觸發(fā)KVO裕偿。
  • 在觀察者類(lèi)的監(jiān)聽(tīng)方法中洞慎,應(yīng)該為無(wú)法識(shí)別的context或者objectkeyPath調(diào)用父類(lèi)的實(shí)現(xiàn)[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];嘿棘。

5. KVO的實(shí)現(xiàn)原理

Key-Value Observing Implementation Details
  • Automatic key-value observing is implemented using a technique called 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.
  • 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.
  • 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.

??以上是蘋(píng)果官方對(duì)KVO實(shí)現(xiàn)的解釋?zhuān)徽f(shuō)明了KVO是使用isa-swizzling技術(shù)來(lái)實(shí)現(xiàn)的劲腿,并沒(méi)有做過(guò)多介紹。

5.1 isa-swizzling

??蘋(píng)果使用了isa混寫(xiě)技術(shù)(isa-swizzling)來(lái)實(shí)現(xiàn)KVO鸟妙。當(dāng)我們調(diào)用了addObserver:forKeyPath:options:context:方法焦人,為instance被觀察對(duì)象添加KVO監(jiān)聽(tīng)后,系統(tǒng)會(huì)在運(yùn)行時(shí)利用Runtime API動(dòng)態(tài)創(chuàng)建instance對(duì)象所屬類(lèi)A的子類(lèi)NSKVONotifying_A重父,并且讓instance對(duì)象的isa指向這個(gè)全新的子類(lèi)花椭,并重寫(xiě)原類(lèi)A的被觀察屬性的setter方法來(lái)達(dá)到可以通知所有觀察者對(duì)象的目的。
??這個(gè)子類(lèi)的isa指針指向它自己的meta-class對(duì)象房午,而不是原類(lèi)的meta-class對(duì)象矿辽。
??重寫(xiě)的setter方法的SEL對(duì)應(yīng)的IMPFoundation中的_NSSetXXXValueAndNotify函數(shù)(XXXKey的數(shù)據(jù)類(lèi)型),當(dāng)被觀察對(duì)象的屬性發(fā)送改變時(shí),會(huì)調(diào)用_NSSetXXXValueAndNotify函數(shù)袋倔,這個(gè)函數(shù)中會(huì)調(diào)用:

  • willChangeValueForKey:方法
  • 父類(lèi)原來(lái)的setter方法
  • didChangeValueForKey:方法(內(nèi)部會(huì)觸發(fā)監(jiān)聽(tīng)器即觀察對(duì)象observer的監(jiān)聽(tīng)方法:observeValueForKeyPath:ofObject:change:context:

??在移除KVO監(jiān)聽(tīng)后雕蔽,被觀察對(duì)象的isa會(huì)指回原類(lèi)A,但是NSKVONotifying_A類(lèi)并沒(méi)有銷(xiāo)毀奕污,還保存在內(nèi)存中萎羔。

5.2 KVO 動(dòng)態(tài)生成的子類(lèi)都有哪些方法

??NSKVONotifying_A除了重寫(xiě)了setter方法,還重寫(xiě)了class碳默、dealloc贾陷、_isKVOA這三個(gè)方法(可以使用runtimeclass_copyMethodList函數(shù)打印方法列表獲得),其中:

  • classclass方法中返回的是父類(lèi)的class對(duì)象嘱根,目的是為了不讓外界知道KVO動(dòng)態(tài)生成類(lèi)的存在髓废;
  • dealloc:釋放KVO使用過(guò)程中產(chǎn)生的東西;
  • _isKVOA:用來(lái)標(biāo)志它是一個(gè)KVO的類(lèi)该抒。

6. FBKVOController

6.1 系統(tǒng) KVO 的缺點(diǎn)

  • 使用比較麻煩慌洪,需要三個(gè)步驟:添加/注冊(cè)KVO監(jiān)聽(tīng)、實(shí)現(xiàn)監(jiān)聽(tīng)方法以接收屬性改變通知凑保、 移除KVO監(jiān)聽(tīng)冈爹,缺一不可;
  • 需要手動(dòng)移除觀察者欧引,移除觀察者的時(shí)機(jī)必須合適频伤,還不能重復(fù)移除;
  • 注冊(cè)觀察者的代碼和事件發(fā)生處的代碼上下文不同芝此,傳遞上下文context是通過(guò)void *指針憋肖;
  • 需要實(shí)現(xiàn)-observeValueForKeyPath:ofObject:change:context:方法,比較麻煩婚苹;
  • 在復(fù)雜的業(yè)務(wù)邏輯中岸更,準(zhǔn)確判斷被觀察者相對(duì)比較麻煩,有多個(gè)被觀測(cè)的對(duì)象和屬性時(shí)膊升,需要在方法中寫(xiě)大量的if進(jìn)行判斷怎炊。

6.2 FBKVOController 的介紹

FBKVOController是 Facebook 開(kāi)源的一個(gè)基于系統(tǒng)KVO實(shí)現(xiàn)的框架。支持Objective-CSwift語(yǔ)言廓译。
GitHub:https://github.com/facebook/KVOController

6.3 FBKVOController 的優(yōu)點(diǎn)

  • 會(huì)自動(dòng)移除觀察者结胀;
  • 函數(shù)式編程,可以一行代碼實(shí)現(xiàn)系統(tǒng)KVO的三個(gè)步驟责循;
  • 實(shí)現(xiàn)KVO與事件發(fā)生處的代碼上下文相同糟港,不需要跨方法傳參數(shù);
  • 增加了blockSEL自定義操作對(duì)NSKeyValueObserving回調(diào)的處理支持院仿;
  • 每一個(gè)keyPath會(huì)對(duì)應(yīng)一個(gè)block或者SEL秸抚,不需要使用if判斷keyPath速和;
  • 可以同時(shí)對(duì)一個(gè)對(duì)象的多個(gè)屬性進(jìn)行監(jiān)聽(tīng),寫(xiě)法簡(jiǎn)潔剥汤;
  • 線程安全颠放。

6.4 FBKVOController 的使用

FBKVOController實(shí)現(xiàn)了觀察者和被觀察者的角色反轉(zhuǎn),系統(tǒng)的KVO是被觀察者添加觀察者吭敢,而FBKVO實(shí)現(xiàn)了觀察者主動(dòng)去添加被觀察者碰凶,實(shí)現(xiàn)了角色上的反轉(zhuǎn),使用比較方便鹿驼。

// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// observe clock date property
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];

6.5 FBKVOController 的解析

如何優(yōu)雅地使用KVO(簡(jiǎn)書(shū))
iOS - FBKVOController 實(shí)現(xiàn)原理(簡(jiǎn)書(shū))

參考

Key-Value Observing Programming Guide(蘋(píng)果官方文檔)
iOS - 關(guān)于 KVC 的一些總結(jié)(簡(jiǎn)書(shū))
KVO原理分析及使用進(jìn)階(簡(jiǎn)書(shū))
iOS開(kāi)發(fā) - 黑科技防止多次添加刪除KVO出現(xiàn)的問(wèn)題(簡(jiǎn)書(shū))
談?wù)?KVO(簡(jiǎn)書(shū))
GitHub/facebook/KVOController(GitHub)
如何優(yōu)雅地使用KVO(簡(jiǎn)書(shū))
iOS - FBKVOController 實(shí)現(xiàn)原理(簡(jiǎn)書(shū))

最后編輯于
?著作權(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)離奇詭異腊瑟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)块蚌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)闰非,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人峭范,你說(shuō)我怎么就攤上這事财松。” “怎么了虎敦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵游岳,是天一觀的道長(zhǎng)政敢。 經(jīng)常有香客問(wèn)我其徙,道長(zhǎng),這世上最難降的妖魔是什么喷户? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任唾那,我火速辦了婚禮,結(jié)果婚禮上褪尝,老公的妹妹穿的比我還像新娘闹获。我一直安慰自己,他們只是感情好河哑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布避诽。 她就那樣靜靜地躺著,像睡著了一般璃谨。 火紅的嫁衣襯著肌膚如雪沙庐。 梳的紋絲不亂的頭發(fā)上鲤妥,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音拱雏,去河邊找鬼棉安。 笑死,一個(gè)胖子當(dāng)著我的面吹牛铸抑,可吹牛的內(nèi)容都是我干的贡耽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鹊汛,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蒲赂!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起柒昏,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤凳宙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后职祷,有當(dāng)?shù)厝嗽跇?shù)林里發(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
  • 文/蒙蒙 一扫皱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捷绑,春花似錦韩脑、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至壮吩,卻和暖如春进苍,著一層夾襖步出監(jiān)牢的瞬間蕾总,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工琅捏, 沒(méi)想到剛下飛機(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

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