一、通知
通知是一种一对多的信息广播机制,与 delegate 和 block 的区别是:通知是一对多传递,delegate 和 block 是一对一的传递。
由于 OC 的闭源,我们无法确切的知道通知具体的实现机制,但是如果是我们自己会怎样实现通知的机制呢?下面是我的看法:
我们在添加一个通知时往往会在一开始调用 [NSNotificationCenter.defaultCenter addObserver: selector: name: object:]
方法这时会发生什么?
// 一个全局的 manager 来管理通知的接收和发送机制
class NotificationManager {
static NotificationHashMap *_map;
};
// 一个哈希表,通过哈希算法查以通知 name 作为键值来查找通知的接收对象和执行方法
struct NotificationHashMap {
struct NotificationMap * _Nonnull * _Nonnull _map;
};
// name: 通知名称
// value: 可以看成一个二维数组,里面一层存放的是
// 1.接收对象(observer) 2.执行方法(sel) 3.对象(object)
struct NotificationMap {
const char * _Nonnull name;
struct NotificationValueList * _Nonnull * _Nonnull value;
};
// observer: 接收对象
// sel: 执行方法
// object: 对象
struct NotificationValueList {
id observer;
SEL sel;
id object;
};
首先系统会创建一个 NotificationValueList
实例,通过 NotificationManager
和 name 查找到 NotificationHashMap
中对应的 value,将刚才创建的 NotificationValueList
实例添加到 value 中。这里查找 value 的方式为哈希算法,value 类似一个集合,不会添加相同的实例(observer, sel, object
都相同的不会再次添加)
在这里我们会将 observer 和 object 两个对象添加到全局变量中,但是系统不会为它们的引用计数加 1。否则对象将无法被释放。这也是为什么在对象的 - dealloc 中我们要调用 [NSNotificationCenter.defaultCenter removeObserver: name: object:]
方法。因为我们需要把 name 对应的 value 中的相应NotificationValueList
实例删除。
我们发送通知的时候会调用这个方法:[NSNotificationCenter.defaultCenter postNotificationName: object: userInfo:]
,它会通过 name 找到 NotificationHashMap
中的 value,然后遍历 value 集合,判断每个NotificationValueList
实例的 object 是否与发送消息方法中的 object 参数相同,如果相同,就会找到该实例的 observer,调用 sel 方法,如果 sel 方法带有 NSNotification 参数,就把 userInfo 传递过去。
以上就是我自己对通知实现机制的理解,系统肯定不是这样实现的,但是我们不得而知。
二、KVC
KVC 全称 Key Valued Coding(键值编码),是基于 NSKeyValueCoding 非正式协议实现的机制,它可以在运行时通过 key 值对对象的属性动态的进行存取操作。
主要方法有:
valueForKey:
setValue: forKey:
原理探究
为了探究 KVC 的系统实现机制请看下面代码:
@interface KVCTestObject : NSObject
@end
@implementation KVCTestObject {
NSString *_test;
NSString *_isTest;
NSString *test;
NSString *isTest;
}
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
#pragma mark - 重写 KVC 方法
- (void)setValue:(id)value forKey:(NSString *)key {
[super setValue:value forKey:key];
if (![key isEqualToString:@"test"]) {
return;
}
NSLog(@"_test: %@, test: %@, _isTest: %@, isTest: %@", _test, test, _isTest, isTest);
}
- (id)valueForKey:(NSString *)key {
id value = [super valueForKey:key];
if (![key isEqualToString:@"test"]) {
return value;
}
NSLog(@"_test: %@, test: %@, _isTest: %@, isTest: %@", _test, test, _isTest, isTest);
return value;
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"valueForUndefinedKey:");
return nil;
// return [super valueForUndefinedKey:key];
}
#pragma mark - set
- (void)setTest:(NSString *)test {
NSLog(@"setTest:");
}
- (void)_setTest:(NSString *)test {
NSLog(@"_setTest:");
}
- (void)setIsTest:(NSString *)isTest {
NSLog(@"setIsTest:");
}
- (void)_setIsTest:(NSString *)isTest {
NSLog(@"_setIsTest:");
}
#pragma mark - get
- (NSString *)getTest {
NSLog(@"getTest");
return @"";
}
- (NSString *)test {
NSLog(@"test");
return @"";
}
- (NSString *)isTest {
NSLog(@"isTest");
return @"";
}
- (NSString *)_getTest {
NSLog(@"_getTest");
return @"";
}
- (NSString *)_test {
NSLog(@"_test");
return @"";
}
- (NSString *)_isTest {
NSLog(@"_isTest");
return @"";
}
- (void)viewDidLoad {
[super viewDidLoad];
KVCTestObject *kvc = [KVCTestObject new];
[kvc setValue:@"2" forKey:@"test"];
[kvc valueForKey:@"test"];
}
结论
通过上面的代码,每次把走的 set 和 get 方法注释掉,可以知道 KVC 赋值和取值查找的方法和优先级。
通过试验得出以下结论:
- 赋值(setValue: forKey:):查找顺序:
第一步:setTest: -> _setTest: -> setIsTest:
。而_setIsTest:
没有任何作用前三个都没有也不会走。
第二步:+ (BOOL)accessInstanceVariablesDirectly
方法如果返回 YES 则会赋值_test->_isTest->test->isTest
实例变量,如果为 NO,到第三步。
第三步:找不到会走- (void)setValue: forUndefinedKey:
方法,抛出异常,我们可以重写该方法使其不去抛出异常。一般配合 2 中的第三步使用。
- 取值(valueForKey: ):查找顺序是:
第一步:getTest: -> test: -> isTest: -> _getTest: -> _test:
。而_isTest:
没有任何作用前五个都没有也不会走。
第二步:+ (BOOL)accessInstanceVariablesDirectly
方法如果返回 YES 则会取值_test->_isTest->test->isTest
实例变量,如果为 NO,到第三步。
第三步:找不到会走- (id)valueForUndefinedKey:
方法,抛出异常,我们可以重写该方法返回 nil 使其不去抛出异常。
理解了 KVC 的原理我们就可以很容易理解我们在开发中常用的解析 json 后为 mode 批量赋值的方法。也明白为什么要在 BaseModel 中重写 - (void)setValue: forUndefinedKey:
和 - (id)valueForUndefinedKey:
方法了。
// KVC 的批量取值和赋值
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
三、KVO
KVO 是 Key-Value Observing 的简写,它是 OC 系统实现观察者模式的方式。当指定对象的属性被修改,就会通知观察者,告诉观察者相应对象的相应属性被修改。
系统如何实现 KVO?
@interface KVOTestObject : NSObject
@property (nonatomic, copy) NSString *test;
@end
- (void)viewDidLoad {
[super viewDidLoad];
KVOTestObject *kvoObj = [KVOTestObject new];
NSLog(@"%s", class_getName(object_getClass(kvoObj)));
[kvoObj addObserver:self forKeyPath:@"test" options:(NSKeyValueObservingOptionNew) context:nil];
NSLog(@"%s", class_getName(object_getClass(kvoObj)));
}
我们看看 - addObserver:self forKeyPath: options: context:
方法做了什么,看上面的代码,两条打印日志是什么?结果是:
KVOTestObject
NSKVONotifying_KVOTestObject
我们可以看到在添加键值观察之前,kvoObj 的类是 KVOTestObject
,这和我们的定义一样,但是添加键值观察后,kvoObjc 的类变成了 NSKVONotifying_KVOTestObject
。其实NSKVONotifying_KVOTestObject
是 KVOTestObject
的子类。
当我们为一个类 A 添加一个键值观察时,系统会自动创建一个 NSKVONotifying_A 类,继承类 A,然后 将kvoObjc 的 isa 指针指向 NSKVONotifying_A。 通过 重写键值属性的 set 方法 的形式来实现 KVO 观察者模式
@interface KVOTestObject : NSObject
@property (nonatomic, copy) NSString *test;
- (void)changeTest:(NSString *)test;
@end
@implementation KVOTestObject
- (void)changeTest:(NSString *)test {
_test = test.copy;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
KVOTestObject *kvoObj = [KVOTestObject new];
[kvoObj addObserver:self forKeyPath:@"test" options:(NSKeyValueObservingOptionNew) context:nil];
kvoObj.test = @"1";
[kvoObj changeTest:@"2"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"test"] && [object isKindOfClass:KVOTestObject.class]) {
NSLog(@"newKey:%@", change[NSKeyValueChangeNewKey]);
}
}
请看上面的代码会打印几条日志?
newKey:1
答案是只会打印一条日志,因为 KVO 实现的机制是重写 set 方法,而 - changeTest:
方法直接为实例变量赋值,没有走 set 方法,故而不会响应 KVO。
我们可以通过手动 KVO 的形式强行通知观察者响应 - observeValueForKeyPath ofObject: change: context
方法,方式如下:
@implementation KVOTestObject
- (void)changeTest:(NSString *)test {
[self willChangeValueForKey:@"test"];
_test = test.copy;
[self didChangeValueForKey:@"test"];
}
@end
打印日志变为:
newKey:1
newKey:2