KVO本质
面试题:
1.ios是用什么方式实现对一个对象的KVO?(KVO的本质是啥)
析:利用runtimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey;
- 父类原来的setter
- didChangeValueForKey;
内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObjectchange:context)
2.如何手动的触发KVO?
析:手动调用willChangeValueForKey和didChangeValueForKey;
3直接修改成员变量会 触发KVO?
不会触发
简单使用:
#import "ViewController.h"
@interface Person : NSObject{
@public
int _age;
}
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)CGFloat height;
@end
@implementation Person
@end
@interface ViewController ()
@property (nonatomic,strong)Person *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc]init];
self.person1.name = @"Jack";
self.person1.height = 175;
self.person1->_age = 18;
//添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"height-contenxt"];
[self.person1 addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
[self.person1 addObserver:self forKeyPath:@"_age" options:options context:@"_age-contenxt"];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"Jack2";
self.person1.height = 180;
self.person1->_age = 20;
}
// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"height"];
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
控制台Log:
======================
监听到<Person: 0x600003962700>的name属性值改变了 - {
kind = 1;
new = Jack2;
old = Jack;
} - name-contenxt
监听到<Person: 0x600003962700>的height属性值改变了 - {
kind = 1;
new = 180;
old = 175;
} - height-contenxt
发现:直接修改成员变量会 是不会触发KVO的.
//分析:NSDictionary<NSKeyValueChangeKey,id> *)
-
new:新值
-
old:旧值
-
kind(指明了变更的类型):(NSKeyValueChange-->NSKeyValueChangeKindKey)
typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, };
一般情况下返回的都是1也就是第一个NSKeyValueChangeSetting,但是如果你监听的属性是一个集合对象的话,当这个集合中的元素被插入,删除,替换时,就会分别返回NSKeyValueChangeInsertion,NSKeyValueChangeRemoval和NSKeyValueChangeReplacement(有兴趣的可以去试试哟)
下面来一步步分析:
1.首先新建一个myPerson类如下:
//.h
#import <Foundation/Foundation.h>
@interface myPerson : NSObject
@property (nonatomic,strong)NSString *name;
@end
//.m
#import "myPerson.h"
@implementation myPerson
-(void)willChangeValueForKey:(NSString *)key{
NSLog(@"%s-%d",__FUNCTION__,__LINE__);
[super willChangeValueForKey:key]
}
-(void)didChangeValueForKey:(NSString *)key{
NSLog(@"%s-%d",__FUNCTION__,__LINE__);
[super didChangeValueForKey:key]
}
@end
//viewcontroller.m
#import "ViewController2.h"
#import "myPerson.h"
#import <objc/runtime.h>
@interface ViewController2 ()
@property (nonatomic,strong)myPerson *p;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"KVO本质";
self.p = [[myPerson alloc]init];
self.p.name = @"jack";
NSLog(@"添加之前“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
NSLog(@"添加之前的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
//添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
NSLog(@"添加之后“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
NSLog(@"添加之后的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
NSLog(@"End");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.p setName:@"Jack2"];
}
// 当监听对象的属性值发生改变时,就会调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
[self.p removeObserver:self forKeyPath:@"name"];
NSLog(@"%s-%d",__FUNCTION__,__LINE__);
}
@end
分析:在 NSLog(@"End");处打一个断点
打印log日志:
添加之前“类(isa)“
myPerson
添加之前的name的set方法
0x10290c5f0
添加之后“类(isa)“
NSKVONotifying_myPerson
添加之后的name的set方法
0x102c6663a
我们可以得出:
- 1.在添加kvo监听之前class对象为:myPerson、在添加kvo监听之后class对象为NSKVONotifying_myPerson
- 2.在添加kvo监听之前name的set方法:0x10290c5f0、在添加kvo监听之后name的set方法:0x102c6663a
(不一样) - 3.继续验证
输入:p (IMP)0x10ec655f0
结果:(IMP) $0 = 0x000000010ec655f0 (KVO`-[myPerson setName:] at myPerson.h:14)
输入:p (IMP)0x10efbf63a
结果:(IMP) $1 = 0x000000010efbf63a (Foundation`_NSSetObjectValueAndNotify)
我们猜测:
(1).在添加之后:底层会动态的创建一个myPerson的子类(继承了类myPerson), NSKVONotifying_myPerson
(2).然后会去重新实现setName的方法
- 4.会执行-[myPerson willChangeValueForKey:]-15
- 5.会执行-[myPerson didChangeValueForKey:]-19
2.我们在修改下viewcontroller文件
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"KVO本质";
self.p = [[myPerson alloc]init];
self.p.name = @"jack";
NSLog(@"添加之前“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
NSLog(@"添加之前的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
[self printMethodNamesOfClass:object_getClass(self.p)];
//添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p addObserver:self forKeyPath:@"name" options:options context:@"name-contenxt"];
NSLog(@"添加之后“类(isa)“\n%@",object_getClass(self.p)); //self.p->isa
NSLog(@"添加之后的name的set方法\n%p",[self.p methodForSelector:@selector(setName:)]);
[self printMethodNamesOfClass:object_getClass(self.p)];
NSLog(@"End");
}
//利用runtime来打印类的属性和方法值
- (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.p)];第一次:
myPerson .cxx_destruct, name, setName:,
[self printMethodNamesOfClass:object_getClass(self.p)];第二次:
NSKVONotifying_myPerson setName:, class, dealloc, _isKVOA,
在NSKVONotifying_myPerson里面多了setName、class、dealloc, _isKVOA的方法
我们来写一个:
//NSKVONotifying_myPerson_Test.h
#import "myPerson.h"
@interface NSKVONotifying_myPerson_Test : myPerson
@end
#import "NSKVONotifying_myPerson_Test.h"
@implementation NSKVONotifying_myPerson_Test
-(void)setName:(NSString *)name{
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify(){
//里面做了一些列的处理
}
//内部实现,隐藏了NSKVONotifying_myPerson类的存在
-(Class)class{
return [myPerson class];
}
-(void)dealloc{
//释放相应的资源
}
- (BOOL)_isKVOA
{
return YES;
}
@end
[self printMethodNamesOfClass:[NSKVONotifying_myPerson_Test class]];
打印:NSKVONotifying_myPerson_Test _isKVOA, dealloc, setName:, class,
分析:
- dealloc、_isKVOA方法:return YES;(显然可知)
- (Class)class方法:return [myPerson class]; (屏幕内部实现,隐藏了NSKVONotifying_myPerson类的存在)
我们在来看看:_NSSetObjectValueAndNotify的里面具体的逻辑:
- 1.从多了setName方法,我们可以知道,它肯定重新写了setName的方法,而在原来的myPerson对象,监听的方法同样走了willChangeValueForKey和didChangeValueForKey方法 ,所以它肯定也调用了super的方法;
- 2.我们在来打个断点看看,看看栈的执行函数
//将断点打在:touches方法处,然后在控制台输入:
imagewatchpoint set variable _p->_name(注意这里只能使用_的成员变量)
点击stepOver(上绿色按钮)会发现:
那么我们可以猜测: _NSSetObjectValueAndNotify的方法的简单内部实现
void _NSSetObjectValueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
综上所述:我们可以得出下关系图:
image
image
说明:
-
调用addObserve的方法的时候,苹果会动态的生成一个继承了myPerson的子类NSKVONotifying_myPerson,
-
在NSKVONotifying_myPerson内部会重新实现setName的方法,setName方法里面的内部实现是一个C函数
-
该C函数内部的实现大致的为,先实现willChangeValueForKey,然后实现父类的setName的方法,然后实现didChangeValueForKey,里面在通知监听者,该属性变化了
-
在NSKVONotifying_myPerson也添加了一个isKVO的方法.标记是KVO使用的类
-
在NSKVONotifying_myPerson也添加了一个dealloc的方法,来释放不用的资源
-
在NSKVONotifying_myPerson也添加了一个class的方法(return [myPerson class]),屏蔽了系统的内部实现(但是这里不是isa直接指向myPerson的元类对象,证明见下)
#import "ViewController3.h"
#import "myPerson.h"
#import <objc/runtime.h>
struct my_class{
Class isa;
};
@interface ViewController3 ()
@property (nonatomic,strong)myPerson *p;
@end
@implementation ViewController3
- (void)viewDidLoad {
[super viewDidLoad];
//0x0000000ffffffff8ULL 0x00007ffffffffff8ULL
self.p = [[myPerson alloc]init];
self.p.name = @"jack";
NSLog(@"添加之前“类(isa)“\n%p",object_getClass(self.p)); //self.p->isa
NSLog(@"添加之前“元类”\n%p",object_getClass(object_getClass(self.p)));
Class class = object_getClass(self.p);
Class mclass = object_getClass(object_getClass(self.p));
struct my_class *myC = (__bridge struct my_class *)class;
struct my_class *mMetaC = (__bridge struct my_class *)mclass;
//添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p addObserver:self forKeyPath:@"name" options:options context:@"name-context"];
NSLog(@"添加之后“类(isa)“\n%p",object_getClass(self.p)); //self.p->isa
NSLog(@"添加之后“元类”\n%p",object_getClass(object_getClass(self.p)));
Class class2 = object_getClass(self.p);
Class mclass2 = object_getClass(object_getClass(self.p));
struct my_class *myC2 = (__bridge struct my_class *)class2;
struct my_class *mMetaC2 = (__bridge struct my_class *)mclass2;
NSLog(@"End");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.p.name = @"Jack2";
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc {
[self.p removeObserver:self forKeyPath:@"name"];
NSLog(@"%s-%d",__FUNCTION__,__LINE__);
}
@end
在 NSLog(@"End");打个断点:
image
从里面的内存值:很容易看出,素虽然返回了return [myPerson class],但是它的isa这里不是指向myPerson的元类对象。而是自己的元类的对象哟
我们试试看手动写一个KVO
NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
//给NSObject类添加一个监听的方法
-(void)my_ddObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation NSObject (KVO)
-(void)my_ddObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//动态添加一个类
NSString *originClassName = NSStringFromClass([self class]);
//苹果创建的是 NSKVONotifying_ 为前缀的
NSString *newClassName = [@"myKVONotifying_" stringByAppendingString:originClassName];
const char *newName = [newClassName UTF8String];
// 继承自当前类,创建一个子类
Class kvoClass = objc_allocateClassPair([self class], newName, 0);
// 添加setter方法
class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@@");
//注册新添加的这个类
objc_registerClassPair(kvoClass);
// 修改isa,本质就是改变当前对象的类名
object_setClass(self, kvoClass);
// 保存观察者属性到当前类中
objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - 重写父类方法
void setName(id self, SEL _cmd, NSString *name) {
// 保存当前KVO的类
Class kvoClass = [self class];
// 将self的isa指针指向父类myPerson,调用父类setter方法
object_setClass(self, class_getSuperclass([self class]));
#warning -- 这边方法会报错:build Settings 里面搜索enable strict 修改为NO即可
// 调用父类setter方法,重新复制
objc_msgSend(self, @selector(setName:), name);
// 取出myKVO_Person观察者
id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
// 通知观察者,执行通知方法
//这里是直接重新写的setName方法,所以很容易知道是 “name”
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"kind":@"kind_value",@"new":@"new_value",@"old":@"old_value"}, @"这里是监听的上下文环境");
// 重新修改为myKVO_Person类
object_setClass(self, kvoClass);
}
@end
实现:
#import "ViewController4.h"
#import "NSObject+KVO.h"
#import "myPerson.h"
@interface ViewController4 ()
@property (nonatomic,strong)myPerson *p;
@end
@implementation ViewController4
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"手动写一个KVO";
self.p = [myPerson new];
self.p.name = @"jack";
[self.p my_ddObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:@"123"];
}
- (void)dealloc {
[self.p removeObserver:self forKeyPath:@"name"];
NSLog(@"%s-%d",__FUNCTION__,__LINE__);
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.p.name = @"Lucy";
}
@end
友情链接: