1. 使用
1.1 自动通知
// 调用set方法
[account setName:@"Savings"];
// 使用KVC forKey或forKeyPath
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];
// 使用 mutableArrayValueForKey: 检索关系代理对象
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
示例
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) NSMutableArray<Person *> *people;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 非集合
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.person.name = @"Tom";
[self.person setValue:@"Jerry" forKey:@"name"];
[self setValue:@"Tom" forKeyPath:@"person.name"];
// 集合
self.people = [NSMutableArray array];
Person *person0 = [[Person alloc] init];
person0.name = @"Tom";
[self.people addObject:person0];
Person *person1 = [[Person alloc] init];
person1.name = @"Jerry";
[self.people addObject:person1];
NSString *key = @"people";
[self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
Person *person2 = [[Person alloc] init];
person2.name = @"Frank";
NSMutableArray *people = [self mutableArrayValueForKey:key];
[people addObject:person2];
NSLog(@"People: \n%@", self.people);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"new name: %@", change[NSKeyValueChangeNewKey]);
} else if ([keyPath isEqualToString:@"people"]) {
NSLog(@"new array: %@", change[NSKeyValueChangeNewKey]);
NSArray<Person *> *people = change[NSKeyValueChangeNewKey];
NSLog(@"new person: %@", people.firstObject.name);
}
}
@end
// 输出
new name: Tom
new name: Jerry
new name: Tom
new array: (
"<Person: 0x60000276cc20>"
)
new person: Frank
People:
(
"Person name: Tom",
"Person name: Jerry",
"Person name: Frank"
)
1.2 手动通知
手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。想要使用手动通知必须实现automaticallyNotifiesObserversForKey:
(或者automaticallyNotifiesObserversOf<Key>
)方法。在一个类中同时使用自动和手动通知是可行的。对于想要手动通知的属性,可以根据它的keyPath返回NO,而其对于其他位置的keyPath,要返回父类的这个方法。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
// 或者
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
一对一关系
- (void)setOpeningBalance:(double)theBalance {
if (theBalance != _openingBalance) {
[self willChangeValueForKey:@"openingBalance"];
_openingBalance = theBalance;
[self didChangeValueForKey:@"openingBalance"];
}
}
如果一个操作会导致多个属性改变,需要嵌套通知:
- (void)setOpeningBalance:(double)theBalance {
[self willChangeValueForKey:@"openingBalance"];
[self willChangeValueForKey:@"itemChanged"];
_openingBalance = theBalance;
_itemChanged = _itemChanged + 1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"openingBalance"];
}
一对多的关系
必须注意不仅仅是这个key改变了,还有它改变的类型以及索引。
- (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"];
}
1.3 键之间的依赖
在很多种情况下一个属性的值依赖于在其他对象中的属性。如果一个依赖属性的值改变了,这个属性也需要被通知到。
一对一关系
@interface Person : NSObject
@property (nonatomic, strong, nullable) NSString *firstName;
@property (nonatomic, strong, nullable) NSString *lastName;
@property (nonatomic, strong, readonly) NSString *fullName;
@end
可以重写 keyPathsForValuesAffectingValueForKey:
方法。也可以通过实现 keyPathsForValuesAffecting<Key>
方法来达到前面同样的效果,这里的<Key>
就是属性名,不过第一个字母要大写。
@implementation Person
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
// 或者
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
@end
一对多关系
keyPathsForValuesAffectingValueForKey:
方法不能支持一对多关系。
举个例子,比如你有一个Department
对象,和很多个Employee
对象。而Employee
有一个salary
属性。你可能希望Department
对象有一个totalSalary
的属性,依赖于所有的Employee
的salary
。
注册Department
成为所有Employee
的观察者。当Employee
被添加或者被移除时进行计算。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
else
// deal with other observations and/or invoke super...
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
2. 实现细节
2.1 isa-swizzling
KVO的实现用了一种叫 isa-swizzling
的技术。
当一个对象的一个属性注册了观察者后,被观察对象的isa
指针的就指向了一个系统为我们生成的中间类,而不是我们自己创建的类。在这个类中,系统为我们重写了被观察属性的setter方法。
通过 object_getClass(id obj)
方法可以获得实例对象真实的类(isa
指针的指向)。
@interface Person : NSObject
@property (nonatomic, strong, nullable) NSString *name;
@end
@implementation Person
@end
@interface ViewController ()
@property (nonatomic, strong) Person *p1;
@property (nonatomic, strong) Person *p2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.p1 = [[Person alloc] init];
self.p2 = [[Person alloc] init];
self.p1.name = @"Tom";
NSLog(@"before kvo --- p2: %s", object_getClassName(self.p2));
[self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"after kvo --- p2: %s", object_getClassName(self.p2));
self.p2.name = @"Jerry";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"new name: %@", change[NSKeyValueChangeNewKey]);
}
}
@end
// 输出
before kvo --- p2: Person
after kvo --- p2: NSKVONotifying_Person
new name: Jerry
我们在p2
实例对象被键值观察的前后打印其isa
指针(实际使用的类)。
从结果中我们可以看到isa
指针指向了一个中间类NSKVONotifying_Person
。
苹果的KVO中间类的命名规则是在类名前添加NSKVONotifying_
,如果我们的类叫Son
KVO之后的中间类为NSKVONotifying_Son
。
2.2 IMP
我们再看一下KVO前后的函数方法的地址是否一样。
- (void)viewDidLoad {
[super viewDidLoad];
self.p1 = [[Person alloc] init];
self.p2 = [[Person alloc] init];
self.p1.name = @"Tom";
NSLog(@"before kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
[self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@" after kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
self.p2.name = @"Jerry";
}
// 输出
before kvo --- p1: 0x10ccee670 p2: 0x10ccee670
after kvo --- p1: 0x10ccee670 p2: 0x7fff258e454b
我们看到监听之间两个实例对象的setName:
方法的函数地址相同,KVO之后p2
实例对象的setName:
方法地址变了。
我们可以查看一下这个方法地址:
(lldb) image lookup -a 0x7fff258e454b
Address: Foundation[0x000000000006954b] (Foundation.__TEXT.__text + 422667)
Summary: Foundation`_NSSetObjectValueAndNotify
这个是Foundation
框架中的一个私有方法_NSSetObjectValueAndNotify
。
3. 自定义KVO
下面我们根据KVO的实现细节,仿写一个简化版的KVO。
NSString *ObserverKey = @"SetterMethodKey";
// 根据方法名获取Key
NSString *getKeyForSetter(NSString *setter) {
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *key = [setter substringWithRange:range];
NSString *letter = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:letter];
return key;
}
// 实现一个setter和通知函数
void _MySetObjectValueAndNotify(id self, SEL selector, NSString *name) {
// 1.调用父类的方法
struct objc_super superClass = {
self,
class_getSuperclass([self class])
};
objc_msgSendSuper(&superClass, selector, name);
// 2.通知观察者
NSObject *observer = objc_getAssociatedObject(self, &ObserverKey);
NSString *selectorName = NSStringFromSelector(selector);
NSString *key = getKeyForSetter(selectorName);
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{NSKeyValueChangeNewKey: name}, nil);
}
@implementation Person
- (void)snx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
// 1.创建一个子类
NSString *oldName = NSStringFromClass([self class]);
NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
objc_registerClassPair(customClass);
// 2.修改修改isa指针
object_setClass(self, customClass);
// 3.重写set方法
NSString *selectorName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
SEL sel = NSSelectorFromString(selectorName);
class_addMethod(customClass, sel, (IMP)_MySetObjectValueAndNotify, "v@:@");
// 4.绑定观察者
objc_setAssociatedObject(self, &ObserverKey, observer, OBJC_ASSOCIATION_ASSIGN);
}
@end
重要
使用objc_msgSendSuper时,可能编译器会报错:
Too many arguments to function call, expected 0, have 3
解决办法:在Build Setting修改Enable Strict Checking of objc_msgSend Calls为No。
- (void)viewDidLoad {
[super viewDidLoad];
self.p1 = [[Person alloc] init];
self.p2 = [[Person alloc] init];
self.p1.name = @"Tom";
NSLog(@"before kvo --- p2: %s", object_getClassName(self.p2));
NSLog(@"before kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
// [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[self.p2 snx_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"after kvo --- p2: %s", object_getClassName(self.p2));
NSLog(@"after kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
self.p2.name = @"Jerry";
}
// 输出
before kvo --- p2: Person
before kvo --- p1: 0x103514460 p2: 0x103514460
after kvo --- p2: CustomKVO_Person
after kvo --- p1: 0x103514460 p2: 0x103513f90
new name: Jerry
网友评论