以下是的一些我们精选的KVO 进阶 —— 源码实现探究
本篇会对 KVO 的实现进行探究,不涉及太多 KVO 的使用方法,但是会有一些使用时的思考。
一、使用上的疑问
1.keyPath 是什么
当我们使用 @property 时候,keyPath 是指的是我们的属性名,实例变量或者是存取方法?
???? 对一个属性值使用 @synthesize 重新定义了存储变量
# import "Person.h"@interface Student : Person @property ( nonatomic, strong ) NSString* mark;@end@implementation Student@synthesize mark = abc;- ( void ) setMark: ( NSString * ) newMark { abc = newMark; }- ( NSString * ) mark { return abc;}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 类 @interface Person : NSObject@property ( nonatomic, strong ) NSString *firstName;@property ( nonatomic, strong ) NSString *lastName;@property ( nonatomic, strong, readonly ) NSString *fullName;- ( void ) setNewInnerName: ( NSString * ) str;@end@interface Person ( ) @property ( nonatomic,strong ) NSString *innerName;@end@implementation Person- ( 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@implementation Student- ( 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 +00002016-10-11 10:46:55.324 KVCLearn [ 3364:331578 ] StudentKvoObserver 2016-10-11 02:46:55 +00002016-10-11 10:46:55.325 KVCLearn [ 3364:331578 ] dispatch_barrier_async 2016-10-11 02:46:55 +00002016-10-11 10:46:55.325 KVCLearn [ 3364:331578 ] zzzzzz start2016-10-11 02:46:55 +00002016-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 方法开始。
@implementation NSObject ( NSKeyValueObserverRegistration ) - ( void ) addObserver: ( NSObject* ) anObserver forKeyPath: ( NSString* ) aPath options: ( NSKeyValueObservingOptions ) options context: ( void* ) aContext{ .... // 1. 使用当前类创建 GSKVOReplacement 对象 r = replacementForClass ( [ self class ] ) ; .... info = ( GSKVOInfo* ) [ self observationInfo ] ; if ( info == nil ) { info = [ [ GSKVOInfo alloc ] initWithInstance: self ] ; [ self setObservationInfo: info ] ; //2. 重新设置 class object_setClass ( self, [ r replacement ] ) ; } .... //3. 重写 replace 的 setter 方法 [ r overrideSetterFor: aPath ] ; //4. 注册当前类和观察者到全局表中 [ info addObserver: anObserver forKeyPath: aPath options: options context: aContext ] ;}
忽略了一些分支,可以看到主要为上面四个步骤。我们可以一个一个拆开来看。
replacementForClass
// 单例生成一个 GSKVOReplacement 对象,保证一个类只有一个 KVO 子类 static GSKVOReplacement *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 ] ; return r;}- ( 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 ( [ NSArray arrayWithObject: template ] ) ;// objc_registerClassPair 注册 class replacement = NSClassFromString ( name ) ;// 前面动态生成且注册了 GSKVO 子类,然后就可以通过该方法得到 // 添加模板类的一些方法,包括重写 class 和 superClass 让对象类型不暴露,// setValue:forkey 在数据改变前后加上 willChange 和 didChange 方法 GSObjCAddClassBehior ( replacement, baseClass ) ; /* Create the set of setter methods overridden. */ keys = [ NSMutableSet new ] ; return self;}
object_setClass ( self, [ r replacement ] ) ;
// replace 就是新生成的 KVOXXX 的 class@interface GSKVOReplacement : 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 的方法名 selector IMP imp;// 当前 key 值对应 setter 的函数指针 IMP const char *type; NSString *a [ 2 ] ; unsigned i; BOOL found = NO; // 得到 setXxxx: 和 _setXxxx: 方法名 a [ 0 ] = [ NSString stringWithFormat: @"set%@%@:", tmp, suffix ] ; a [ 1 ] = [ NSString stringWithFormat: @"_set%@%@:", tmp, suffix ] ; for ( i = 0; i
这个步骤是将 keypath 对应的 setter 方法重写找出来,把原有的 SEL 函数名和重写后的实现 IMP 加入到子类中去。这样做,新生成的子类就有和原父类一样表现了,再加上之前的 class 替换,在 KVO 的对外接口上已经没有差别。这里也解释了我一开始的问题,keypath 到底指的是什么,其实是 setter 方法,或者说方法名的后缀。因为我们用 @property 生成了默认的 set 方法是满足规范的,所以会将 keypath 和 property 关联起来。
// setter 方法的实现细节 @implementation GSKVOSetter- ( void ) setter: ( void* ) val{ NSString *key; Class c = [ self class ] ; 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 [ self willChangeValueForKey: key ] ; ( *imp ) ( self, _cmd, val ) ; // post setting code here [ self didChangeValueForKey: 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
子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO 能否观察到?
由于在 overrideSetterFor 中,我们是直接通过 sel 去得到方法签名 signature,所以和暴不暴露没啥关系。
子类继承父类属性并重写了它的 setter 方法,当这个属性被改变时,KVO 能否观察到?
在上一条中知道,其实子类听父类属性,并不依赖继承,而是通过 ISA 指针在消息转发的时候能够获取到父类方法就足够。所以当我们重写父类 setter 方法,相当于在子类定义了该 setter 函数,在我们去用 sel 找方法签名时,直接在子类中就拿到了,甚至都不需要去到父类里。所以理解了 KVO 听父类属性和继承没有直接联系这一点,就不再纠结 set 方法是否重写这个问题了。
最后线程安全的部分,没有做深入的研究,在这篇就不多做表述了。在我贴的源码中都去掉了很多枝叶,其中就包括加锁的部分。有兴趣的朋友可以去下面贴的源码地址去看完整版,其中对线程安全的考虑,递归锁、惰性递归锁使用,也是很值得学习的。
例子和源码的资料
- 张大千擅长:下列哪一位著名画家擅长画虎A、张
- 中通400客服电话
- 怎么换身份证上的照片
- 专硕和学硕代码
- 中国法律还有死刑吗
- 在六点半用英语怎么说
- 主任医师是什么级别
- 知困下一句:人心齐泰山移人心散请问下一句是什么
- 怎么缩小电脑桌面图标
- 专业技术岗位:事业单位管理岗位如何转专业技术岗位
- 天龙八部中逍遥派
- 苹果平板忘记id及密码怎么办
- 可可以加什么偏旁
- 什么夺目成语四字词语
- 蛋挞底皮酥脆的诀窍
- 中国联通电话号码客服电话是
- 星星还是那颗星星歌曲
- 顺丰有单号查不到物流
- iphone设置短信中心号码失败
- oppo自动开关机在哪里设置
- 灭楚之战有多惨
- 迂回什么折成语
- 案件到法院多久能开庭
- 武汉人流俱佳都巿
- 春雨绵绵的意思
- b的大写字母怎么写
- 物什么什么什么成语
- 现任国家副主席都有谁
- 打印表格怎么铺满a4纸
- 一什么玻璃填量词