談?wù)?KVO


本文結(jié)構(gòu)如下:

  • Why? (為什么要用KVO)
  • What? (KVO是什么)
  • How? ( KVO怎么用)
  • More (更多細節(jié))
  • 原理
  • 自己實現(xiàn)KVO

在我的上一篇文章淺談 iOS Notification中,我們說到了iOS中觀察者模式的一種實現(xiàn)方式:NSNotification 通知,這次我們再來談?wù)刬OS中觀察者模式的另一種實現(xiàn)方式:KVO

Why?

假如,有一個person類,和一個Account類,account類中又有兩個公開的屬性,balance和interestRate,當account中的balance和interestRate發(fā)生變化時,需要知道通知到這個person,這個要求很正常,我的銀行賬戶里的錢增加或減少了我當然要及時知道啊。有人可能會想,每隔一段時間去輪詢Account中的balance和interestRate,當其發(fā)生變化就通知person,但是這樣做不僅低效而且通知也不能及時發(fā)出。


這個時候KVO就派上用場了。

What?

KVO到底是什么呢?不著急,要說KVO還得先說下KVC,KVC(Key-value coding)是一種基于NSKeyValueCoding非正式協(xié)議的機制,能讓我們直接使用一個或一串字符串標識符去訪問,操作類的屬性。
常用的方法比如:

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

通過這些方法加上正確的標識符(一般和屬性同名),可以直接獲取或者設(shè)置一個類的屬性,甚至可以輕易越過多個類的層級結(jié)構(gòu),直接獲取目標屬性。


KVC還提供了集合操作的方法,直接獲取到集合屬性的同時還能對其進行求和,取平均數(shù),求最大最小值等操作,如下為求和操作,具體可以到蘋果官方文檔詳細了解。

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
KVO

KVO (Key-Value Observing) 是Cocoa提供的一種基于KVC的機制,允許一個對象去監(jiān)聽另一個對象的某個屬性,當該屬性改變時系統(tǒng)會去通知監(jiān)聽的對象(不是被監(jiān)聽的對象)。

上面那個例子如果用KVO實現(xiàn)的話,大概就是,用Person類的一個對象去監(jiān)聽Account類的一個對象的屬性,然后當Account類對象的相應(yīng)屬性改變時,Person類的對象就會收到通知。這也是iOS種觀察者模式的一種實現(xiàn)方式。

也就是說,一般情況下,任何一個對象可以監(jiān)聽任何一個對象(當然也包括自己本身)的任意屬性,然后在其屬性變化后收到通知。

How?

那么KVO怎么用呢?KVO的使用步驟主要分為3步:添加監(jiān)聽接收通知移除監(jiān)聽

1. 添加監(jiān)聽

通過以下方法添加一個監(jiān)聽者:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

首先說下這個消息(方法)的接收者,就是調(diào)用這個方法的對象,即觀察者模式中的被觀察者,這里有一點需要注意的是,被監(jiān)聽的對象最好不要設(shè)置為,在當前上下文環(huán)境中容易改變的對象,因為KVO只會把添加KVO時的被監(jiān)聽對象保存下來,之后要是被監(jiān)聽對象發(fā)生改變了,KVO監(jiān)聽的還是之前保存的那個對象,KVO回調(diào)方法就不會觸發(fā)了。

然后,我們重點關(guān)注一下這個方法的4個參數(shù):

  • observer:就是要添加的監(jiān)聽者對象,,當監(jiān)聽的屬性發(fā)生改變時就會去通知該對象,該對象必須實現(xiàn)- observeValueForKeyPath:ofObject:change:context:方法,要不然當監(jiān)聽的屬性的改變通知發(fā)出來,卻發(fā)現(xiàn)沒有相應(yīng)的接收方法時,程序會拋出異常。

  • keyPath:就是要被監(jiān)聽的屬性,這里和KVC的規(guī)則一樣。但是這個值不能傳nil,要不然會報錯。通常我們在用的時候會傳一個與屬性同名的字符串,但是這樣可能會因為拼寫錯誤,導(dǎo)致監(jiān)聽不成功,一個推薦的做法是,用這種方式NSStringFromSelector(@selector(propertyName)),其實就是是將屬性的getter方法轉(zhuǎn)換成了字符串,這樣做的好處就是,如果你寫錯了屬性名,xcode會用警告提醒你。

  • options:是一些配置選項,用來指明通知發(fā)出的時機和通知響應(yīng)方法- observeValueForKeyPath:ofObject:change:context:change字典中包含哪些值,它的取值有4個,定義在NSKeyValueObservingOptions中,可以用|符號連接,如下:
    1> NSKeyValueObservingOptionNew:指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變后的新值。

2>NSKeyValueObservingOptionOld: 指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變前的舊值。

3>NSKeyValueObservingOptionInitial: 當指定了這個選項時,在addObserver:forKeyPath:options:context:消息被發(fā)出去后,甚至不用等待這個消息返回,監(jiān)聽者對象會馬上收到一個通知。這種通知只會發(fā)送一次,你可以利用這種“一次性“的通知來確定要監(jiān)聽屬性的初始值。當同時制定這3個選項時,這種通知的change字典中只會包含新值,而不會包含舊值。雖然這時候的新值實際上是改變前的'舊值',但是這個值對于監(jiān)聽者來說是新的。

4>NSKeyValueObservingOptionPrior:當指定了這個選項時,在被監(jiān)聽的屬性被改變前,監(jiān)聽者對象就會收到一個通知(一般的通知發(fā)出時機都是在屬性改變后,雖然change字典中包含了新值和舊值,但是通知還是在屬性改變后才發(fā)出),這個通知會包含一個NSKeyValueChangeNotificationIsPriorKeykey,其對應(yīng)的值為一個NSNumber類型的YES。當同時指定該值、new和old的話,change字典會包含舊值而不會包含新值。你可以在這個通知中調(diào)用- (void)willChangeValueForKey:(NSString *)key;

  • context:添加監(jiān)聽方法的最后一個參數(shù),是一個可選的參數(shù),可以傳任何數(shù)據(jù),這個參數(shù)最后會被傳到監(jiān)聽者的響應(yīng)方法中,可以用來區(qū)分不同通知,也可以用來傳值。如果你要用context來區(qū)分不同的通知,一個推薦的做法是聲明一個靜態(tài)變量,其保持它自己的地址,這個變量沒有什么意義,但是卻能起到區(qū)分的作用,如下:
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

然后,結(jié)合上面Person,account的例子,我們可以給Account對象添加監(jiān)聽:

 - (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

需要注意的是,添加監(jiān)聽的方法addObserver:forKeyPath:options:context:并不會對監(jiān)聽和被監(jiān)聽的對象以及context做強引用,你必須自己保證他們在監(jiān)聽過程中不被釋放。

2. 接受通知

前面說過了,每一個監(jiān)聽者對象都必須實現(xiàn)下面這個方法來接收通知:

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

keyPath,object,context和監(jiān)聽方法中指定的一樣,關(guān)于change參數(shù),它是一個字典,有五個常量作為它的鍵:

NSString *const NSKeyValueChangeKindKey;  
NSString *const NSKeyValueChangeNewKey;  
NSString *const NSKeyValueChangeOldKey;  
NSString *const NSKeyValueChangeIndexesKey;  
NSString *const NSKeyValueChangeNotificationIsPriorKey;

一個一個分析下:

  • NSKeyValueChangeKindKey:指明了變更的類型,值為“NSKeyValueChange”枚舉中的某一個,類型為NSNumber。
enum {
   NSKeyValueChangeSetting = 1,
   NSKeyValueChangeInsertion = 2,
   NSKeyValueChangeRemoval = 3,
   NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

一般情況下返回的都是1也就是第一個NSKeyValueChangeSetting,但是如果你監(jiān)聽的屬性是一個集合對象的話,當這個集合中的元素被插入,刪除,替換時,就會分別返回NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement

  • NSKeyValueChangeNewKey:被監(jiān)聽屬性改變后新值的key,當監(jiān)聽屬性為一個集合對象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時,該值返回的是一個數(shù)組,包含插入,替換后的新值(刪除操作不會返回新值)。

  • NSKeyValueChangeOldKey:被監(jiān)聽屬性改變前舊值的key,當監(jiān)聽屬性為一個集合對象,且NSKeyValueChangeKindKey不為NSKeyValueChangeSetting時,該值返回的是一個數(shù)組,包含刪除,替換前的舊值(插入操作不會返回舊值)

  • NSKeyValueChangeIndexesKey:如果NSKeyValueChangeKindKey的值為NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,這個鍵的值是一個NSIndexSet對象,包含了增加,移除或者替換對象的index。

  • NSKeyValueChangeNotificationIsPriorKey:如果注冊監(jiān)聽者是options中指明了NSKeyValueObservingOptionPriorchange字典中就會帶有這個key,值為NSNumber類型的YES.

最后,完整的change字典大概就類似這樣:

    NSDictionary *change = @{
                             NSKeyValueChangeKindKey : NSKeyValueChange(枚舉值),
                             NSKeyValueChangeNewKey : newValue,
                             NSKeyValueChangeOldKey : oldValue,
                             NSKeyValueChangeIndexesKey : @[NSIndexSet, NSIndexSet],
                             NSKeyValueChangeNotificationIsPriorKey : @1,
                             };

繼續(xù)用上面的例子實現(xiàn)接受通知如下:

 - (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或者keypath來區(qū)分不同的通知,但是要注意的是,正如上面實例代碼中那樣,當接收到一個不能識別的context或者keypath的話,需要調(diào)用一下父類的- observeValueForKeyPath:ofObject:change:context:方法

3. 移除監(jiān)聽

當一個監(jiān)聽者完成了它的監(jiān)聽任務(wù)之后,就需要注銷(移除)監(jiān)聽者,調(diào)用以下2個方法來移除監(jiān)聽。通常會在-dealloc方法或者-observeValueForKeyPath:ofObject:change:context:方法中移除。

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

有幾點需要注意的:

  • 當你向一個不是監(jiān)聽者的對象發(fā)送remove消息的時候(也可能是,你發(fā)送remove消息時,接受消息的對象已經(jīng)被remove了一次,或者在注冊為監(jiān)聽者前就調(diào)用了remove),xcode會拋出一個NSRangeException異常,所以,保險的做法是,把remove操作放在try/catch中。

  • 一個監(jiān)聽者在其被銷毀時,并不會自己注銷監(jiān)聽,而給一個已經(jīng)銷毀的監(jiān)聽者發(fā)送通知,會造成野指針錯誤。所以至少保證,在監(jiān)聽者被釋放前,將其監(jiān)聽注銷。保證有一個add方法,就有一個remove方法。

More

再說更多的一些東西,想讓類的某個屬性支持KVO機制的話,這個類必須滿足一下3點:

  1. 這個類必須使得該屬性支持KVC。
  2. 這個類必須保證能夠?qū)⒏淖兺ㄖl(fā)出。
  3. 當有依賴關(guān)系的時候,注冊合適的依賴鍵。
  • 第一個條件:這個類必須使得該屬性支持KVC
    就是需要實現(xiàn)與該屬性對應(yīng)的getter和setter方法和其他一些可選方法。幸運的是,NSObject類已經(jīng)幫我們實現(xiàn)了這些,只要你的類最終是繼承自NSObject,并且使用正常的方式創(chuàng)建屬性,這些屬性都是支持KVO的。

KVO支持的類型和KVC一樣,包括對象類型,標量(例如 intCGFloat)和 struct(例如 CGRect)。

  • 第二個條件:這個類必須保證能夠?qū)⒏淖兺ㄖl(fā)出。
    通知發(fā)出的方式又分為自動通知手動通知
    1> 自動通知
    自動通知由NSObject默認實現(xiàn)了,也就是說一般情況下,你不用寫額外的一些代碼,屬性改變的通知就會自動發(fā)出,這也是我們平常開發(fā)中接觸最多的。

觸發(fā)自動通知發(fā)出的方式包括下面這些:

 // Call the accessor method.
 [account setName:@"Savings"];
 
 // Use setValue:forKey:.
 [account setValue:@"Savings" forKey:@"name"];
 
 // Use a key path, where 'account' is a kvc-compliant property of 'document'.
 [document setValue:@"Savings" forKeyPath:@"account.name"];
 
 // Use mutableArrayValueForKey: to retrieve a relationship proxy object.
 Transaction *newTransaction = <#Create a new transaction for the account#>;
 NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
 [transactions addObject:newTransaction];

其中包括調(diào)用setter方法,調(diào)用KVC的setValue:forKey:setValue:forKeyPath:,最后一個方法需要說一下,mutableArrayValueForKey:也是KVC的方法,大家應(yīng)該都知道,如果你用KVO監(jiān)聽了一個集合對象(比如一個數(shù)組),當你給數(shù)組發(fā)送addObject:消息時,是不會觸發(fā)KVO通知的,但是通過mutableArrayValueForKey:這個方法對集合對象進行的相關(guān)操作(增加,刪除,替換元素)就會觸發(fā)KVO通知,這個方法會返回一個中間代理對象,這個中間代理對象的類會指向一個中間類,你在這個代理對象上進行的操作最終應(yīng)在原始對象上造成同樣的效果。
2> 手動通知
有時候,你可能會想控制通知的發(fā)送,比如,阻止一些不必要的通知發(fā)出,或者把一組類似的通知合并成一個,這時候就需要手動發(fā)送通知了。

首先,你需要重寫NSObject的一個類方法,來指明你不想讓哪個屬性的改變通知自動發(fā)出。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

   BOOL automatic = NO;
   if ([theKey isEqualToString:@"balance"]) {
       automatic = NO;
   }
   else {
       automatic = [super automaticallyNotifiesObserversForKey:theKey];
   }
   return automatic;
}

如上,return NO就可以阻止,該key對應(yīng)的屬性改變時,通知不會自動發(fā)送給監(jiān)聽者對象,當然對于其他的屬性別忘了調(diào)用super方法保持它原來的狀態(tài)。(改方法默認返回YES)

然后,你需要重寫你想手動發(fā)送通知屬性的setter方法,然后在屬性值改變之前和之后分別調(diào)用willChangeValueForKey:didChangeValueForKey:方法。

 - (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
 }

這樣就基本實現(xiàn)了一個KVO的手動通知,當該屬性值改變時,監(jiān)聽者對象就能收到改變通知了。

你還可以過濾一些通知,像下面的例子就是只有當屬性真正改變時才會發(fā)出通知

 - (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
 }

如果一個操作導(dǎo)致了多個鍵的變化,你必須嵌套變更通知:

 - (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
 }

在to-many關(guān)系操作的情形中,你不僅必須表明key是什么,還要表明變更類型和影響到的索引。變更類型是一個 NSKeyValueChange值,被影響對象的索引是一個 NSIndexSet對象。

下面的代碼示范了在to-many關(guān)系transactions對象中的刪除操作:

 - (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"];
 }
  • 第三個條件:這個類必須使得該屬性支持KVC

有時候會存在這樣一種情況,一個屬性的改變依賴于別的一個或多個屬性的改變,也就是說當別的屬性改了,這個屬性也會跟著改變,比如說一個人的全名fullName包括firstName和lastName,當firstName或者lastName中任何一個值改變了,fullName也就改變了。一個監(jiān)聽者監(jiān)聽了fullName,當firstName或者lastName改變時,這個監(jiān)聽者也應(yīng)該被通知。

一種方法就是重寫keyPathsForValuesAffectingValueForKey:方法去指明fullName屬性是依賴于lastNamefirstName的:

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

另一種實現(xiàn)同樣結(jié)果的方法是實現(xiàn)一個遵循命名方式為keyPathsForValuesAffecting<Key>的類方法,<Key>是依賴于其他值的屬性名(首字母大寫),用上面代碼的例子來重新實現(xiàn)一下:

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

但是在To-many Relationships中(比如數(shù)組屬性),上面的方法就不管用了,比如,假如你有一個Department類,它有一個針對Employee類的to-many關(guān)系(即擁有一個裝有Employee類對象的數(shù)組),Employee類有salary屬性。你希望Department類有一個totalSalary屬性來計算所有員工的薪水,也就是在這個關(guān)系中DepartmenttotalSalary依賴于所有Employeesalary屬性。這種情況你不能通過實現(xiàn)keyPathsForValuesAffectingTotalSalary方法并返回employees.salary

有兩種解決方法:

  1. 你可以用KVO將parent(比如Department)作為所有children(比如Employee)相關(guān)屬性的觀察者。你必須在把child添加或刪除到parent時也把parent作為child的觀察者添加或刪除。在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;
}
  1. 使用iOS中觀察者模式的另一種實現(xiàn)方式:通知 (NSNotification) ,有關(guān)通知相關(guān)的概念和用法,可以參考我上一篇文章 淺談 iOS Notification 。??

原理

說了這么多,KVO的原理到底是什么呢?

先上官方文檔:

  Automatic key-value observing is implemented using a technique called 
isa-swizzling...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.

對于KVO實現(xiàn)的原理,蘋果官方文檔描述的比較少,從中只能知道蘋果使用了一張叫做isa-swizzling的黑魔法...

其實,當某個類的對象第一次被觀察時,系統(tǒng)就會在運行期動態(tài)地創(chuàng)建該類的一個派生類(類名就是在該類的前面加上NSKVONotifying_ 前綴),在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。

派生類在被重寫的 setter 方法實現(xiàn)真正的通知機制,就如前面手動實現(xiàn)鍵值觀察那樣,調(diào)用willChangeValueForKey:didChangeValueForKey:方法。這么做是基于設(shè)置屬性會調(diào)用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設(shè)置方式來變更屬性值,如果僅是直接修改屬性對應(yīng)的成員變量,是無法實現(xiàn) KVO 的。

同時派生類還重寫了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個類。然后系統(tǒng)將這個對象的 isa 指針指向這個新誕生的派生類,因此這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調(diào)用就會調(diào)用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。

自己實現(xiàn)KVO

港真,原生的KVO API是不太友好的,需要監(jiān)聽者對象,和被監(jiān)聽的對象分別去實現(xiàn)一些東西,代碼實現(xiàn)比較分散,并且響應(yīng)通知的方法也不能自定義,只能在蘋果提供的方法中處理,不能用我們熟悉的block或者Target-Action,最后還不能忘了調(diào)用removeObserve方法,一忘可能程序運行的時候就奔潰了...

在知道了KVO的使用方法和內(nèi)部原理之后,我們其實可以自己去實現(xiàn)一個使用起來更加便捷,API更加友好的KVO的,這類的實現(xiàn)網(wǎng)上有很多,我就不獻丑了... github上也有一些開源的實現(xiàn)代碼,感興趣的童鞋可以自行查閱。

其實基本思路和蘋果官方的原理差不多,都是創(chuàng)建一個原類的派生類當做中間類,再把原來的對象指向這個中間類,再重寫監(jiān)聽屬性的Setter方法,在屬性改變后調(diào)用回調(diào)通知監(jiān)聽者。


參考:

KVO官方文檔
KVC官方文檔
Objective-C中的KVC和KVO
詳解鍵值觀察(KVO)及其實現(xiàn)機理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

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

  • 本文由我們團隊的 糾結(jié)倫 童鞋撰寫。 文章結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識小集閱讀 7,420評論 7 105
  • 在iOS開發(fā)中,我們常常用到鍵值編碼KVC和鍵值監(jiān)聽KVO兩個東東,今天小編和大家分享的就是這兩個東東在應(yīng)用開發(fā)中...
    突然自我閱讀 1,007評論 2 3
  • 由于ObjC主要基于Smalltalk進行設(shè)計,因此它有很多類似于Ruby、Python的動態(tài)特性,例如動態(tài)類型、...
    JonesCxy閱讀 376評論 0 0
  • 你要知道的KVC、KVO、Delegate、Notification都在這里 轉(zhuǎn)載請注明出處 http://www...
    WWWWDotPNG閱讀 1,820評論 1 3
  • 由于ObjC主要基于Smalltalk進行設(shè)計,因此它有很多類似于Ruby、Python的動態(tài)特性,例如動態(tài)類型、...
    JonesCxy閱讀 480評論 1 0