KVO
和KVC
, 機器底層是如何實現的KVO
的全稱是Key-Value Observing
,俗稱鍵值監聽,能夠用於監聽某個對象屬性值的改變KVO
是使用獲取其餘對象的特定屬性變化的通知機制,控制器層的綁定技術就是嚴重依賴鍵值觀察得到模型層和控制器層的變化通知的KVC
和KVO
都是基於OC
的動態特性和Runtime
機制的以下所示, 咱們爲person
對象添加一個監聽html
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
self.person.age = 10;
// 給person添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.age = 10;
}
// 當監聽的對象發生改變時就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
}
複製代碼
上面添加監聽的方法微信
addObserver:forKeyPath:options:context:
監聽方法各個參數的做用分別是什麼
[object addObserver: observer forKeyPath: @"frame" options: 0 context: nil];
/** object: 被觀察者 observer: 觀察者 KeyPath: 被觀察者索貝觀察的屬性 options: 有四個值 一、NSKeyValueObservingOptionNew 把更改以前的值提供給處理方法 二、NSKeyValueObservingOptionOld 把更改以後的值提供給處理方法 三、NSKeyValueObservingOptionInitial 把初始化的值提供給處理方法,一旦注 冊,立馬就會調用一次。一般它會帶有新值,而不會帶有舊值。 四、NSKeyValueObservingOptionPrior 分2次調用。在值改變以前和值改變以後。 context:上下文,能夠帶一些參數,任何類型均可以 */
複製代碼
當被監聽的對象的屬性發生改變時就會調用下面的方法測試
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
}
/* 1. keyPath: 被監聽的屬性 2. object: 被監聽的對象 3. change 屬性變化字典(新/舊) 4. 上下文,與監聽的時候傳遞的一致 */
複製代碼
這裏咱們建立兩個pweson
對象, 可是隻對person1
實行監聽ui
self.person1 = [[Person alloc]init];
self.person2 = [[Person alloc]init];
self.person1.age = 10;
self.person2.age = 10;
// 給person添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
複製代碼
下面咱們能夠在touchesBegan
方法中分別添加斷點打印兩個對象的isa
, 以下編碼
pweson2
對象的isa
依然是Person
, 可是添加KVO
監聽的person1
的isa
變成了NSKVONotifying_Person
NSKVONotifying_Person
這個類是由Runtime
在運行狀態下動態建立的一個類, 是Person
的一個子類age
屬性進行賦值操做的時候, 其實調用的是Person
類的setAge
方法
person1
經過isa
找到其對應的類對象Person
類, 並調用Person
類的setAge
方法person2
經過isa
找到其對應的類對象NSKVONotifying_Person
類, 並調用NSKVONotifying_Person
類的setAge
方法setAge
方法的實現是不同的, 後面會詳解Person
和NSKVONotifying_Person
對應的類對象以下所示使用了KVO
監聽的對象動態生成的NSKVONotifying_Person
類atom
實際上NSKVONotifying_Person
類中的setAge:
方法內部是調用了Foundation
的_NSSetIntValueAndNotify
方法, 有興趣的能夠反編譯一下Foundation.framwork
的源碼, 查看其僞代碼, 大體的能夠推出內部方法的實現, 代碼大體以下spa
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 僞代碼
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知監聽器,某某屬性值發生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
複製代碼
_NSSetIntValueAndNotify
其實重寫了willChangeValueForKey
和didChangeValueForKey
兩個方法didChangeValueForKey
方法中實現的首先咱們在
Person
類內部重寫willChangeValueForKey
和didChangeValueForKey
兩個方法, 在運行的過程當中分別加斷點進行調試, 以下調試
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
複製代碼
而後在以下代碼中加斷點code
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
複製代碼
在輸出結果中能夠看到代碼的執行順序, 從下面的代碼能夠看出監聽屬性的改變實際上是在didChangeValueForKey
方法中實現的cdn
setAge:
didChangeValueForKey - begin
監聽到<MJPerson: 0x60000389b680>的age屬性值改變了
didChangeValueForKey - end
複製代碼
KVC
全稱是Key Value Coding
(鍵值編碼),是一個基於NSKeyValueCoding
非正式協議實現的機制,它能夠直接經過key
值對對象的屬性進行存取操做,而不需經過調用明確的存取方法KVC
提供了一種間接訪問屬性方法或成員變量的機制,能夠經過字符串來訪問對象的的屬性方法或成員變量// 通用的訪問方法
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
// 衍生的keyPath方法, 用來進行深層訪問(key使用點語法),也可單層訪問:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKeyPath:(NSString *)keyPath;
複製代碼
通用訪問方法使用示例
// 使用示例
Person *person = [[Person alloc] init];
// 賦值
[person setValue:@"titan" forKey:@"name"];
// 取值
NSLog(@"-------name = %@",person.name);
NSLog(@"-------name = %@",[person valueForKey:@"name"]);
複製代碼
keyPath
方法使用示例
//注意,這裏要想使用keypath對adress的屬性進行賦值,必須先給myself賦一個Address對象
Address *myAddress = [[Address alloc] init];
[myself setValue:myAddress forKey:@"address"];
//KeyPath爲多級訪問
[myself setValue:@"rizhao" forKeyPath:@"address.city"];
//取值
NSLog(@"-------city = %@",myself.address.city);
NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);
複製代碼
setValue:forKey:
0. 咱們先建立一個Person
類, 並在Person.h
文件中聲明一個age
屬性, 以下
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
複製代碼
下面咱們在ViewController.m
裏面調用一下看看
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
// 這種方式調用的是setAge方法
person.age = 10;
// 內部實際上是調用的setAge方法
[person setValue:@20 forKey:@"age"];
NSLog(@"%d", person.age);
// 打印結果20
}
複製代碼
Person.h
文件中沒有聲明age
屬性,也就是在Person.m
文件中沒有默認生成的setAge
和getAge
方法setValue
方法對age
存值的時候就會致使程序崩潰, 並會報出setValue:forUndefinedKey:]
的錯誤setValue:forKey:
的原理實際上就是先按照setAge:
和_setAge:
順序查找方法, 若是找到了對應方法中的一個, 則代碼能夠執行成功, 下面咱們就一個個驗證一下吧1. 驗證setKey
和_setKey
方法, 代碼以下
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
// .h文件中不添加age屬性
//@property (assign, nonatomic) int age;
@end
複製代碼
在.m
文件中分別添加一下兩個方法, 側其中一個方法的時候, 能夠先註釋掉另一個方法
#import "Person.h"
@implementation Person
- (void)setAge:(int)age {
NSLog(@"setAge--");
}
- (void)_setAge:(int)age {
NSLog(@"_setAge--");
}
@end
複製代碼
而後在ViewController.m
調用setValue
方法的時候, 能夠看到打印對應的輸出, 當上述兩個方法同事存在的時候, 則會默認執行setAge
方法
[person setValue:@20 forKey:@"age"];
複製代碼
2. 若是沒有setKey:
和_setKey:
兩個方法, 則會繼續查找Person.m
文件中是否有accessInstanceVariablesDirectly
方法, 若是沒有程序會奔潰
#import "Person.h"
@implementation Person
+ (BOOL)accessInstanceVariablesDirectly {
// 默認返回值是YES
return YES;
}
@end
複製代碼
accessInstanceVariablesDirectly
方法默認是返回YES
的, 若是return NO
, 則程序一樣會崩潰, 並拋出NSUnknownKeyException
異常return YES
的狀況下, 會按照順序查找_key、_isKey、key、isKey
等成員變量, 若是找不到依然會拋出NSUnknownKeyException
異常Person.h
文件中, 分別聲明四個變量#import <Foundation/Foundation.h>
@interface Person : NSObject
{
@public
int age;
int isAge;
int _age;
int _isAge;
}
@end
複製代碼
在ViewController.m
中添加以下代碼, 執行結果以下所示
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
[person setValue:@20 forKey:@"age"];
NSLog(@"-----------");
}
複製代碼
Person.h
中聲明age、isAge、_age、_isAge
四個變量的時候, 上述代碼會默認賦值給_age
變量_age
屬性時, 則會默認賦值給_isAge
屬性, 以此類推依次是age
和isAge
變量, 有興趣的能夠親自測試一番valueForKey
valueForKey
經過key
進行取值的時候, 取值流程和setValue
相似, 途中也比較清晰, 這裏就不在贅述了
歡迎您掃一掃下面的微信公衆號,訂閱個人博客!