iOS底层原理 KVO和KVC本质与联系 --(3)

上篇讲了类的本质,咱们知道实例实际是存储了成员变量的值和指向类的isa指针,class对象和meta-class对象包含 isasuperclassclass_rw_t这几种结构体,只是数据不同,isa须要ISA_MASK&以后才是真正的值。那么今天咱们在看一下Key-Value Observing的本质。git

KVO本质

首先须要了解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 *)contextbash

触发条件

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//	self.person.age += 1;
   [self.person willChangeValueForKey:@"age"];
   [self.person didChangeValueForKey:@"age"];
}
复制代码

当把age具体值的改变,变成手动调用willChangeValueForKeydidChangeValueForKey的时候,结果以下:app

监听到了age变化: {
   kind = 1;
   new = 10;
   old = 10;
}
复制代码

newold的值居然同样,经测试只有同时前后调用willChangeValueForKeydidChangeValueForKey,会触发回调函数observeValueForKeyPath,由此可知触发条件是willChangeValueForKeydidChangeValueForKey配合使用。框架

探寻KVO底层实现原理

经过上述代码咱们发现,一旦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】以后,personisa指针指向了NSKVONotifying_FYPerson,而person2isaFYPerson,能够看出系统是对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)
复制代码

ageint的时候添加以后是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方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。咱们在p1添加过KVO监听以后,分别打印p1和p2对象的class能够发现他们都返回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的本质

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_isKeykeyisKey的查找优先级查找成员变量,找到以后直接复制,不然抛出异常。 咱们使用这段代码来验证:

@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"];

复制代码

当执行code1code2都有的时候,输出-[FYPerson setAge:] 2,当code1注释掉,输出-[FYPerson _setAge:] 2,能够看出执行顺序是setAge,没有setAge的时候再去执行_setAge

如今新增FYPerson4个成员变量,依次注释掉他们来测试寻找成员变量的顺序。

@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和KVO联系

咱们知道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,感受这个挺好玩的。

本文章之因此图片比较少,我以为仍是跟着代码敲一遍,印象比较深入。


最怕一辈子碌碌无为,还安慰本身平凡难得。

广告时间

相关文章
相关标签/搜索