1.KVC
關(guān)于 KVC 和 KVO ,我之前的總結(jié)文章有寫過,但是趨于表面,沒有探究其內(nèi)部真正的實現(xiàn)原理和進(jìn)階用法,這次總結(jié)正好給了我很好的學(xué)習(xí)機(jī)會,在此深入的總結(jié)一下 KVC 和 KVO 。
KVC,即是指 NSKeyValueCoding,一個非正式的 Protocol,提供一種機(jī)制來間接訪問對象的屬性。KVO 就是基于 KVC 實現(xiàn)的關(guān)鍵技術(shù)之一,相關(guān)的技術(shù)還有 Cocoa 綁定,Core Data 和 AppleScript。
Api示例
Objective-C 中 KVC 的定義是對 NSObject 的擴(kuò)展來實現(xiàn)的。所以對于所有繼承了 NSObject 在類型,都可以使用KVC ,下面是 KVC 最為重要的四個方法
- (nullable id)valueForKey:(NSString *)key; //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過Key來設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設(shè)值
一般來講,Obj-C 對象中都會有一些屬性。如代碼所示
#import <Foundation/Foundation.h>
@interface Person : NSObject
/** name */
@property ( nonatomic,copy ) NSString *name;
/** Address */
@property ( nonatomic,copy ) NSString *address;
/** Friends */
@property ( nonatomic,copy ) NSArray<Person *> *address;
/** Spouse */
@property ( nonatomic,copy ) Person *Spouse;
@end
上面的 Person 對象所擁有的多個屬性,以 KVC 的角度來看,就是 Person 對象的 name , address 等屬性分別有一個Value 對應(yīng)他們的 Key 值。
- Key 是一個字符串類型。
- Value 可以為任何類型。
KVC 為存取值提供了兩個最基礎(chǔ)的方法。
Person *man = [Person new];
// 存值
[man setValue:@"LiMing" forKey:@"name"];
// 取值
NSString *name = [man valueForKey:@"name"];
KVC 為了便于使用還提供了另外兩個方法。
假設(shè)我們之前創(chuàng)建的這個對象有一個配偶,配偶也是一個Person對象,此時我們想在man這里讀出woman的name屬性
可以這樣操作
Person *woman = [Person new];
man.spouse = woman;
[man setValue:@"Lily" forKeyPath:@"spouse.name"];
NSLog(@"%@",[man valueForKeyPath:@"spouse.name"]);
// Key 與 KeyPath 要區(qū)分開來
// Key 可以讓你從一個對象中獲取值
// KeyPath 可以讓你通過連續(xù)的多個Key獲取值,著多個key值用點號 “.” 分割連接起來
簡單對比一下
// 結(jié)果一樣的,但是用 KeyPath 更簡單
[man valueForKeyPath:@"spouse.name"]
[[man valueForKey:@"spouse"] valueForKey:@"name"];
// 其實點語法完全可以實現(xiàn)(為什么要這么用呢?)
NSLog(@"%@",man.spouse.name);
KVC 尋找 Key 值過程
KVC在某種程度上提供了訪問器的替代方案,不過只要有可能,KVC也是在訪問器方法的幫助下工作。KVC按照以下順序?qū)ふ襅ey值。
1.賦值
當(dāng)程序調(diào)用
- (void)setValue:(nullable id)value forKey:(NSString *)key;
1.優(yōu)先尋找訪問器方法
程序會優(yōu)先調(diào)用 setKey 的屬性值方法,代碼直接通過 Setter 方法完成設(shè)置。這里的 key 值指的是成員變量名,Key 值首字母大寫要符合 Setter 和 Getter 方法的命名規(guī)則。
2.尋找_key
如果沒有找到 setKey 的訪問器方法,KVC 機(jī)制會檢查
+ (BOOL)accessInstanceVariablesDirectly
的返回值是否為NO,此方法默認(rèn)返回的是YES。如果開發(fā)者重寫了該方法讓這個返回值為NO時,接下來KVC會直接調(diào)用
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
這個時候如果你不做其他操作,就要報出異常了,所以一般人都不會這么做。
接下來 KVC 機(jī)制會搜索該類里面有沒有 _key 的成員變量,無論你是在聲明文件中定義,還是在實現(xiàn)文件中定義,也無論使用了什么樣的屬性修飾符,只要存在著 _key 命名的變量,KVC 都可以對該成員變量賦值。
3.尋找_isKey
如果該類既沒有 setKey: 的訪問器方法,也沒有 _key 成員變量,KVC 機(jī)制會搜索 _isKey 的成員變量。
4.尋找Key和isKey
和上面一樣,如果該類既沒有 setKey: 的訪問器方法,也沒有 _key 和 _isKey 成員變量,KVC 機(jī)制再會繼續(xù)搜索 key 和 isKey 的成員變量,再給它們賦值。
如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的 setValue:forUNdefinedKey: 方法,默認(rèn)是拋出異常。
如果開發(fā)者想讓這個類禁用 KVC ,那么重寫 + (BOOL)accessInstanceVariablesDirectly 方法讓其返回NO即可,這樣的話如果 KVC 沒有找到 set<Key>: 屬性名時,會直接用 setValue:forUNdefinedKey: 方法。
2.取值
當(dāng)程序調(diào)用
- (nullable id)valueForKey:(NSString *)key;
1.優(yōu)先查找訪問器的方法
首先按 getKey 、key、isKey 的順序查找 getter 方法,找到直接調(diào)用。如果是 bool 、 int 等內(nèi)建值類型,會做NSNumber的轉(zhuǎn)換。
2.有序集合中查找
上面的 getter 沒有找到,查找 countOfKey 、 objectInKeyAtIndex: 、 KeyAtIndexes 格式的方法。
如果 countOfKey 和另外兩個方法中的一個找到,那么就會返回一個可以響應(yīng) NSArray 所有方法的代理集合。發(fā)送給這個代理集合的 NSArray 消息方法,就會以countOfKey、objectInKeyAtIndex:、KeyAtIndexes這幾個方法組合的形式調(diào)用。還有一個可選的 getKey:range: 方法。
3.無序集合中查找
還沒查到,那么查找 countOfKey 、 enumeratorOfKey 、 memberOfKey: 格式的方法。
如果這三個方法都找到,那么就返回一個可以響應(yīng)NSSet所有方法的代理集合。發(fā)送給這個代理集合的NSSet消息方法,就會以countOfKey、enumeratorOfKey、memberOfKey:組合的形式調(diào)用。
4.搜索成員變量名
還是沒查到,那么如果類方法 accessInstanceVariablesDirectly 返回 YES ,那么按 _key ,_isKey ,key,iskey 的順序直接搜索成員名。
5.報出異常
再找不到,調(diào)用ValueForUndefinedKey:,默認(rèn)報出異常
針對集合類型的 KVC
我們上面講的KVC是一對一關(guān)系,比如 Person 類中的 name 屬性。但也有一對多的關(guān)系,比如 Person 中有一個friends屬性,保存的是一個人的所有的朋友,這時候就需要集合來處理了。
對于集合類的處理,我們有兩種選擇
1.通過KVC將集合類先取出,然后在針對集合進(jìn)行處理
2.采用KVC提供的模板方法
有序集合
這里面的Key,就是被監(jiān)聽的屬性名稱
-countOfKey
//必須實現(xiàn),對應(yīng)于NSArray的基本方法count:
- objectInKeyAtIndex:
- keyAtIndexes:
//這兩個必須實現(xiàn)一個,對應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
- getKey:range:
//不是必須實現(xiàn)的,但實現(xiàn)后可以提高性能,其對應(yīng)于 NSArray 方法
- getObjects:range:
- insertObject:inKeyAtIndex:
- insertKey:atIndexes:
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
- removeObjectFromKeyAtIndex:
- removeKeyAtIndexes:
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
- replaceObjectInKeyAtIndex:withObject:
- replaceKeyAtIndexes:withKey:
//可選的,如果在此類操作上有性能問題,就需要考慮實現(xiàn)之
無序集合
- countOfKey
//必須實現(xiàn),對應(yīng)于NSArray的基本方法count:
- objectInKeyAtIndex:
- keyAtIndexes:
//這兩個必須實現(xiàn)一個,對應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
- getKey:range:
//不是必須實現(xiàn)的,但實現(xiàn)后可以提高性能,其對應(yīng)于 NSArray 方法
- getObjects:range:
- insertObject:inKeyAtIndex:
- insertKey:atIndexes:
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
- removeObjectFromKeyAtIndex:
- removeKeyAtIndexes:
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
- replaceObjectInKeyAtIndex:withObject:
- replaceKeyAtIndexes:withKey:
//這兩個都是可選的,如果在此類操作上有性能問題,就需要考慮實現(xiàn)之
KVC對基本數(shù)據(jù)類型和結(jié)構(gòu)體的支持
1.對基本數(shù)據(jù)類型會以 NSNumber 進(jìn)行包裝
+ (NSNumber *)numberWithChar:(char)value;
+ (NSNumber *)numberWithUnsignedChar:(unsigned char)value;
+ (NSNumber *)numberWithShort:(short)value;
+ (NSNumber *)numberWithUnsignedShort:(unsigned short)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithUnsignedInt:(unsigned int)value;
+ (NSNumber *)numberWithLong:(long)value;
+ (NSNumber *)numberWithUnsignedLong:(unsigned long)value;
+ (NSNumber *)numberWithLongLong:(long long)value;
+ (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithDouble:(double)value;
+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithInteger:(NSInteger)value NS_AVAILABLE(10_5, 2_0);
+ (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value NS_AVAILABLE(10_5, 2_0);
2.對結(jié)構(gòu)體會以 NSValue 進(jìn)行包裝
+ (NSValue *)valueWithCGPoint:(CGPoint)point;
+ (NSValue *)valueWithCGSize:(CGSize)size;
+ (NSValue *)valueWithCGRect:(CGRect)rect;
+ (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue *)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue *)valueWithUIOffset:(UIOffset)insets NS_AVAILABLE_IOS(5_0);
所有的結(jié)構(gòu)體都支持以NSValue進(jìn)行封裝
KVC中的集合運(yùn)算符
[圖片上傳失敗...(image-739934-1602312865707)]
集合運(yùn)算符是一個特殊的KeyPath,可以作為參數(shù)傳遞給valueForKeyPath:方法
1.簡單的集合運(yùn)算符
簡單的集合運(yùn)算符有以下幾個 @avg,@count,@max,@min,@sum5
2.對象運(yùn)算符
對象運(yùn)算符有@distinctUnionOfObjects,
@unionOfObjects,這兩個運(yùn)算符返回的對象都是NSArray。
1.@distinctUnionOfObjects會將集合在剔除重復(fù)對象之后返回
2.@unionOfObjects會直接返回所有對象
NSKeyValueCoding其他方法
+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES,表示如果沒有找到SetKey方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設(shè)置成NO就會直接拋出異常。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值確認(rèn)的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設(shè)置新值并返回錯誤原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性,則會調(diào)用這個方法,默認(rèn)是拋出異常
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個方法一樣,只不過是設(shè)值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時面給Value傳nil,則會調(diào)用這個方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應(yīng)的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。
2.KVO
1.認(rèn)識KVO
KVO 類似于觀察者模式,我們利用簡單的代碼來了解什么是 KVO。
// 注冊一個Person類
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@end
// 再注冊一個Dog類
#import <Foundation/Foundation.h>
@interface Dog : NSObject
@property (nonatomic,copy ) NSString *name;
@end
我們在 ViewController 中引入頭文件,并創(chuàng)建兩個全局的屬性。我們希望Person作為Dog的觀察者,當(dāng)Dog的name屬性發(fā)生變化的時候,Person可以第一時間知道。這時我們就可以運(yùn)用KVO的技術(shù)。
Person *p = [Person new];
self.p = p;
Dog *dog = [Dog new];
self.dog = dog;
// 成為其他對象的觀察者要進(jìn)行注冊
// KeyPath代表監(jiān)聽對象的具體屬性
// Observe就是觀察者
// Options可以指定觀察的值的新舊等
// Context可以是任何對象,可以向觀察者傳遞信息,也可以用指定的標(biāo)識對不同的觀察者進(jìn)行區(qū)分
[dog addObserver:p
forKeyPath:@"name"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:nil];
dog.name = @"旺財";
監(jiān)聽選項Options是由枚舉NSKeyValueObservingOptions定義的,他決定了哪些值可以被傳入到觀察者內(nèi)部實現(xiàn)的方法中。
定義如下:
enum {
// 提供新值
NSKeyValueObservingOptionNew = 0x01,
// 提供舊值
NSKeyValueObservingOptionOld = 0x02,
// 添加觀察者時立即發(fā)送一個通知給觀察者,
// 并且是在注冊觀察者方法返回之前
NSKeyValueObservingOptionInitial = 0x04,
// 如果指定,則在每次修改屬性時,會在修改通知被發(fā)送之前預(yù)先發(fā)送一條通知給觀察者,
// 這與-willChangeValueForKey:被觸發(fā)的時間是相對應(yīng)的。
// 這樣,在每次修改屬性時,實際上是會發(fā)送兩條通知。
NSKeyValueObservingOptionPrior = 0x08
};
typedef NSUInteger NSKeyValueObservingOptions;
// 選項值可以支持多個選項
注冊之后,我們要在觀察者內(nèi)部實現(xiàn)如下方法
// 此時,當(dāng)被觀察者的屬性發(fā)生變更,觀察者就會自動調(diào)用如下方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// keyPath被觀察的屬性值
NSLog(@"keyPath = %@",keyPath);
// object被觀察的對象
NSLog(@"object = %@",object);
// 被觀察屬性值得變化,后面還會講
NSLog(@"change = %@",change);
// 上下文,也可以是任意的額外數(shù)據(jù)
// 這個Context的作用十分重要,我在后面會強(qiáng)調(diào)
NSLog(@"context = %@",context);
}
// 我們通過這個方法,可以得到一些關(guān)鍵信息
Change選項,它記錄了被監(jiān)聽屬性的變化情況??梢酝ㄟ^key來獲取值:
// 屬性變化的類型,是一個NSNumber對象,包含NSKeyValueChange枚舉相關(guān)的值
NSString *const NSKeyValueChangeKindKey;
// 屬性的新值。當(dāng)NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加觀察的方法設(shè)置了NSKeyValueObservingOptionNew時,我們能獲取到屬性的新值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionNew時,則我們能獲取到一個NSArray對象,包含被插入的對象或
// 用于替換其它對象的對象。
NSString *const NSKeyValueChangeNewKey;
// 屬性的舊值。當(dāng)NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加觀察的方法設(shè)置了NSKeyValueObservingOptionOld時,我們能獲取到屬性的舊值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionOld時,則我們能獲取到一個NSArray對象,包含被移除的對象或
// 被替換的對象。
NSString *const NSKeyValueChangeOldKey;
// 如果NSKeyValueChangeKindKey的值是NSKeyValueChangeInsertion、NSKeyValueChangeRemoval
// 或者NSKeyValueChangeReplacement,則這個key對應(yīng)的值是一個NSIndexSet對象,
// 包含了被插入、移除或替換的對象的索引
NSString *const NSKeyValueChangeIndexesKey;
// 當(dāng)指定了NSKeyValueObservingOptionPrior選項時,在屬性被修改的通知發(fā)送前,
// 會先發(fā)送一條通知給觀察者。我們可以使用NSKeyValueChangeNotificationIsPriorKey
// 來獲取到通知是否是預(yù)先發(fā)送的,如果是,獲取到的值總是@(YES)
NSString *const NSKeyValueChangeNotificationIsPriorKey;
NSKeyValueChangeKindKey的值取自于NSKeyValueChange,這是一個枚舉值,定義如下
enum {
// 設(shè)置一個新值。被監(jiān)聽的屬性可以是一個對象,也可以是一對一關(guān)系的屬性或一對多關(guān)系的屬性。
NSKeyValueChangeSetting = 1,
// 表示一個對象被插入到一對多關(guān)系的屬性。
NSKeyValueChangeInsertion = 2,
// 表示一個對象被從一對多關(guān)系的屬性中移除。
NSKeyValueChangeRemoval = 3,
// 表示一個對象在一對多的關(guān)系的屬性中被替換
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
注意,觀察者在不需要使用的時候一定要移除,否則會產(chǎn)生崩潰
- (void)dealloc {
[self.dog removeObserver:self.p forKeyPath:@"name"];
}
通過上面簡要的代碼示例,我們可以得知,時運(yùn)觀察者只需要實現(xiàn)簡單的幾步。
- 注冊觀察者
- 觀察者實現(xiàn)相應(yīng)的方法
- 移除觀察者
2.KVC和KVO的實現(xiàn)原理
KVC和KVO是基于強(qiáng)大的Runtime來實現(xiàn)的。其中使用到的技術(shù)就是isa-swilling,isa-swilling這項技術(shù)也是一個重點,我們會在后續(xù)的 Runtime 部分會講到。如果有看到此處不明白的同學(xué)也請保持耐心。
網(wǎng)上有一篇文章針對實現(xiàn)原理寫的很好,鏈接在此。
整體來說就是,當(dāng)某個類的對象第一次被觀察時,系統(tǒng)會在運(yùn)行期間動態(tài)的為這個類創(chuàng)建一個派生類,假如被監(jiān)聽類名為ClassA,那么派生類的名稱就為NSKVONotifying_ClassA。
1.原有對象的isa指針會指向全新的派生類,派生類為了混淆,避免別人知道他不是原來的類,所以派生類重寫了Class的類方法。
2.同時重寫了Dealloc方法,用于資源的銷毀處理。
3.還重寫了_isKVOA,這個是一個標(biāo)記,用于標(biāo)示這個類是遵守KVO機(jī)制的。
4.最關(guān)鍵的是重寫了被監(jiān)聽屬性的Setter方法,這是實現(xiàn)KVO的關(guān)鍵。至于為什么,后面會講解到。
簡單的畫了張圖,可能會有助于理解。
[圖片上傳失敗...(image-74ea05-1602312865707)]
我們上面講重寫了被觀察對象屬性的Setter方法是十分關(guān)鍵的,這就要說起另外兩個十分重要的方法
// 在屬性值即將被修改的時候,會調(diào)用這個方法
- (void)willChangeValueForKey:(NSString *)key;
// 在屬性值已經(jīng)被修改的時候,會調(diào)用這個方法
- (void)didChangeValueForKey:(NSString *)key;
// didChangeValueForKey:方法會顯式的調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
}
其實我個人猜測,重寫Setter方法內(nèi)部應(yīng)該這樣實現(xiàn)的
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
說到這里,相信你應(yīng)該完整的明白KVO的實現(xiàn)機(jī)制了。
// 這才是KVO機(jī)制觸發(fā)的關(guān)鍵
- (void)didChangeValueForKey:(NSString *)key;
3.調(diào)用KVO的三種方法
綜合上面KVO的實現(xiàn)原理,我們可以得出如下結(jié)論:
1.使用了KVC
使用了 KVC ,如果有 訪問器方法 ,則運(yùn)行時會在訪問器方法中調(diào)用 will/didChangeValueForKey: 方法;
沒用訪問器方法,運(yùn)行時會在setValue:forKey方法中調(diào)用will/didChangeValueForKey:方法。
2.有訪問器方法
運(yùn)行時會重寫訪問器方法調(diào)用will/didChangeValueForKey:方法。
因此,直接調(diào)用訪問器方法改變屬性值時,KVO也能監(jiān)聽到。
3.直接調(diào)用
顯式調(diào)用will/didChangeValueForKey:方法。
4.KVO自動通知、手動通知
通常意義下我們使用的都是自動通知,注冊觀察者之后,當(dāng)觸發(fā)will/didChangeValueForKey:方法后,觀察者對象的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { }方法會被調(diào)用。
如果想實現(xiàn)手動通知,我們需要借助一個額外的方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
這個方法默認(rèn)返回YES,用來標(biāo)記Key指定的屬性是否支持KVO,如果返回值為NO,則需要我們手動更新。
我們還是用我們最上面的例子,監(jiān)聽Person的name屬性,不過這次我們采取手動通知的方式。
#import "Person.h"
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automaic = NO;
if ([key isEqualToString:@"name"])
{
automaic = NO;
}
else
{
// 此處需要注意,沒有被處理的其他屬性要調(diào)用父類的原有方法
automaic = [super automaticallyNotifiesObserversForKey:key];
}
return automaic;
}
@end
這樣我們就已經(jīng)標(biāo)記好當(dāng)Person的name屬性發(fā)生改變時,手動發(fā)送通知,代碼如下:
@implementation Person
- (void)setName:(NSString *)name {
if(name != _name)// 加一處判斷,如果值相同,就無需發(fā)送通知了
{
// 我們需要在值修改前調(diào)用`will...`方法
[self willChangeValueForKey:@"name"];
_name = name;
// 我們還需要在修改后調(diào)用`did...`方法,顯式調(diào)用觀察者的方法
[self didChangeValueForKey:@"name"];
}
}
@end
手動發(fā)送通知一對一的操作方法如上,如果是一對多的案例,則可以使用如下方法
- (void)willChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
- (void)didChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects
5.注冊依賴鍵(類似于 Vue 里面的計算屬性)
實際開發(fā)過程中可能會遇到這種場景,某個變量的值取決于其它的值。
我們還是看一個例子吧:
// 聲明一個Person類,有三個屬性
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,copy) NSString *fullName;
@property (nonatomic,copy) NSString *firstName;
@property (nonatomic,copy) NSString *lastName;
@end
// 其中 fullName 取決于 firstName 和 lastName的值.
// 同時如果 firstName 和 lastName發(fā)生改變的話,fullName也會受到影響。
#import "Person.h"
@implementation Person
// 注冊 fullName依賴于 firstName 和 lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName",@"lastName",nil];
}
- (NSString *)fullName {
NSString *tempName = _fullName;
if (_firstName || _lastName)
{
tempName = [NSString stringWithFormat:@"%@-%@",_firstName,_lastName];
}
return tempName;
}
回到Controller:
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [Person new];
self.p = p;
[self.p addObserver:self
forKeyPath:NSStringFromSelector(@selector(name))
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:ContextMark];
self.p.fullName = @"lilei";
NSLog(@"fullName = %@",self.p.fullName);
self.p.firstName = @"lala";
NSLog(@"fullName = %@",self.p.fullName);
self.p.lastName = @"papa";
NSLog(@"fullName = %@",self.p.fullName);
}
// 打印結(jié)果如下
fullName = lilei
fullName = lala-(null)
fullName = lala-papa
6.KVO使用中的"坑"
最近我在看這方面資料的時候,發(fā)現(xiàn)大家都以
tableView和ContentOffset作為例子。咱們就用這個最常見的控件來說明一下吧。
1.keyPath為字符串
眾所周知,KVO里面的KeyPath是NSString類型,結(jié)合Obj-C動態(tài)語言的特性,在編譯時是不做檢查的,只有運(yùn)行到執(zhí)行的時候,才會動態(tài)的去方法列表、實例變量列表中去查找,所以一旦我們寫錯了KeyPath,不運(yùn)行的時候很難發(fā)現(xiàn)。
基于這個問題,我們用以下的方法規(guī)避
// 這樣就不會寫錯了
NSStringFromSelector(@selector(contentSize))
2.多層繼承、共用同一個回調(diào)方法
假如父類的控制器監(jiān)聽了tableView的ContentOffset屬性,同時該控制器還監(jiān)聽了其他控件的一些屬性,但是同一個對象或者控制器作為多個對象屬性的觀察者,實際上最后調(diào)用的都是同一個回調(diào)方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { },這樣寫極其容易混淆,所以我們?yōu)榱私鉀Q這個問題,把代碼寫成如下的樣子
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"])
{
[self doSomethingWhenContentOffsetChanges];
}
}
但是光這樣寫是不全面的,因為當(dāng)前的這個類很可能有父類,并且它的父類可能綁定了一些其他的KVO,上面的代碼只有一個條件判斷,一旦不成立,此次KVO的觸發(fā)操作也就斷了。而當(dāng)前類無法捕捉的這個KVO事件很可能就在它的父類里,或者是父類的父類,上述操作,將這一鏈條截斷,所以正確的方法應(yīng)該如下:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (object == _tableView && [keyPath isEqualToString:@"contentOffset"])
{
[self doSomethingWhenContentOffsetChanges];
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
這樣做這一鏈條就完整的保留了。
3.觀察者的注銷
上面的方法做完之后還是有隱患的。我們知道KVO不用的時候是需要注銷的。我們知道當(dāng)你對同一個KVO注銷兩次的時候,系統(tǒng)默認(rèn)是拋出異常的。
你可能會好奇,什么時候我會對同一個Observer注銷多次呢?
這個時候我們可以想一下我們注銷Observer的時機(jī),是不是多在Dealloc方法中?
在Obj-C中,有很多系統(tǒng)的方法被重寫時需要調(diào)用super xxxxxxx等方法,這是Obj-C的繼承關(guān)系決定的。
例如:
// 在重寫init方法時,我們要調(diào)用一下父類的init方法
- (instancetype)init {
[super init];
}
// 布局子控件時,要調(diào)用一下父類的layoutSubviews方法
- (void)layoutSubviews {
[super layoutSubviews];
}
還有些方法,不需要調(diào)用父類的方法,自動就會幫你調(diào)用,就如我們所說的Dealloc。其實只有在ARC模式下才不需要調(diào)用父類,MRC下的Dealloc還是要手動調(diào)用super dealloc的。
所以我們在注銷觀察者的時候就這么寫
- (void)dealloc {
[_tableView removeObserver:self forKeyPath:@"contentOffset"];
}
假設(shè)我們有三個類 ClassA(父類),ClassB(子類),ClassC(孫子類)。這三個類都作為觀察者,觀察tableView的contentOffset屬性。
如果我們在ClassC(孫子類)的Dealloc方法中釋放觀察者
- (void)dealloc {
[_tableView removeObserver:self forKeyPath:@"contentOffset"];
}
當(dāng)ClassC(孫子類)的Dealloc執(zhí)行完畢后,就會自動去ClassB(子類)的Dealloc方法中,釋放觀察者
- (void)dealloc {
[_tableView removeObserver:self forKeyPath:@"contentOffset"];
}
這個時候就出現(xiàn)崩潰了,因為我們在前面提到過這樣會導(dǎo)致相同的removeObserver被執(zhí)行兩次,于是導(dǎo)致crash。
4.正確寫法
針對這種類型的Crash,我們就要談一下在注冊Observer似的一個關(guān)鍵的參數(shù)Context,之前我是不知道這個Context是做啥用的,對于KVO的使用只是流于表面,所以對于這個神秘的Context的作用一直沒有深究,現(xiàn)在我們將使用Context來為每一個Observer做區(qū)分,避免多次調(diào)用相同的removeObserver。
KVO的三個關(guān)鍵方法
// 注冊觀察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// 觀察者響應(yīng)方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
// 移除觀察者(有兩個方法)
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
相比細(xì)心的同學(xué)已經(jīng)看出來了,我們在注冊、響應(yīng)、移除的三個步驟里,都可以找到Context這個關(guān)鍵字。所以為了保持注冊、響應(yīng)、移除的一致性,正確的寫法應(yīng)該如下:
// 首先我們應(yīng)在使用KVO的類中,創(chuàng)建一個獨一無二的Context,用來和其他類進(jìn)行區(qū)分
static Void *ContextMark = &ContextMark;
// 接下來注冊的時候用
[_tableView addObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:ContextMark];
// 響應(yīng)的時候用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == ContextMark)
{
// do someThing
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// 注銷的時候用
- (void)dealloc {
[_tableView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
context:ContextMark];
}
如果還不放心,也可以使用@try @catch去捕獲異常
7.總結(jié)、
[圖片上傳失敗...(image-7a5c0a-1602312865707)]
KVO 這套 API 真麻煩~~~~~