KVO

KVO即key-value-observing,鍵值觀察,是一種觀察者模式的實現機制(另一種為Notification)。KVO提供了一種機制,指定一個被觀察對象(如student對象),當被觀察對象student的某個屬性(如name)發生改變時觀察者對象(如teacher)會收到通知,同時作出相應處理(這孩子,怎么能隨便改名呢,把你家長叫來)。

1.KVO的應用步驟
  • 注冊觀察者,實施監聽:
[student addObserver:teacher forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

參數說明:
1)student:被觀察者對象
2)teacher:觀察者對象,該對象必須實現
observeValueForKeyPath:ofObject:change:context: 方法
3)forKeyPath:被觀察者的屬性名,必須和被觀察者屬性名相同
4)options:屬性配置,有四個值:

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
//接收方法中傳入屬性變化后的新值,鍵為NSKeyValueChangeNewKey
NSKeyValueObservingOptionNew = 0x01,
//接收方法中傳入屬性變化前的舊值,鍵為NSKeyValueChangeOldKey
NSKeyValueObservingOptionOld = 0x02,
//注冊之后會立刻調用接收方法,可以在程序第一次運行時做一些初始化操作,
如果配置了NSKeyValueObservingOptionNew,change參數內容會包含新值
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
//接收方法會在屬性變化前后分別調用一次,變化前的通知change參數包含鍵值對:notificationIsPrior = 1。
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
}

5)context:接收一個C指針,可以為kvo的回調方法傳值。

  • 在回調方法處理屬性變化
    每當監聽的keypath發生改變就會調用該方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == &PrivateKVOContext) {
if ([keyPath isEqualToString:@"name"]) {
            NSString *oldName = change[NSKeyValueChangeOldKey];
            NSString *newName = change[NSKeyValueChangeNewKey];
      }
   }
}

參數說明:
1)keyPath:被監聽的keyPath , 用來區分不同的KVO監聽;
2)object:被觀察對象,可以獲得修改后的屬性值;
3)change:保存信息改變的字典(可能有舊的值,新的值等)

默認change參數會包含一個NSKeyValueChangeKindKey鍵值對,傳遞被監聽 屬性的變化類型:
enum {
//屬性被重新設置
NSKeyValueChangeSetting = 1,
//表示更改的是集合屬性,分別代表插入、刪除、替換操作
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

如果NSKeyValueChangeKindKey參數是針對集合屬性的三個之一,change 參數還會包含一個NSKeyValueChangeIndexesKey鍵值對,表示變化的index。
4)context:上下文,可以用來區分不同的KVO監聽。

  • 移除觀察者
    因為添加觀察者并不會retain,所以即使被觀察者被釋放了,其監聽信息仍舊存在,如果不將觀察者移除就會出現崩潰。
[student removeObserver:teacher forKeyPath:@"name" context:&PrivateKVOContext];
2.KVO的簡單應用實例

KVO的常用場景是在MVC中同步model和UI,實現這樣的需求:點擊view的時候更新model的(person)數據并觸發UI同步。可以看到應用KVO輕松的監聽到模型數據的變化,進而在回調中更新UI。


pic8-1.gif
@interface ViewController ()
- (IBAction)randomAge:(UIButton *)sender;
@property (weak, nonatomic) IBOutlet UILabel *ageLabel;
@property (strong, nonatomic) Person *person;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
//創建觀察者
[self.person addObserver:self
forKeyPath:@"myAge"
options:NSKeyValueObservingOptionNew
context:nil];

}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//UI同步
self.ageLabel.text = [NSString stringWithFormat:@"%@",change[NSKeyValueChangeNewKey]];
}

- (IBAction)randomAge:(UIButton *)sender {
//更新model數據
self.person.myAge = arc4random() % 100 ;
}

- (void)dealloc {
//移除觀察者
[self removeObserver:self forKeyPath:@"myAge"];
}
@end
3.手動鍵值觀察

在以上KVO的應用中通過創建觀察者,在屬性變化時就會自動發出通知,而有些場景需要人為的控制通知的發送,這需要重寫被觀察者對象屬性的getter/setter方法。

#import "Person.h"

@implementation Person
{
NSUInteger myAge;
}

- (NSUInteger)myAge {
return myAge;
}

- (void)setMyAge:(NSUInteger)newAge {
//發送通知:鍵值即將改變
[self willChangeValueForKey:@"myAge"];
myAge = newAge;
//發送通知:鍵值已經修改
[self didChangeValueForKey:@"myAge"];
}

//當設置鍵值之后,通過此方法,決定是否發送通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
//當key為myAge時,手動發送通知
if ([key isEqualToString:@"myAge"]) {
return NO;
}
//當為其他key時,自動發送通知
return [super automaticallyNotifiesObserversForKey:key];
}

@end
4.設置屬性之間的依賴

假設我要監聽一個color屬性的變化,而一個color又與三原色相關,每種原色又由不同的組分構成,如此我們就需要監聽N個原色屬性的變化,每個原色屬性變化就去設置color的值,我的天,太麻煩了,事情總是有解決的辦法:KVO給我們提供了這種鍵之間的依賴方法

+ (NSSet *)keyPathsForValuesAffecting<Key>;
//設置屬性依賴,屬性greenComponent,依賴于屬性lComponent和屬性aComponent
+ (NSSet *)keyPathsForValuesAffectingGreenComponent {
return [NSSet setWithObjects:@"lComponent", @"aComponent",nil];
}

+ (NSSet *)keyPathsForValuesAffectingRedComponent {
return [NSSet setWithObject:@"lComponent"];
}

+ (NSSet *)keyPathsForValuesAffectingBlueComponent {
return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingColor {
return [NSSet setWithObjects:@"redComponent", @"greenComponent",@"blueComponent", nil];
}

通過color的屬性依賴設置,在原色組分lComponent、aComponent、bComponent發生變化時觀察者仍能收到color變化的通知。

5.KVO機制

前面已經了解到KVO的基本用法,那么KVO底層是如何實現的呢?Let me think,既然KVO能夠監聽屬性的變化,那么在被觀察者屬性的set方法中判斷如果屬性值發生了變化就向觀察者發送通知就可以實現,似乎很簡單,但事實是KVO并沒有對被觀察者進行顯示地重寫set方法,那應該在哪重寫set方法呢?
蘋果通過isa混寫(isa-swizzling)來實現KVO。當創建觀察者觀察一個對象person時,KVO機制會動態的創建一個名為NSKVONotifying_Person的新類(繼承自person的本類Person)并將對象person的isa指針從Person類指向NSKVONotifying_Person(在這里只需要知道對象isa指針指向哪個類,對象就會到哪個類去尋找對應方法)。到這里我們應該找到了重寫set方法的地方,只要在NSKVONotifying_Person中重寫了Person的對應觀察屬性name的set方法,在被觀察對象person的屬性name被修改的時候會調用NSKVONotifying_Person中該屬性的set方法,在set方法中完成相應的通知工作從而實現了屬性變化的監聽。由此可以知道只有當屬性值是通過set方法修改的時KVO才有效,例如在Person中有這樣一個方法:

//不是通過set方法修改的,不會觸發KVO
- (void)changeName {
_name = @"newName";
}

KVO測試:

//輔助代碼
static NSArray *ClassMethodNames(Class c) {
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for (i = 0; i < methodCount; i++) {
[array addObject:NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}

static void PrintDescription(NSString *name, id obj) {
struct objc_object *objcet = (__bridge struct objc_object *)obj;
Class cls = objcet->isa;
NSString *str = [NSString stringWithFormat:@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s : super class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(cls),
class_getName(class_getSuperclass(cls)),
[ClassMethodNames(cls) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}

// 測試代碼
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];

[person2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
PrintDescription(@"person1", person1);
PrintDescription(@"person2", person2);
//輸出
person1: <Person: 0x60800002fec0>
NSObject class Person
libobjc class Person : super class NSObject
implements methods <.cxx_destruct, name, setName:>
person2: <Person: 0x60800002fee0>
NSObject class Person
libobjc class NSKVONotifying_Person : super class Person
implements methods <setName:, class, dealloc, _isKVOA>

可以看到被觀察者對像person2的類已經被改為NSKVONotifying_Person了,并且重寫了setName方法(在set方法里實現通知)。此外當我們試著打印person2對象的類([person2 class])時,仍然是Person。所以在NSKVONotifying_Person中除了重寫相應的setter,還重寫了class方法,讓它返回原先的類。

PS: I am xinghun who is on the road.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文結構如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會閱讀 1,663評論 1 21
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,045評論 0 26
  • 本文由我們團隊的 糾結倫 童鞋撰寫。 文章結構如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識小集閱讀 7,427評論 7 105
  • 一、概述 KVO,即:Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,128評論 11 33
  • 1.KVC 關于 KVC 和 KVO ,我之前的總結文章有寫過,但是趨于表面,沒有探究其內部真正的實現原理和進階用...
    Liberalism閱讀 1,112評論 0 5