本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。
一、使用上的疑问
1.keyPath是什么
当我们使用@property时候,keyPath是指的是我们的属性名,实例变量或者是存取方法?
:point_down: 对一个属性值使用@synthesize重新定义了存储变量
```
# import"Person.h"
@interface Student:Person
@property(nonatomic,strong)NSString* mark;
@end
@implementation Student
@synthesizemark = abc;
- (void)setMark:(NSString*)newMark {
abc = newMark;
}
- (NSString*)mark {
returnabc;
}
main() {
Student *stu = [[Student alloc] init];
stu.mark =@"65";
StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu]; [stuObserver addObserverForKeyPath:@"mark"];
```
// 重命名get方法stu.mark =@"85";}
实际结果是,能够监听到mark值的变化,反之,我将mark替换成真正的实例变量abc时,无法获取状态。
现在想想其实答案早就存在了,我们不做显示的@synthesize的指定时,其实等价于 @synthesize mark = _mark; ,由此看来keyPath实际指的并不是真正存储你数据的变量。
2.KVO是否能够继承
我是否能够监听我父类里的属性,哪怕他并没有暴露出来?通过某些手段得(猜)到了keyPath,然后去监听它甚至是KVC修改他的值。
子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?
子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO能否观察到?
子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?
// Person类@interfacePerson:NSObject
@property(nonatomic,strong)NSString*firstName;
@property(nonatomic,strong)NSString*lastName;
@property(nonatomic,strong,readonly)NSString*fullName;
- (void)setNewInnerName:(NSString*)str;
@end
@interfacePerson()
@property(nonatomic,strong)NSString*innerName;
@end@implementationPerson
- (void)setNewInnerName:(NSString*)str {
self.innerName = str;// 通过get、set访问 触发KVO
// [self setValue:str forKey:@"innerName"];
// KVC方式,其实调用的也是setter方法 触发KVO
// _innerName = str;
// 直接访问成员变量,不触发KVO}
// Student类
@interface Student:Person
@end
@implementationStudent
- (void)setFirstName:(NSString*)firstName {
NSLog(@"重写的setFirstName方法");
}
@end
// 执行文件
main() {
Person *p = [[Person alloc] init];
p.firstName =@"zhao";
p.lastName =@"zhiyu";
PersonKvoObserver *personKvoObserver = [[PersonKvoObserver alloc]initWithPerson:p];
[personKvoObserver addObserverForKeyPath:@"fullName"];
// 属性关联
[personKvoObserver addObserverForKeyPath:@"innerName"];
// 内部属性
p.firstName =@"zhao1";
[p setNewInnerName:@"newInnerNmame"];
// 没有暴露的属性的get、set方法被调用时,也会发送通知// 子类的属性监听
Student *stu = [[Student alloc] init];
stu.firstName =@"stu";
stu.lastName =@"dent";
StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu]; [stuObserver addObserverForKeyPath:@"fullName"];// 子类继承属性依旧被监听[stuObserver addObserverForKeyPath:@"firstName"];// 重写方法,不加super,依旧会监听kvo[stuObserver addObserverForKeyPath:@"innerName"];
stu.firstName =@"stu1";
stu.lastName =@"dent1";
[stu setNewInnerName:@"newInnerNmame"];
// 没有暴露的属性的get、set方法被调用时,也会发送通知}
通过上面的例子,我们能看出几点:
①通过KVO,能观察父类的属性值。
②只要知道了keyPath,不管有没有暴露方法,依旧可以通过KVO方式观察值的变化,而且同属性一样,可以被继承。
③子类重写父类的set方法,也并不会影响KVO的观察。
从这儿开始就有点好奇了,这个KVO是否通过子类化的方法实现?那如何让子类的继承属性也能被监听到?了解到KVO依赖setter方法的重写,那我子类重写的setter方法之后,为什么子类继承属性的监听依然生效?
3.跨线程的监听
我们知道使用Notification时,跨线程发送通知是无法被接受到的,那么现在看看KVO在多线程中的表现。
// 在两个线程定义目标和观察者
dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__block Student *stu1 =nil;
dispatch_async(concurrentQueue, ^{// 对象属性
stu1 = [[Student alloc] init];
NSLog(@"Student %@",[NSDate new]);
stu1.lastName =@"yyyyyyy";
});
__block StudentKvoObserver *stuObserver1;
dispatch_async(concurrentQueue, ^{
sleep(2);
stuObserver1 = [[StudentKvoObserver alloc] initWithStudent:stu1];
[stuObserver1 addObserverForKeyPath:@"fullName"];
// 子类继承属性依旧被监听
NSLog(@" StudentKvoObserver %@",[NSDate new]); }); dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"dispatch_barrier_async %@",[NSDate new]);
NSLog(@"zzzzzz start%@",[NSDate new]);
stu1.lastName =@"zzzzzz";
NSLog(@"zzzzzz end%@",[NSDate new]); });
输出结果
2016-10-11 10:46:53.319 KVCLearn[3364:331572] Student 2016-10-11 02:46:53 +0000
2016-10-11 10:46:55.324 KVCLearn[3364:331578] StudentKvoObserver 2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.325 KVCLearn[3364:331578] dispatch_barrier_async 2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.325 KVCLearn[3364:331578] zzzzzz start2016-10-11 02:46:55 +0000
2016-10-11 10:46:55.326 KVCLearn[3364:331578] fullName
{
kind = 1;
new = “(null)zzzzzz”;
old = “(null)yyyyyyy”;
}
2016-10-11 10:46:55.326 KVCLearn[3364:331578] zzzzzz end2016-10-11 02:46:55 +0000
可以看到在两个不同的线程里创建的Observer和Target,观察变化也是能够生效的。
这里有一个关于GCD的问题,这里我使用了dispatch_barrier_async,分发到自定义的并发队列上,这时barrier是正常工作的,保证了第三个task在前两个执行完之后执行。但是当我直接使用系统全局的并发队列时,barrier不起作用,不能保证他们的执行顺序。这里希望有高人看见了能解答下。
二、实现探究
1.API接口
Foundation里关于KVO的部分都定义在NSKeyValueObserving.h中,KVO通过以下三个NSObject分类实现。
NSObject(NSKeyValueObserving)
NSObject(NSKeyValueObserverRegistration)
NSObject(NSKeyValueObservingCustomization)
这里会从NSObject (NSKeyValueObserverRegistration) 的 - addObserver:forKeyPath:options:context: 为入口,去一步步分析如何整个KVO的实现方式。
2.先说结论
实现方式:
一个对象在被调用addObserver方法时,会动态创建一个KVO前缀的原类的子类,用来重写所有的setter方法,并且该子类的 - (Class) class 和 - (Class) superclass 方法会被重写,返回父类(原始类)的Class。最后会将当前对象的类改为这个KVO前缀的子类。
比较绕,让我们来看个例子。比如说类Person的实例person调用了addObserver方法时,addObserver方法内部给你创建了一个KVOPerson类,KVOPerson的所有的setter方法会被重写,它的class和superClass方法会被改写成返回Person和NSObject,之后使用 object_setClass 将KVOPerson设置成person的class。
当我们调用person的setName方法时,实际是调用的一个KVOPerson实例的setName方法,但由于重写了class,在外部看不出来其中的差别。在setter方法中,我们在实际值被改变的前后回调用 - (void)willChangeValueForKey:(NSString *)key; 和 - (void)didChangeValueForKey:(NSString *)key; 方法,通知观察者值的变化。
3.代码
源码是来自GNUSetup里的Foundation,据说和apple的实现类似,只是相关API的版本会比较老一些。我们先从addObserver方法开始。
@implementationNSObject(NSKeyValueObserverRegistration)- (void) addObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext{ ....// 1.使用当前类创建GSKVOReplacement对象r = replacementForClass([selfclass]); .... info = (GSKVOInfo*)[selfobservationInfo];if(info ==nil) { info = [[GSKVOInfo alloc] initWithInstance:self]; [selfsetObservationInfo: info];//2.重新设置classobject_setClass(self, [r replacement]); } ....//3.重写replace的setter方法[r overrideSetterFor: aPath];//4.注册当前类和观察者到全局表中[info addObserver: anObserver forKeyPath: aPath options: options context: aContext];}
忽略了一些分支,可以看到主要为上面四个步骤。我们可以一个一个拆开来看。
replacementForClass
// 单例生成一个GSKVOReplacement对象,保证一个类只有一个KVO子类staticGSKVOReplacement *replacementForClass(Class c){ GSKVOReplacement *r; setup(); [kvoLock lock]; r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);if(r ==nil) { r = [[GSKVOReplacement alloc] initWithClass: c];NSMapInsert(classTable, (void*)c, (void*)r); } [kvoLock unlock];returnr;}- (id) initWithClass: (Class)aClass{NSValue*template;NSString*superName;NSString*name; original = aClass; superName =NSStringFromClass(original); name = [@"GSKVO"stringByAppendingString: superName];// 添加前缀template = GSObjCMakeClass(name, superName,nil);// 通过objc_allocateClassPair得到class指针GSObjCAddClasses([NSArrayarrayWithObject: template]);// objc_registerClassPair注册classreplacement =NSClassFromString(name);// 前面动态生成且注册了GSKVO子类,然后就可以通过该方法得到// 添加模板类的一些方法,包括重写class和superClass让对象类型不暴露,// setValue:forkey在数据改变前后加上willChange和didChange方法GSObjCAddClassBehavior(replacement, baseClass);/* Create the set of setter methods overridden.
*/keys = [NSMutableSetnew];returnself;}
object_setClass(self, [r replacement]);
// replace就是新生成的KVOXXX的class@interfaceGSKVOReplacement:NSObject{ Class original;/* The original class */Class replacement;/* The replacement class */NSMutableSet*keys;/* The observed setter keys */}replacement =NSClassFromString(name);// 在initWithClass方法中赋值
overrideSetterFor
重写setter方法,在值改变前后添加上willChange&didChange- (void) overrideSetterFor: (NSString*)aKey{if([keys member: aKey] ==nil) {NSMethodSignature*sig;// 当前key值对应setter的方法签名SEL sel;// 当前key值对应setter的方法名selectorIMP imp;// 当前key值对应setter的函数指针IMPconstchar*type;NSString*a[2];unsignedi;BOOLfound =NO;// 得到setXxxx:和_setXxxx:方法名a[0] = [NSStringstringWithFormat:@"set%@%@:", tmp, suffix]; a[1] = [NSStringstringWithFormat:@"_set%@%@:", tmp, suffix];for(i =0; i <2; i++) {/*
得到方法签名
*/sel =NSSelectorFromString(a[i]); sig = [original instanceMethodSignatureForSelector: sel]; type = [sig getArgumentTypeAtIndex:2];// 第三个参数即入参的类型switch(*type) {// 字符case_C_CHR:case_C_UCHR: imp = [[GSKVOSetter class] instanceMethodForSelector:@selector(setterChar:)];// 返回setterChar:函数的函数指针IMPbreak;// 对象、类、指针case_C_ID:case_C_CLASS:case_C_PTR: imp = [[GSKVOSetter class] instanceMethodForSelector:@selector(setter:)];// 返回setter:函数的函数指针IMP,后面有详解break;break; ....default: imp =0;break; }if(imp !=0) {if(class_addMethod(replacement, sel, imp, [sig methodType]))// 将原sel和新imp加到replacement类中去{ found =YES; }else{NSLog(@"Failed to add setter method for %s to %s", sel_getName(sel), class_getName(original)); } } }if(found ==YES) { [keys addObject: aKey]; } }}
这个步骤是将keypath对应的setter方法重写找出来,把原有的SEL函数名和重写后的实现IMP加入到子类中去。这样做,新生成的子类就有和原父类一样表现了,再加上之前的class替换,在KVO的对外接口上已经没有差别。这里也解释了我一开始的问题,keypath到底指的是什么,其实是setter方法,或者说方法名的后缀。因为我们用@property生成了默认的set方法是满足规范的,所以会将keypath和property关联起来。
// setter方法的实现细节@implementationGSKVOSetter- (void) setter: (void*)val{NSString*key; Class c = [selfclass];void(*imp)(id,SEL,void*); imp = (void(*)(id,SEL,void*))[c instanceMethodForSelector: _cmd]; key = newKey(_cmd);if([c automaticallyNotifiesObserversForKey: key] ==YES) {// pre setting code here[selfwillChangeValueForKey: key]; (*imp)(self, _cmd, val);// post setting code here[selfdidChangeValueForKey: key]; }else{ (*imp)(self, _cmd, val); } RELEASE(key);}
对于这个setter方法的实现,我其实是没大看懂的。 [c instanceMethodForSelector: _cmd]; 这个取到的imp,应该是当前方法的函数指针(GSKVOSetter的setter),后面也是直接调用的该imp实现。没有找到这个setter是如何和原类方法中实际的setter联系起来的,之前通过sig方法签名也只取出了sel,原有实现并没有出现。希望有大牛看到这个能给我解答一下。
-(void) addObserver: forKeyPath: options: context:
这个部分就是观察者的注册了。通过以下类图可以很方便得看到,所有的类的KVO观察都是通过infoTable管理的。以被观察对象实例作key,GSKVOInfo对象为value的形式保存在infoTable表里,每个被观察者实例会对应多个keypath,每个keypath会对应多个observer对象。顺带提一下,关于Notification的实现也类似,也是全局表维护通知的注册监听者和通知名。
GSKVOInfo的结构可以看出来,一个keyPath可以对应有多个观察者。其中观察对象的实例和option打包成GSKVOObservation对象保存在一起。
三、总结
看完了KVO的实现部分,我们再回过头来看开头提到的几个问题。
keyPath是什么
首先keyPath,是对于setter方法的关联,会使用keypath作为后缀去寻找原类的setter方法的方法签名,和实际存取对象和property名称没有关系。所以这也是为什么我们重命名了setter方法之后,没有办法再去使用KVO或KVC了,需要手动调用一次willChangeValue方法。
子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?
因为继承的关系Father <- Son <- KVOSon,当我监听一个父类属性的keyPath的时候,Son实例同样可以通过消息查找找到父类的setter方法,再将该方法加入到KVOSon类当中去。
子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO能否观察到?
由于在overrideSetterFor中,我们是直接通过sel去得到方法签名signature,所以和暴不暴露没啥关系。
子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?
在上一条中知道,其实子类监听父类属性,并不依赖继承,而是通过ISA指针在消息转发的时候能够获取到父类方法就足够。所以当我们重写父类setter方法,相当于在子类定义了该setter函数,在我们去用sel找方法签名时,直接在子类中就拿到了,甚至都不需要去到父类里。所以理解了KVO监听父类属性和继承没有直接联系这一点,就不再纠结set方法是否重写这个问题了。
最后线程安全的部分,没有做深入的研究,在这篇就不多做表述了。在我贴的源码中都去掉了很多枝叶,其中就包括加锁的部分。有兴趣的朋友可以去下面贴的源码地址去看完整版,其中对线程安全的考虑,递归锁、惰性递归锁使用,也是很值得学习的。