KVO
KVO的全稱是Key-Value Observing,俗稱“鍵值監聽”,可以用于監聽某個物件屬性值的改變
KVO的使用
可以通過addObserver: forKeyPath:方法對屬性發起監聽,然后通過observeValueForKeyPath: ofObject: change:方法中對應進行監聽,見下面示例代碼
// 示例代碼
@interface Person : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) int height;
@end
@implementation Person
@end
@interface ViewController ()
@property (strong, nonatomic) Person *person1;
@property (strong, nonatomic) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 列印添加監聽之前person1和person2對應的isa指標指向的型別
NSLog(@"person1添加KVO監聽之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
// 列印結果:Person Person
// 列印添加監聽之前person1和person2對應的setAge方法是否有改變
NSLog(@"person1添加KVO監聽之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 0x10b60c4b0 0x10b60c4b0
// 給person1物件添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
// 列印添加監聽之后person1和person2對應的isa指標指向的型別
NSLog(@"person1添加KVO監聽之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
// 列印結果:NSKVONotifying_Person Person
// 列印添加監聽之后person1和person2對應的setAge方法是否有改變
NSLog(@"person1添加KVO監聽之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 0x7fff207b62b7 0x10b60c4b0
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 20;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 當監聽物件的屬性值發生改變時,就會呼叫
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
@end
注意:監聽的物件銷毀之前要移除該監聽removeObserver: forKeyPath:
KVO的實作本質
1.通過上面示例代碼發現,函式在呼叫addObserver: forKeyPath:方法之后,person1的實體物件的isa指標指向了一個新的型別NSKVONotifying_Person,而沒有添加監聽的person2的isa指標還是指向了Person這個型別
2.我們發現通過object_getClass列印person1的類物件和元類物件都是新派生出來的NSKVONotifying_Person這個型別
NSLog(@"類物件 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
// NSKVONotifying_Person Person
NSLog(@"元類物件 - %@ %@",
object_getClass(object_getClass(self.person1)),
object_getClass(object_getClass(self.person2)));
// NSKVONotifying_Person Person
3.我們發現通過object_getClass列印person1的superclass是Person這個型別,說明新派生出來的NSKVONotifying_Person是Person的子類
NSLog(@"父類 - %@ %@",
object_getClass(self.person1).superclass,
object_getClass(self.person2).superclass);
// Person NSObject
4.通過列印我們發現,person1呼叫的setAge方法的記憶體地址發生了改變,通過LLDB列印該地址的詳細資訊發現setAge方法的實作實際是Foundation框架中的_NSSetIntValueAndNotify這個函式
(lldb) p (IMP)0x7fff207b62b7
(IMP) $2 = 0x00007fff207b62b7 (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP) 0x108801480
(IMP) $3 = 0x0000000108801480 (Interview01`-[Person setAge:] at Person.m:13)
5.我們手動創建這個派生型別NSKVONotifying_Person,并且在Person里面重寫setAge:、willChangeValueForKey:、didChangeValueForKey:這三個方法,運行程式并觀察呼叫情況
@interface NSKVONotifying_Person : Person
@end
@implementation NSKVONotifying_Person
@end
@interface Person : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) int height;
@end
@implementation Person
- (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");
}
@end
由此可見,當監聽的屬性發生改變,系統派生出的這個類NSKVONotifying_Person會對應的先后呼叫willChangeValueForKey:、setAge:、didChangeValueForKey:這三個方法,并在didChangeValueForKey:里呼叫觀察者的observeValueForKeyPath: ofObject: change:來通知值屬性值的變化
// 執行后列印
2021-01-19 13:42:02.071987+0800 Interview01[37119:19609444] willChangeValueForKey
2021-01-19 13:42:02.072192+0800 Interview01[37119:19609444] setAge:
2021-01-19 13:42:02.072332+0800 Interview01[37119:19609444] didChangeValueForKey - begin
2021-01-19 13:42:02.072662+0800 Interview01[37119:19609444] 監聽到<Person: 0x6000036ac2c0>的age屬性值改變了 - {
kind = 1;
new = 21;
old = 1;
} - 123
2021-01-19 13:42:02.072817+0800 Interview01[37119:19609444] didChangeValueForKey - end
6.通過class方法列印person1的類發現還是Person這個型別,說明在派生出的這個類NSKVONotifying_Person內部重寫了class方法,并回傳的是Person這個型別,所以只能通過object_getClass才能獲取到真實的型別
NSLog(@"%@ %@",
[self.person1 class],
[self.person2 class]);
// Person Person
NSLog(@"%@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
// NSKVONotifying_Person Person
7.通過Runtime的class_copyMethodList函式查看NSKVONotifying_Person內部還動態生成了dealloc、_isKVOA這兩個函式
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 獲得方法陣列
Method *methodList = class_copyMethodList(cls, &count);
// 存盤方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 列印方法名
NSLog(@"%@ %@", cls, methodNames);
}
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
// 列印結果
2021-01-19 15:38:13.552990+0800 Interview01[41940:19730538] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
2021-01-19 15:38:13.553166+0800 Interview01[41940:19730538] MJPerson setAge:, age,
通過上面一系列操作可以匯總為:
- 利用
RuntimeAPI動態生成一個子類,并且讓instance物件的isa指向這個全新的子類 - 全新的子類會重寫
class這個函式,并回傳父型別別- 當修改instance物件的屬性時,會呼叫Foundation的_NSSetXXXValueAndNotify函式- 呼叫willChangeValueForKey:- 呼叫父類原來的setter- 呼叫didChangeValueForKey:- 內部會觸發監聽器(Oberser)的監聽方法observeValueForKeyPath:ofObject:change:context:

KVO的應用場景
1.監聽ScrollView的偏移量,改變導航欄背景色
2.給TextView增加placeHolder,通過KVO監聽文本是否輸入對應隱藏展示placeHolder
KVC
KVC的全稱是Key-Value Coding,俗稱“鍵值編碼”,可以通過一個key來訪問某個屬性
KVC的使用
可以通過setValue: forKeyPath: 和setValue: forKey:來給屬性賦值,valueForKeyPath:和valueForKey:來獲取屬性值,
setValue: forKeyPath:可以根據keyPath找到更深層次的屬性來賦值,setValue: forKey:就只能找當前物件的屬性,見下面示例代碼
// 示例代碼
@interface Cat : NSObject
@property (assign, nonatomic) int weight;
@end
@interface Person : NSObject
@property (assign, nonatomic) int age;
@property (strong, nonatomic) Cat *cat;
@end
@implementation Cat
@end
@implementation Person
@end
Person *person = [[Person alloc] init];
[person setValue:@10 forKey:@"age"];
person.cat = [[Cat alloc] init];
[person setValue:@80 forKeyPath:@"cat.weight"];
// NSLog(@"%d, %d", person.age, person.cat.weight);
NSLog(@"%@", [person valueForKey:@"age"]);
NSLog(@"%@", [person valueForKeyPath:@"cat.weight"]);
// 輸出:10,80
注意:
- 如果
person.cat沒有創建物件,那么setValue: forKeyPath:也不能給cat.weight屬性賦值 - 如果用
setValue: forKey:方法來給cat.weight屬性賦值,那么會拋出例外[<Person 0x100510ec0> setValue:forUndefinedKey:]
KVC的實作本質
setValue: forKey: 的實作本質
1.在Person里分別添加和注釋setAge:、_setAge:兩個方法,然后運行程式發現,內部會按順序分別查找每個方法是否存在
@interface Person : NSObject
@end
@implementation Person
// 分別打開和注釋下面兩個方法
//- (void)setAge:(int)age
//{
// NSLog(@"setAge: - %d", age);
//}
- (void)_setAge:(int)age
{
NSLog(@"_setAge: - %d", age);
}
@end
2.注釋掉上面兩個方法后,重寫accessInstanceVariablesDirectly方法并對應回傳YES和NO,運行程式發現回傳NO會拋出例外,說明不會再去查找是否有對應的屬性,
accessInstanceVariablesDirectly默認的回傳值就是YES
// 默認的回傳值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
//return YES;
return NO;
}
3.最后我們在給Person物件分別添加和注釋_age、_isAge、age、isAge這幾個成員變數,運行程式發現,內部會按順序分別查找每個成員變數是否存在,如果都沒找到也會拋出例外
// 分別打開和注釋下面的每個成員變數
@interface Person : NSObject
{
@public
// int age;
// int isAge;
// int _isAge;
int _age;
}
@end
通過上面一系列操作可以匯總為:

valueForKey: 的實作本質
1.在Person里分別添加和注釋getAge、age、isAge、_age幾個方法,然后運行程式發現,內部會按順序查找每個方法是否存在
@interface Person : NSObject
@end
@implementation MJPerson
// 分別打開和注釋下面兩個方法
- (int)getAge
{
return 11;
}
//- (int)age
//{
// return 12;
//}
//- (int)isAge
//{
// return 13;
//}
//- (int)_age
//{
// return 14;
//}
@end
2.同setValue: forKey:第二部操作一樣,如果回傳值為NO則拋出例外[<Person 0x105820160> valueForUndefinedKey:]
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x105820160> valueForUndefinedKey:]: this class is not key value coding-compliant for the key age.'
3.同setValue: forKey:最后一步操作一樣,只不過找到了對應的對應的成員變數直接取值,找不到也會拋出上面的例外
通過上面一系列操作也可以匯總為:

KVC的應用場景
1.可以通過KVC獲取到私有成員變數,以及修改私有成員變數的值
iOS13之后蘋果不允許通過KVC獲取系統API的私有成員了,會crash
通過KVC訪問自定義型別的私有成員還是可以的
2.字典轉模式
面試題
1.如何手動觸發KVO?手動呼叫willChangeValueForKey:和didChangeValueForKey:
2.直接修改成員變數會觸發KVO么
不會觸發KVO
3.通過KVC修改屬性會觸發KVO么?
會觸發KVO
如示例代碼所示,我們給Person添加一個成員變數age和一個只讀屬性weight,然后都是通過KVC的方式分別給它們賦值,發現都會觸發KV0監聽,并呼叫了willChangeValueForKey和didChangeValueForKey方法
// Person.h
@interface Person : NSObject
{
@public
int age;
}
@property (assign, nonatomic, readonly) int weight;
@end
// Person.m
@implementation Person
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
// ViewController.m
@interface ViewController ()
@property (strong, nonatomic) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
//添加KVO監聽
[self.person addObserver: self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[self.person addObserver: self forKeyPath:@"weight" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[self.person setValue:@10 forKey:@"age"];
[self.person setValue:@20 forKey:@"weight"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"observeValueForKeyPath: %@",change);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
[self.person removeObserver:self forKeyPath:@"weight"];
}
@end
// 輸出結果
//willChangeValueForKey
//didChangeValueForKey - begin
//observeValueForKeyPath: {
// kind = 1;
// new = 10;
// old = 0;
//}
//didChangeValueForKey - end
//
//
//willChangeValueForKey
//didChangeValueForKey - begin
//observeValueForKeyPath: {
// kind = 1;
// new = 20;
// old = 0;
//}
//didChangeValueForKey - end
4.怎么通過KVO監聽陣列的元素變化?
我們可以通過陣列的KVC方式添加元素,其底層會呼叫KVO觸發監聽器來監聽陣列元素變化
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *lines;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.lines = [NSMutableArray array];
[self addObserver: self forKeyPath:@"lines" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[[self mutableArrayValueForKey:@"lines"] addObject:@"1"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"observeValueForKeyPath: %@",change);
}
- (void)dealloc {
[self removeObserver:self forKeyPath:@"lines"];
}
@end
// 列印:
observeValueForKeyPath: {
indexes = "<_NSCachedIndexSet: 0x6000030afe60>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/273192.html
標籤:iOS
