iOS 通知、KVC、KVO 原理和实现机制剖析

一、通知

通知是一种一对多的信息广播机制,与 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 赋值和取值查找的方法和优先级。

通过试验得出以下结论:

  1. 赋值(setValue: forKey:):查找顺序:
    第一步:setTest: -> _setTest: -> setIsTest:。而_setIsTest:没有任何作用前三个都没有也不会走。
    第二步:+ (BOOL)accessInstanceVariablesDirectly 方法如果返回 YES 则会赋值 _test->_isTest->test->isTest实例变量,如果为 NO,到第三步。
    第三步:找不到会走- (void)setValue: forUndefinedKey:方法,抛出异常,我们可以重写该方法使其不去抛出异常。一般配合 2 中的第三步使用。

KVC Set 查找原理

  1. 取值(valueForKey: ):查找顺序是:
    第一步:getTest: -> test: -> isTest: -> _getTest: -> _test:。而_isTest:没有任何作用前五个都没有也不会走。
    第二步:+ (BOOL)accessInstanceVariablesDirectly 方法如果返回 YES 则会取值 _test->_isTest->test->isTest实例变量,如果为 NO,到第三步。
    第三步:找不到会走- (id)valueForUndefinedKey:方法,抛出异常,我们可以重写该方法返回 nil 使其不去抛出异常。

KVC Get 查找原理.jpg

理解了 KVC 的原理我们就可以很容易理解我们在开发中常用的解析 json 后为 mode 批量赋值的方法。也明白为什么要在 BaseModel 中重写 - (void)setValue: forUndefinedKey:- (id)valueForUndefinedKey: 方法了。

// KVC 的批量取值和赋值
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)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_KVOTestObjectKVOTestObject的子类。

当我们为一个类 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<NSKeyValueChangeKey,id> *)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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358

推荐阅读更多精彩内容