上篇讲了类的本质,咱们知道实例实际是存储了成员变量的值和指向类的isa
指针,class
对象和meta-class
对象包含 isa
、superclass
和class_rw_t
这几种结构体,只是数据不同,isa
须要ISA_MASK
&以后才是真正的值。那么今天咱们在看一下Key-Value Observing的本质。git
首先须要了解KVO基本使用,KVO的全称 Key-Value Observing,俗称“键值监听”,能够用于监听某个对象属性值的改变。下面咱们展现一下KVO的基本使用。github
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FYPerson : NSObject
@property (nonatomic,assign) NSInteger age;
@end
NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "FYPerson.h"
@interface ViewController ()
@property (nonatomic,strong)FYPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person=[FYPerson new];
self.person.age = 10;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age += 1;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到了age变化: %@",change);
}
-(void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
//下边是输出结果
监听到了age变化: {
kind = 1;
new = 12;
old = 11;
}
复制代码
从上述代码能够看出,添加监听以后,当值改变时,会触发函数observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
。bash
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person.age += 1;
[self.person willChangeValueForKey:@"age"];
[self.person didChangeValueForKey:@"age"];
}
复制代码
当把age
具体值的改变,变成手动调用willChangeValueForKey
和didChangeValueForKey
的时候,结果以下:app
监听到了age变化: {
kind = 1;
new = 10;
old = 10;
}
复制代码
new
和old
的值居然同样,经测试只有同时前后调用willChangeValueForKey
和didChangeValueForKey
,会触发回调函数observeValueForKeyPath
,由此可知触发条件是willChangeValueForKey
和didChangeValueForKey
配合使用。框架
经过上述代码咱们发现,一旦age属性的值发生改变时,就会通知到监听者,而且咱们知道赋值操做都是调用 set方法,咱们能够来到Person类中重写age的set方法,观察是不是KVO在set方法内部作了一些操做来通知监听者。 咱们发现即便重写了set方法,p1对象和p2对象调用一样的set方法,可是咱们发现p1除了调用set方法以外还会另外执行监听器的observeValueForKeyPath方法。 说明KVO在运行时获取对p1对象作了一些改变。至关于在程序运行过程当中,对p1对象作了一些变化,使得p1对象在调用setage方法的时候可能作了一些额外的操做,因此问题出在对象身上,两个对象在内存中确定不同,两个对象可能本质上并不同。接下来来探索KVO内部是怎么实现的。 KVO底层实现分析 首先咱们对上述代码中添加监听的地方打断点,看观察一下,addObserver方法对p1对象作了什么处理?也就是说p1对象在通过addObserver方法以后发生了什么改变,咱们经过打印isa指针:函数
@interface ViewController ()
@property (nonatomic,strong)FYPerson *person;
@property (nonatomic,strong)FYPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person=[FYPerson new];
self.person2 =[FYPerson new];
self.person.age = 10;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:nil];
Class superclass = NSStringFromClass( class_getSuperclass(NSClassFromString(@"NSKVONotifying_FYPerson")));
Class NSKVONotifying_FYPerson = objc_getClass("NSKVONotifying_FYPerson");
fy_objc_class* NSKVONotifying_FYPerson_class = (__bridge fy_objc_class *)NSKVONotifying_FYPerson;
//此处打断点
//p 命令输出isa指针
(lldb) p self.person2->isa
(Class) $0 = FYPerson
(lldb) p self.person->isa
(Class) $1 = NSKVONotifying_FYPerson
(lldb) p superclass
(Class) $0 = FYPerson
(lldb) p NSKVONotifying_FYPerson_class->superclass
(Class) $4 = FYPerson
}
复制代码
从输出的isa指针看来,通过【person addObserver】
以后,person
的isa
指针指向了NSKVONotifying_FYPerson
,而person2
的isa
是FYPerson
,能够看出系统是对instance
对象的isa
进行了赋值操做。经过p NSKVONotifying_FYPerson_class->superclass==FYPerson
能够看出isa是指向了子类,那么子类NSKVONotifying_FYPerson
到底作了那些事情呢?post
看下边代码查看函数isa改变过程:学习
self.person=[FYPerson new];
self.person2 =[FYPerson new];
self.person.age = 10;
//打断点 输出 po [_person methodForSelector:@selector(setAge:)]
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:nil];
//打断点 输出 po [_person methodForSelector:@selector(setAge:)]
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x000000010666b720
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x00000001069c63d2
//查看IMP指针对应地址和内容
(lldb) p (IMP)0x000000010666b720
(IMP) $2 = 0x000000010666b720 (day03-KVO本质`::-[FYPerson setAge:](int) at FYPerson.h:14)
(lldb) p (IMP)0x00000001069c63d2
(IMP) $3 = 0x00000001069c63d2 (Foundation`_NSSetIntValueAndNotify)
复制代码
能够看出来两次的函数地址不一致,添加KVO以前是[FYPerson setAge:]
,添加以后是(Foundation_NSSetIntValueAndNotify)
。咱们将age
的类型改为double
,再看一下结果:测试
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x00000001080c4710
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x000000010841f18c
(lldb) p (IMP)0x00000001080c4710
(IMP) $2 = 0x00000001080c4710 (day03-KVO本质`::-[FYPerson setAge:](double) at FYPerson.h:14)
(lldb) p (IMP)0x000000010841f18c
(IMP) $3 = 0x000000010841f18c (Foundation`_NSSetDoubleValueAndNotify)
复制代码
age
是int
的时候添加以后是Foundation _NSSetIntValueAndNotify
,改为double
以后,是Foundation _NSSetDoubleValueAndNotify
。那么咱们能够推测Foundation
框架中还有不少例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify
等等函数。 运行nm Foundation | grep ValueAndNotify
结果以下:ui
nm Foundation | grep ValueAndNotify
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSetObjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
复制代码
在macOS中可使用
//开始记录日志
instrumentObjcMessageSends(YES);
// Do stuff...
instrumentObjcMessageSends(NO);//结束记录日志
复制代码
若是将NSObjCMessageLoggingEnabled
环境变量设置为YES
,则Objective-C
运行时会将全部已分派的Objective-C
消息记录到名为/tmp/msgSends-<pid>
的文件中。每一次运行会生成一个文件,咱们进入到该文件内部:
//初始化
+ FYPerson NSObject initialize
+ FYPerson NSObject new
- FYPerson NSObject init
- FYPerson NSObject addObserver:forKeyPath:options:context:
- FYPerson NSObject _isKVOA
****
//子类设置age [NSKVONotifying_FYPerson setAge:]
- NSKVONotifying_FYPerson NSKVONotifying_FYPerson setAge:
- NSKVONotifying_FYPerson NSObject _changeValueForKey:key:key:usingBlock:
- NSKVONotifying_FYPerson NSObject _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty keyPathIfAffectedByValueForKey:exactMatch:
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty _keyPathIfAffectedByValueForKey:exactMatch:
//will changeValueForKey
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:willChangeValueForKeyOrKeys:recurse:forwardingValues:
//FYPerson 设置age
- FYPerson FYPerson setAge:
// didChangeValueForKeyOrKeys
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:
- NSKeyValueUnnestedProperty NSKeyValueProperty keyPath
//找到key 发送 具体的key对应的value 到observe
- NSKVONotifying_FYPerson NSObject valueForKeyPath:
- NSKVONotifying_FYPerson NSObject valueForKey:
+ NSKVONotifying_FYPerson NSObject _createValueGetterWithContainerClassID:key:
-
+ NSKVONotifying_FYPerson NSObject resolveInstanceMethod:
+ NSKVONotifying_FYPerson NSObject resolveInstanceMethod:
- NSKVONotifying_FYPerson FYPerson age
+ NSKeyValueMethodGetter NSObject alloc
- NSKeyValueMethodGetter NSKeyValueMethodGetter initWithContainerClassID:key:method:
- NSKeyValueGetter NSKeyValueAccessor initWithContainerClassID:key:implementation:selector:extraArguments:count:
- NSKVONotifying_FYPerson NSObject respondsToSelector:
- NSKVONotifying_FYPerson NSKVONotifying_FYPerson class
- NSKVONotifying_FYPerson NSKVONotifying_FYPerson _isKVOA
+ FYPerson NSObject class
+ FYPerson NSObject resolveInstanceMethod:
+ FYPerson NSObject resolveInstanceMethod:
//数据字典
+ NSDictionary NSObject self
+ NSMutableDictionary NSObject self
- NSKeyValueChangeDictionary NSKeyValueChangeDictionary initWithDetailsNoCopy:originalObservable:isPriorNotification:
- NSDictionary NSObject init
// 执行观察者回调函数
- NSKVONotifying_FYPerson FYPerson observeValueForKeyPath:ofObject:change:context:
+ Student NSObject alloc
- Student NSObject init
- Student NSObject dealloc
***//省略一部分代码
NSKVONotifying_FYPerson NSObject release
- NSKeyValueChangeDictionary NSObject release
- NSKeyValueChangeDictionary NSKeyValueChangeDictionary dealloc
- NSDictionary NSObject dealloc
- NSKeyValueObservationInfo NSObject release
- NSKVONotifying_FYPerson NSObject release
复制代码
通过仔细把重要的函数过滤出来,咱们能够了解到person.age = 12
的执行过程是NSKVONotifying_FYPerson setAge:
->NSKeyValueUnnestedProperty object:withObservance:willChangeValueForKeyOrKeys:recurse:forwardingValues
->FYPerson FYPerson setAge:
->NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:
->NSKVONotifying_FYPerson NSObject valueForKeyPath:
->NSMutableDictionary NSObject self
->- NSKVONotifying_FYPerson FYPerson observeValueForKeyPath:ofObject:change:context:
,咱们来用伪代码实现一遍:
//person.age = 12
[NSKVONotifying_FYPerson setAge:12];
willChangeValueForKey@"age";
[FYPerson setAge:12];
didChangeValueForKey@"age";
[[NSMutableDictionary alloc] init];
[NSKVONotifying_FYPerson observeValueForKeyPath:ofObject:change:context];
复制代码
NSKVONotifyin_Person内部结构是怎样的? 首先咱们知道,NSKVONotifyin_Person做为Person的子类,其superclass指针指向Person类,而且NSKVONotifyin_Person内部必定对setAge方法作了单独的实现,那么NSKVONotifyin_Person同Person类的差异可能就在于其内存储的对象方法及实现不一样。 咱们经过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的对象方法
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
Person *p2 = [[Person alloc] init];
p1.age = 2.0;
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void) printMethods:(Class)cls
{
unsigned int count ;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0 ; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString: methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@",methodNames);
free(methods);
}
//结果以下:
NSKVONotifying_FYPerson - setAge: class dealloc _isKVOA
FYPerson - setAge: age
复制代码
经过上述代码咱们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此咱们能够画出NSKVONotifyin_Person的内存结构以及方法调用顺序。
若是NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到nsobject,而nsobect的class的实现大体为返回本身isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,可是apple不但愿将NSKVONotifyin_Person类暴露出来,而且不但愿咱们知道NSKVONotifyin_Person内部实现,因此在内部重写了class类,直接返回Person类,因此外界在调用p1的class对象方法时,是Person类。这样p1给外界的感受p1仍是Person类,并不知道NSKVONotifyin_Person子类的存在。
那么咱们能够猜想NSKVONotifyin_Person内重写的class内部实现大体为
- (Class) class {
// 获得类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
复制代码
最后本身写代码验证一下:
@implementation FYPerson
-(void)willChangeValueForKey:(NSString *)key{
NSLog(@"%s 开始",__func__);
[super didChangeValueForKey:key];
NSLog(@"%s 结束",__func__);
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"%s 开始",__func__);
[super didChangeValueForKey:key];
NSLog(@"%s 结束",__func__);
}
- (void)setAge:(double)age{
_age = age;
NSLog(@"%s",__func__);
}
@end
复制代码
执行以后结果以下:
-[FYPerson willChangeValueForKey:] 开始
-[FYPerson willChangeValueForKey:] 结束
-[FYPerson setAge:]
-[FYPerson didChangeValueForKey:] 开始
监听到了age变化: {
kind = 1;
new = 11;
old = 10;
}
-[FYPerson didChangeValueForKey:] 结束
复制代码
KVO实际上是一个经过runtime注册创建子类,经过修改instance的isa指针,指向新的子类,重写instace的class方法来掩盖,子类拥有本身的set方法,调用顺序是willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
KVC的全称是Key-Value Coding,俗称“键值编码”,能够经过一个key来访问某个属性。 经常使用的API有
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
复制代码
其实当Obj调用(void)setValue:(id)value forKey:(NSString *)key
的时候,obj
会主动寻找方法setKey
和_setKey
两个方法,没有找到这两个方法会再去寻找accessInstanceVariablesDirectly
,返回值为NO
则抛出异常,返回YES
则去按照_key
、_isKey
、key
、isKey
的查找优先级查找成员变量,找到以后直接复制,不然抛出异常。 咱们使用这段代码来验证:
@interface FYPerson(){
}
@end
@implementation FYPerson
//code1
- (void)setAge:(NSInteger)age{
NSLog(@"%s %ld",__func__,(long)age);
}
//code2
- (void)_setAge:(NSInteger)age{
NSLog(@"%s %ld",__func__,(long)age);
}
@end
FYPerson *p=[[FYPerson alloc]init];
[p setValue:@(2) forKey:@"age"];
复制代码
当执行code1
和code2
都有的时候,输出-[FYPerson setAge:] 2
,当code1
注释掉,输出-[FYPerson _setAge:] 2
,能够看出执行顺序是setAge
,没有setAge
的时候再去执行_setAge
。
如今新增FYPerson
4个成员变量,依次注释掉他们来测试寻找成员变量的顺序。
@interface FYPerson : NSObject
{
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
@end
FYPerson *p=[[FYPerson alloc]init];
[p setValue:@(2) forKey:@"age"];
NSLog(@"age:%d _age:%d isAge:%d _isAge:%d",(int)p->age,(int)p->_age,(int)p->isAge,(int)p->_isAge);
复制代码
age:0 _age:2 isAge:0 _isAge:0
_age
输出 age:0 isAge:0 _isAge:2
_isAge
输出 age:2 isAge:0
age
输出 isAge:2
咱们知道KVC本质也是调用setter方法,那么会出发KVO吗?
FYPerson *p=[[FYPerson alloc]init];
[p addObserver:p
forKeyPath:@"age"
options:NSKeyValueChangeNewKey
context:nil];
[p setValue:@2 forKey:@"age"];
[p removeObserver:p forKeyPath:@"age"];
@interface FYPerson(){
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
@end
@implementation FYPerson
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
@end
//结果
{
kind = 1;
new = 2;
old = 0;
}
复制代码
通过测试,能够看出KVC能触发KVO的。那么valueForKey:key
底层是怎么运行的呢?其实底层是按照顺序查找四个方法_age
->_isAge
->age
->isAge
。咱们测试一下:
FYPerson *p=[[FYPerson alloc]init];
p->_age = 1;
p->_isAge = 2;
p->age = 3;
p->isAge = 4;
NSLog(@"value:%@",[p valueForKey:@"age"]);
//依次注释1,2,3,依次输出是1->2->3->4
复制代码
KVC其实本质是执行4个set方法和4个get方法,当使用setValue:forKey:key
会触发KVO,找不到4个方法的时候会抛出异常。
以前看的没有手动去试验一下,而后再写出来,如今总结一下,参考了不少文章,还有macOS中日志记录是无心搜索出来了一个老外的blog,你们能够了解下,之后会有用,后边会讲如何hook objc_msgsend
,感受这个挺好玩的。
本文章之因此图片比较少,我以为仍是跟着代码敲一遍,印象比较深入。
最怕一辈子碌碌无为,还安慰本身平凡难得。
广告时间