聊聊 KVC 和 KVO 的高阶应用
(点击上方公众号,可快速关注)来源:伯乐在线-智艺链接:http://ios.jobbole.com/8...
来源:伯乐在线 - 智艺
KVC, KVO 作为一种魔法贯穿日常Cocoa开发,笔者原先是准备写一篇对其的全面总结,可网络上对其的表面介绍已经够多了,除去基本层面的使用,笔者跟大家谈下平常在网络上没有提及的KVC, KVO进阶知识。旨在分享交流。链接:http://ios.jobbole.com/84951/
点击 → 了解如何加入专栏作者
KVC的消息传递
valueForKey:的使用并不仅仅用来取值那么简单,还有很多特殊的用法,集合类也覆盖了这个方法,通过调用valueForKey:给容器中每一个对象发送操作消息,并且结果会被保存在一个新的容器中返回,这样我们能很方便地利用一个容器对象创建另一个容器对象。另外,valueForKeyPath:还能实现多个消息的传递。一个例子:
KVC容器操作NSArray *array = [NSArray arrayWithObject:@"10.11",
@"20.22",nil];
NSArray *resultArray = [arrayvalueForKeyPath:@"doubleValue.intValue"];
NSLog(@"%@",resultArray);
//打印结果
(
10,
20
)
容器不仅仅能使用KVC方法实现对容器成员传递普通的操作消息,KVC还定义了特殊的一些常用操作,使用valueForKeyPath:结合操作符来使用,所定义的keyPath格式入下图所示
Left key path:如果有,则代表需要操作的对象路径(相对于调用者)
Collection operator:以”@”开头的操作符
Right key path:指定被操作的属性
常规操作符:
- @avg、@count、@max、@min、@sum
- @distinctUnionOfObjects、@unionOfObjects
@distinctUnionOfObjects操作符返回被操作对象指定属性的集合并做去重操作,而@unionOfObjects则允许重复。如果其中任何涉及的对象为nil,则抛出异常。NSArray *values = [objectvalueForKeyPath:@"@unionOfObjects.value"];
Array和Set操作符:
Array和Set操作符操作对象是嵌套型的集合对象
- @distinctUnionOfArrays、@unionOfArrays
NSArray *values = [arrayOfobjectsArrays valueForKeyPath:@"@distinctUnionOfArrays.value"];
同样的,返回被操作集合下的集合中的对象的指定属性的集合,并且做去重操作,而@unionOfObjects则允许重复。如果其中任何涉及的对象为nil,则抛出异常。
- @distinctUnionOfSets
返回结果同理于NSArray。NSSet *values = [setOfobjectsSets valueForKeyPath:@"@distinctUnionOfSets.value"];
据官方文档说明,目前还不支持自动以操作符。
KVC与容器类(集合代理对象)
当然对象的属性可以是一对一的,也可以是一对多。属性的一对多关系其实就是一种对容器类的映射。如果有一个名为numbers的数组属性,我们可以使用valueForKey:@"numbers"来获取,这个是没问题的,但KVC还能使用更灵活的方式管理集合。——集合代理对象
问题来了,ElfinsArray中并没有定义elfins属性,那么elfins数组从何而来?valueForKey:有如下的搜索规则:ElfinsArray.h
@interfaceElfinsArray : NSObject
@property(assign,nonatomic)NSUInteger count;
- (NSUInteger)countOfElfins;
- (id)objectInElfinsAtIndex:(NSUInteger)index;
@end
ElfinsArray.m
#import "ElfinsArray.h"
@implementation ElfinsArray
- (NSUInteger)countOfElfins{
return self.count;
}
- (id)objectInElfinsAtIndex:(NSUInteger)index{
return[NSString stringWithFormat:@"小精灵%lu",(unsignedlong)index];
}
@end
Main.m
- (void)work{
ElfinsArray *elfinsArr = [ElfinsArray alloc]init];
elfinsArr.count = 3;
NSArray *elfins = [ElfinsArray valueForKey:@"elfins"];
//elfins为KVC代理数组
NSLog(@"%@",elfins);
//打印结果
(
"小精灵0",
"小精灵1",
"小精灵2"
)
}
- 按顺序搜索getVal、val、isVal,第一个被找到的会用作返回。
- countOfVal,或者objectInValAtIndex:与valAtIndexes其中之一,这个组合会使KVC返回一个代理数组。
- countOfVal、enumeratorOfVal、memberOfVal。这个组合会使KVC返回一个代理集合。
- 名为val、isVal、val、isVal的实例变量。到这一步时,KVC会直接访问实例变量,而这种访问操作破坏了封装性,我们应该尽量避免,这可以通过重写+accessInstanceVariablesDirectly返回NO来避免这种行为。
这使得我们调用valueForKey:@"elfins"时,KVC会为我们返回一个可以响应NSArray所有方法的代理数组对象(NSKeyValueArray),这是NSArray的子类,- (NSUInteger)countOfElfins决定了这个代理数组的容量,- (id)objectInElfinsAtIndex:(NSUInteger)index决定了代理数组的内容。本例中使用的key是elfins,同理的如果key叫human,KVC就会去寻找-countOfHuman:- (NSUInteger)countOfElfins;
- (id)objectInElfinsAtIndex:(NSUInteger)index;
可变容器呢
当然我们也可以在可变集合(NSMutableArray、NSMutableSet、NSMutableOrderedSet)中使用集合代理:
这个例子我们不再使用KVC给我们生成代理数组,因为我们是通过KVC拿到的,而不能主动去操作它(insert/remove),我们声明一个可变数组属性elfins。
上例中,我们通过调用ElfinsArray.h
@interfaceElfinsArray : NSObject
@property(strong,nonatomic)NSMutableArray *elfins;
- (void)insertObject:(id)objectinNumbersAtIndex:(NSUInteger)index;
- (void)removeObjectFromNumbersAtIndex:(NSUInteger)index;
@end
ElfinsArray.m
#import "ElfinsArray.h"
@implementation ElfinsArray
- (void)insertObject:(id)objectinElfinsAtIndex:(NSUInteger)index{
[self.elfins insertObject:objectatIndex:index];
NSLog(@"insert %@n",object);
}
- (void)removeObjectFromElfinsAtIndex:(NSUInteger)index{
[self.elfins removeObjectAtIndex:index];
NSLog(@"removen");
}
@end
Main.m
- (void)work{
ElfinsArray *elfinsArr = [ElfinsArray alloc]init];
elfinsArr.elfins = [NSMutableArray array];
NSMutableArray *delegateElfins = [ElfinsArray mutableArrayValueForKey:@"elfins"];
//delegateElfins为KVC代理可变数组,非指向elfinsArr.elfins
[delegateElfins insertObject:@"小精灵10"atIndex:0];
NSLog(@"first log n %@",elfinsArr.elfins);
[delegateElfins removeObjectAtIndex:0];
NSLog(@"second log n %@",elfinsArr.elfins);
//打印结果
insert小精灵10
first log
(
"小精灵10"
)
remove
second log
(
)
}
KVC会给我们返回一个代理可变容器delegateElfins,通过对代理可变容器的操作,KVC会自动调用合适KVC方法(如下):- mutableArrayValueForKey:
- mutableSetValueForKey:
- mutableOrderedSetValueForKey:
间接地对被代理对象操作。//至少实现一个insert方法和一个remove方法
- insertObject:inValAtIndex:
- removeObjectFromValAtIndex:
- insertVal:atIndexes:
- removeValAtIndexes:
还有一组更强大的方法供参考
我认为这就是KVC结合KVO的结果。这里我尝试研究下了文档中的如下两个方法,还没有什么头绪,知道的朋友可否告诉我下- replaceObjectInValAtIndex:withObject:
- replaceValAtIndexes:withVal:
KVO和容器类- willChange:valuesAtIndexes:forKey:
- didChange:valuesAtIndexes:forKey:
要注意,对容器类的观察与对非容器类的观察并不一样,不可变容器的内容发生改变并不会影响他们所在的容器,可变容器的内容改变&内容增删也都不会影响所在的容器,那么如果我们需要观察某容器中的对象,首先我们得观察容器内容的变化,在容器内容增加时添加对新内容的观察,在内容移除同时移除对该内容的观察。
既然容器内容数量改变和内容自身改变都不会触发容器改变,此时对容器属性施加KVO并没有效果,那么怎么实现对容器变化(非容器改变)的观察呢?上面所介绍的代理容器能帮到我们:
当然这样做的前提是要实现insertObject:inValAtIndex:和removeObjectFromValAtIndex:两个方法。如此才能触发observeValueForKeyPath:ofObject:change:context:的响应。//我们通过KVC拿到容器属性的代理对象
NSMutableArray *delegateElfins = [ElfinsArray mutableArrayValueForKey:@"elfins"];
[delegateElfins addObject:@"小精灵10"];
而后,我们就可以轻而易举地在那两个方法实现内对容器新成员添加观察/对容器废弃成员移除观察。
KVO的实现原理
写到这里有点犯困,估计广州的春天真的来了。对于KVO的实现原理就不花笔墨再描述了,网络上哪里都能找到,这里借网上一张图来偷懒带过。
在我们了解明白实现原理的前提下,我们可以自己来尝试模仿,那么我们从哪里下手呢?先来准备一个新子类的setter方法:
setter的实现先留空,下面再详细说,紧接着,我们直接进入主题,runtime注册一个新类,并且让被监听类的isa指针指向我们自己伪造的类,为了大家看得方便,笔者就不做封装了,所有直接写在一个方法内:- (void)notifySetter:(id)newValue{
NSLog(@"我是新的setter");
}
我们来看- (Class)configureKVOSubClassWithSourceClassName:(NSString *)className observeProperty:(NSString *)property{
NSString *prefix = @"NSKVONotifying_";
NSString *subClassName = [prefix stringByAppendingString:className];
//1
ClassoriginClass = [KVOTargetClass class];
ClassdynaClass = objc_allocateClassPair(originClass,subClassName.UTF8String,0);
//重写property对应setter
NSString *propertySetterString = [@"set"stringByAppendingString:[[propertysubstringToIndex:1]uppercaseString]];
propertySetterString = [propertySetterString stringByAppendingString:[propertysubstringFromIndex:1]];
propertySetterString = [propertySetterString stringByAppendingString:@":"];
SEL setterSEL = NSSelectorFromString(propertySetterString);
//2
Method setterMethod = class_getInstanceMethod(originClass,setterSEL);
constchartypes = method_getTypeEncoding(setterMethod);
class_addMethod(dynaClass,setterSEL,class_getMethodImplementation([selfclass],@selector(notifySetter:)),types);
objc_registerClassPair(dynaClass);
returndynaClass;
}
//1处,我们要创建一个新的类,可以通过objc_allocateClassPair来创建这个新类和他的元类,第一个参数需提供superClass的类对象,第二个参数接受新类的类名,类型为const char *,通过返回值我们得到dynaClass类对象。
//2处,我们希望为我们的伪造的类添加跟被观察类一样只能的setter方法,我们可以借助被观察类,拿到类型编码信息,通过class_addMethod,注入我们自己的setter方法实现:class_getMethodImplementation([self class], @selector(notifySetter:)),最后通过objc_registerClassPair完成新类的注册!。
可能有朋友会问class_getMethodImplementation中获取IMP的来源[self class]的self是指代什么?其实就是指代我们自己的setter(notifySetter:)IMP实现所在的类,指代从哪个类可以找到这个IMP,笔者这里是直接开一个新工程,在ViewController里就开干的,notifySetter:和这个手术方法configureKVOSubClassWithSourceClassName: observeProperty:所在的地方就是VC,因此self指向的就是这个VC实例,也就是这个手术方法的调用者。
不用怀疑,经过手术后对KVOTargetClass对应属性的修改,就会进入到我们伪装的setter,下面我们来完成先前留空的setter实现:
我们轻而易举地让willChangeValueForKey:和didChangeValueForKey:包裹了对newValue的修改。- (void)notifySetter:(id)newValue{
NSLog(@"我是新的setter");
structobjc_super originClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
NSString *setterName = NSStringFromSelector(_cmd);
NSString *propertyName = [setterName substringFromIndex:3];
propertyName = [[propertyName substringToIndex:propertyName.length - 1]lowercaseString];
[selfwillChangeValueForKey:propertyName];
//调用super的setter
//1
void(*objc_msgSendSuperKVO)(void * class,SEL _cmd,id value) = (void *)objc_msgSendSuper;
//2
objc_msgSendSuperKVO(&originClass,_cmd,newValue);
[selfdidChangeValueForKey:propertyName];
}
这里需要提的是:
//1处,在IOS8后,我们不能直接使用objc_msgSend()或者objc_msgSendSuper()来发送消息,我们必须自定义一个msg_Send函数并提供具体类型来使用。
//2处,至于objc_msgSendSuper(struct objc_super *, SEL, ...),第一个参数我们需要提供一个objc_super结构体,我们command跳进去来看看这个结构体:
第一个成员receiver表示某个类的实例,第二个成员super_class代表当前类的父类,也就是这里接受消息目标的类。/// Specifies the superclass of an instance.
structobjc_super{
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) & !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Classclass;
#else
__unsafe_unretained Classsuper_class;
#endif
/* super_class is the first class to search */
};
#endif
工作已经完成了,可以随便玩了:
KVO验证笔者就懒得验了,有兴趣的朋友可以试试。最后,感谢!- (void)main{
KVOTargetClass *kvoObject = [[KVOTargetClass alloc]init];
NSString *targetClassName = NSStringFromClass([KVOTargetClass class]);
ClasssubClass = [selfconfigureKVOSubClassWithSourceClassName:targetClassName observeProperty:@"name"];
object_setClass(kvoObject,subClass);
[kvoObject setName:@"haha"];
NSLog(@"property -- %@",kvoObject.name);
}
专栏作者简介 ( 点击 → 加入专栏作者 )
智艺 :iOS忠实RD,热爱分享交流,一直在寻找一个好归属
关注 iOS大全
微信扫一扫关注公众号