iOS底层-KVO、KVC
KVO
- KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
-
代码分析:
#import "ViewController.h" #import "Person.h" #import <objc/runtime.h> @interface ViewController () @property (nonatomic,strong) Person *person1; @property (nonatomic,strong) Person *person2; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.person1 = [[Person alloc] init]; self.person1.age = 10; self.person2 = [[Person alloc] init]; self.person2.age = 20; //监听isa的变化 NSLog(@"===添加KVO之前===%@===%@",object_getClass(self.person1),object_getClass(self.person2)); //监听setAge的实现变化 NSLog(@"person1添加KVO监听之前 - %p === person2===%p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); //给person对象添加监听KVO NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"age"]; //监听isa的变化 NSLog(@"===添加KVO之后===%@===%@",object_getClass(self.person1),object_getClass(self.person2)); //监听setAge的实现变化 NSLog(@"person1添加KVO监听之后 - %p === person2=== %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); //监听类实现了那些方法 [self printMethodNamesOfClass:object_getClass(self.person1)]; [self printMethodNamesOfClass:object_getClass(self.person2)]; //打断点处 NSLog(@"=============="); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person1.age = 20; } //当监听对象的属性值发生改变的时候 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ NSLog(@"age=== %@====%@===%@",keyPath,object,change); } //打印一个类的所有方法实现 - (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); } -(void)dealloc{ [self removeObserver:self forKeyPath:@"age"]; } @end
-
打印调试如下:
//isa指向监听 ===添加KVO之前===Person===Person //setAge方法实现监听 person1添加KVO监听之前 - 0x10fb5d400 === person2===0x10fb5d400 -------person1调用了addObserver方法 //isa指向监听 ===添加KVO之后===NSKVONotifying_Person===Person //setAge方法实现监听 person1添加KVO监听之后 - 0x10feb83d2 === person2=== 0x10fb5d400 //当前类的方法列表监听 类名===NSKVONotifying_Person== 类中的方法列表=== setAge:, class, dealloc, _isKVOA, 类名===Person== 类中的方法列表=== height, setHeight:, setAge:, age, //使用lldb命令,查看地址对应的实现方法名 (lldb) p (IMP)0x10feb83d2 (IMP) $0 = 0x000000010feb83d2 (Foundation`_NSSetIntValueAndNotify) (lldb) po (IMP) 0x10fb5d400 (-[Person setAge:] at Person.h:15)
- 可以发现
person1
执行了addoObserver
之后,person1
的isa指针指向的不是Person
了,而是NSKVONotifying_Person
了 - 说明,
person1
调用setAge
对应的方法实现是_NSSetIntValueAndNotify
这个函数,而不是调用setAge方法了 NSKVONotifying_Person
这个类内部实现了setAge:, class, dealloc, _isKVOA
这4个方法
- 可以发现
_NSSet*ValueAndNotify
的内部实现 (*表示int、char、double等 )- 从上面
Foundation '_NSSetIntValueAndNotify
可以看出,_NSSetIntValueAndNotify
这个函数的实现在Foundation框架中 - 通过手机越狱,导出Foundation框架动态库,然后将动态库文件拖入到Hopper逆向工具,找到该函数,分析该函数内部流程如下:
- 调用
willChangeValueForKey:
- 调用原来的
setter
实现 - 调用
didChangeValueForKey:
didChangeValueForKey:
内部会调用observer
的observeValueForKeyPath:ofObject:change:context:
方法
- 调用
- 从上面
-
Person对象调用addObserver之后如下:
相关面试题
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey:
- 父类原来的setter
- didChangeValueForKey:
- 内部会触发监听器(Oberser)的监听方法(
observeValueForKeyPath:ofObject:change:context:
)
- 内部会触发监听器(Oberser)的监听方法(
- 如何手动触发KVO?
- 手动调用willChangeValueForKey:和didChangeValueForKey:
- 直接修改成员变量会触发KVO么?
- 不会触发KVO,因为本质是监听setter方法
KVC
-
代码示例:
#import <Foundation/Foundation.h> @interface Person : NSObject { @public // int _age; // int _isAge; // int age; int isAge; } @end #import "Person.h" @implementation Person /** valueForKey会按照下面顺序寻找 */ //-(int)getAge{ // return 10; //} //-(int)age{ // return 20; //} //-(int)isAge{ // return 30; //} //-(int)_age{ // return 40; //} /** setValue: forKey: 会按照下面顺序寻找 */ //-(void)setAge:(int)age{ // NSLog(@"setAge====="); //} //-(void)_setAge:(int)age{ // NSLog(@"_setAge====="); //} /* 是否允许直接返回成员变量 如果不主动实现,默认返回值就是YES 如果返回值为YES,那么久允许直接访问成员变量,反之不能 */ +(BOOL)accessInstanceVariablesDirectly{ // return NO; return YES; } /*********证明是否手动触发了KVO**********/ //KVO成员变量修改之前调用 -(void)willChangeValueForKey:(NSString *)key{ [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey - %@", key); } //KVO成员变量修改之后调用 -(void)didChangeValueForKey:(NSString *)key{ NSLog(@"didChangeValueForKey - begin - %@", key); //这个内部会调用observer的observeValueForKeyPath方法 [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey - end - %@", key); } @end #import "ViewController.h" #import "Person.h" @interface ViewController () @property (nonatomic,strong) Person *person; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Person *person = [[Person alloc] init]; self.person = person; //KVO监听 [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; //KVC赋值 [person setValue:@(1) forKey:@"age"]; //打印值 // NSLog(@"=====%d",person->_age); // NSLog(@"=====%d",person->_isAge); // NSLog(@"=====%d",person->age); NSLog(@"=====%d",person->isAge); } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //获取值 NSLog(@"==valueForKey===%@",[self.person valueForKey:@"age"]); } /** 1. 发现问题如下: 1. 即使Person没有实现setAge:方法或者没有age属性,仅仅有_age/_isAge /age/isAge成员变量,当KVC赋值的时候,也会触发KVO。 2. KVO监听的是对象的“age”成员,但是对象即使没有age成员,或者_age成员,只有_isAge或者isAge,也会触发KVO 3. 说明KVC(setValue:forKey:)内部主动通知了KVO //猜测内部实现 // [person willChangeValueForKey:@"age"]; // person->_age = 10; // [person didChangeValueForKey:@"age"]; 4. 因此,使用KVC修改成员变量值,不管对象内部是否实现setter方法,都会触发KVO */ //当监听对象的属性值发生改变的时候 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ NSLog(@"observeValueForKeyPath - %@", change); } - (void)dealloc{ [self.person removeObserver:self forKeyPath:@"age"]; } @end
-
经过上面的代码分析,KVC的原理如下图
- setValue:forKey:的原理
- 到对象寻找是否有setAge方法,找到了传递参数,调用方法,如果没有
- 寻找_setAge方法,找到了传递参数,调用方法,如果没有
- 获取accessInstanceVariablesDirectly这个类方法的返回值
- 返回值为NO,调用setValue:forUndefinedKey:,并抛出异常NSUnknownKeyException
- 返回值为YES,继续寻找
- 按照_age、_isAge、age、isage顺序查找成员变量,只要找到,直接赋值,没找到
-
调用setValue:forUndefinedKey:,并抛出异常NSUnknownKeyException
- valueForKey:的原理
- 按照getAge、age、 isAge、_age顺序查找方法,找到直接调用方法,没找到
- 获取accessInstanceVariablesDirectly这个类方法的返回值
- 返回值为NO,调用valueForUndefinedKey:,并抛出异常NSUnknownKeyException
- 返回值为YES,继续寻找
- 按照_age、_isAge、age、isAge顺序查找成员变量,找到直接赋值,没找到
-
调用valueForUndefinedKey:,并抛出异常NSUnknownKeyException
- setValue:forKey:的原理
面试题
- 通过KVC修改属性会触发KVO么?
- 会触发KVO (上面分析的有)
- KVC的赋值和取值过程是怎样的?原理是什么?