一芋簿、通知
通知是一種一對(duì)多的信息廣播機(jī)制,與 delegate 和 block 的區(qū)別是:通知是一對(duì)多傳遞璃饱,delegate 和 block 是一對(duì)一的傳遞与斤。
由于 OC 的閉源,我們無法確切的知道通知具體的實(shí)現(xiàn)機(jī)制,但是如果是我們自己會(huì)怎樣實(shí)現(xiàn)通知的機(jī)制呢撩穿?下面是我的看法:
我們?cè)谔砑右粋€(gè)通知時(shí)往往會(huì)在一開始調(diào)用 [NSNotificationCenter.defaultCenter addObserver: selector: name: object:]
方法這時(shí)會(huì)發(fā)生什么磷支?
// 一個(gè)全局的 manager 來管理通知的接收和發(fā)送機(jī)制
class NotificationManager {
static NotificationHashMap *_map;
};
// 一個(gè)哈希表,通過哈希算法查以通知 name 作為鍵值來查找通知的接收對(duì)象和執(zhí)行方法
struct NotificationHashMap {
struct NotificationMap * _Nonnull * _Nonnull _map;
};
// name: 通知名稱
// value: 可以看成一個(gè)二維數(shù)組食寡,里面一層存放的是
// 1.接收對(duì)象(observer) 2.執(zhí)行方法(sel) 3.對(duì)象(object)
struct NotificationMap {
const char * _Nonnull name;
struct NotificationValueList * _Nonnull * _Nonnull value;
};
// observer: 接收對(duì)象
// sel: 執(zhí)行方法
// object: 對(duì)象
struct NotificationValueList {
id observer;
SEL sel;
id object;
};
首先系統(tǒng)會(huì)創(chuàng)建一個(gè) NotificationValueList
實(shí)例雾狈,通過 NotificationManager
和 name 查找到 NotificationHashMap
中對(duì)應(yīng)的 value,將剛才創(chuàng)建的 NotificationValueList
實(shí)例添加到 value 中抵皱。這里查找 value 的方式為哈希算法善榛,value 類似一個(gè)集合,不會(huì)添加相同的實(shí)例(observer, sel, object
都相同的不會(huì)再次添加)
在這里我們會(huì)將 observer 和 object 兩個(gè)對(duì)象添加到全局變量中呻畸,但是系統(tǒng)不會(huì)為它們的引用計(jì)數(shù)加 1移盆。否則對(duì)象將無法被釋放。這也是為什么在對(duì)象的 - dealloc 中我們要調(diào)用 [NSNotificationCenter.defaultCenter removeObserver: name: object:]
方法伤为。因?yàn)槲覀冃枰?name 對(duì)應(yīng)的 value 中的相應(yīng)NotificationValueList
實(shí)例刪除咒循。
我們發(fā)送通知的時(shí)候會(huì)調(diào)用這個(gè)方法:[NSNotificationCenter.defaultCenter postNotificationName: object: userInfo:]
,它會(huì)通過 name 找到 NotificationHashMap
中的 value绞愚,然后遍歷 value 集合叙甸,判斷每個(gè)NotificationValueList
實(shí)例的 object 是否與發(fā)送消息方法中的 object 參數(shù)相同,如果相同位衩,就會(huì)找到該實(shí)例的 observer蚁署,調(diào)用 sel 方法,如果 sel 方法帶有 NSNotification 參數(shù)蚂四,就把 userInfo 傳遞過去光戈。
以上就是我自己對(duì)通知實(shí)現(xiàn)機(jī)制的理解,系統(tǒng)肯定不是這樣實(shí)現(xiàn)的遂赠,但是我們不得而知久妆。
二、KVC
KVC 全稱 Key Valued Coding(鍵值編碼)跷睦,是基于 NSKeyValueCoding 非正式協(xié)議實(shí)現(xiàn)的機(jī)制筷弦,它可以在運(yùn)行時(shí)通過 key 值對(duì)對(duì)象的屬性動(dòng)態(tài)的進(jìn)行存取操作。
主要方法有:
valueForKey:
setValue: forKey:
原理探究
為了探究 KVC 的系統(tǒng)實(shí)現(xiàn)機(jī)制請(qǐng)看下面代碼:
@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"];
}
結(jié)論
通過上面的代碼抑诸,每次把走的 set 和 get 方法注釋掉烂琴,可以知道 KVC 賦值和取值查找的方法和優(yōu)先級(jí)。
通過試驗(yàn)得出以下結(jié)論:
- 賦值(setValue: forKey:):查找順序:
第一步:setTest: -> _setTest: -> setIsTest:
蜕乡。而_setIsTest:
沒有任何作用前三個(gè)都沒有也不會(huì)走奸绷。
第二步:+ (BOOL)accessInstanceVariablesDirectly
方法如果返回 YES 則會(huì)賦值_test->_isTest->test->isTest
實(shí)例變量,如果為 NO层玲,到第三步号醉。
第三步:找不到會(huì)走- (void)setValue: forUndefinedKey:
方法反症,拋出異常,我們可以重寫該方法使其不去拋出異常畔派。一般配合 2 中的第三步使用铅碍。
- 取值(valueForKey: ):查找順序是:
第一步:getTest: -> test: -> isTest: -> _getTest: -> _test:
。而_isTest:
沒有任何作用前五個(gè)都沒有也不會(huì)走线椰。
第二步:+ (BOOL)accessInstanceVariablesDirectly
方法如果返回 YES 則會(huì)取值_test->_isTest->test->isTest
實(shí)例變量胞谈,如果為 NO,到第三步憨愉。
第三步:找不到會(huì)走- (id)valueForUndefinedKey:
方法烦绳,拋出異常,我們可以重寫該方法返回 nil 使其不去拋出異常莱衩。
理解了 KVC 的原理我們就可以很容易理解我們?cè)陂_發(fā)中常用的解析 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 的簡(jiǎn)寫,它是 OC 系統(tǒng)實(shí)現(xiàn)觀察者模式的方式趟庄。當(dāng)指定對(duì)象的屬性被修改括细,就會(huì)通知觀察者,告訴觀察者相應(yīng)對(duì)象的相應(yīng)屬性被修改戚啥。
系統(tǒng)如何實(shí)現(xiàn) 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:
方法做了什么,看上面的代碼猫十,兩條打印日志是什么览濒?結(jié)果是:
KVOTestObject
NSKVONotifying_KVOTestObject
我們可以看到在添加鍵值觀察之前,kvoObj 的類是 KVOTestObject
拖云,這和我們的定義一樣贷笛,但是添加鍵值觀察后,kvoObjc 的類變成了 NSKVONotifying_KVOTestObject
宙项。其實(shí)NSKVONotifying_KVOTestObject
是 KVOTestObject
的子類乏苦。
當(dāng)我們?yōu)橐粋€(gè)類 A 添加一個(gè)鍵值觀察時(shí),系統(tǒng)會(huì)自動(dòng)創(chuàng)建一個(gè) NSKVONotifying_A 類尤筐,繼承類 A汇荐,然后 將kvoObjc 的 isa 指針指向 NSKVONotifying_A。 通過 重寫鍵值屬性的 set 方法 的形式來實(shí)現(xiàn) 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]);
}
}
請(qǐng)看上面的代碼會(huì)打印幾條日志?
newKey:1
答案是只會(huì)打印一條日志腺劣,因?yàn)?KVO 實(shí)現(xiàn)的機(jī)制是重寫 set 方法曾棕,而 - changeTest:
方法直接為實(shí)例變量賦值,沒有走 set 方法繁疤,故而不會(huì)響應(yīng) KVO咖为。
我們可以通過手動(dòng) KVO 的形式強(qiáng)行通知觀察者響應(yīng) - observeValueForKeyPath ofObject: change: context
方法,方式如下:
@implementation KVOTestObject
- (void)changeTest:(NSString *)test {
[self willChangeValueForKey:@"test"];
_test = test.copy;
[self didChangeValueForKey:@"test"];
}
@end
打印日志變?yōu)椋?/p>
newKey:1
newKey:2