iOS监听模式之KVO、KVC的高阶应用
KVC, KVO作为一种魔法贯穿日常Cocoa开发,笔者原先是准备写一篇对其的全面总结,可网络上对其的表面介绍已经够多了,除去基本层面的使用,笔者跟大家谈下平常在网络上没有提及的KVC, KVO进阶知识。旨在分享交流。
目录:
KVC的消息传递
KVC容器操作
KVC与容器类(集合代理对象)
KVO和容器类
KVO的实现原理
KVC的消息传递
valueForKey:的使用并不仅仅用来取值那么简单,还有很多特殊的用法,集合类也覆盖了这个方法,通过调用valueForKey:给容器中每一个对象发送操作消息,并且结果会被保存在一个新的容器中返回,这样我们能很方便地利用一个容器对象创建另一个容器对象。另外,valueForKeyPath:还能实现多个消息的传递。一个例子:
NSArray *array = [NSArray arrayWithObject:@"10.11", @"20.22", nil]; NSArray *resultArray = [array valueForKeyPath:@"doubleValue.intValue"]; NSLog(@"%@", resultArray); //打印结果 ( 10, 20 )
KVC容器操作
容器不仅仅能使用KVC方法实现对容器成员传递普通的操作消息,KVC还定义了特殊的一些常用操作,使用valueForKeyPath:结合操作符来使用,所定义的keyPath格式入下图所示
Left key path:如果有,则代表需要操作的对象路径(相对于调用者)
Collection operator:以"@"开头的操作符
Right key path:指定被操作的属性
常规操作符:
@avg、@count、@max、@min、@sum
对象操作符:
@distinctUnionOfObjects、@unionOfObjects
NSArray *values = [object valueForKeyPath:@"@unionOfObjects.value"];
@distinctUnionOfObjects操作符返回被操作对象指定属性的集合并做去重操作,而@unionOfObjects则允许重复。如果其中任何涉及的对象为nil,则抛出异常。
Array和Set操作符:
Array和Set操作符操作对象是嵌套型的集合对象
@distinctUnionOfArrays、@unionOfArrays
NSArray *values = [arrayOfobjectsArrays valueForKeyPath:@"@distinctUnionOfArrays.value"];
同样的,返回被操作集合下的集合中的对象的指定属性的集合,并且做去重操作,而@unionOfObjects则允许重复。如果其中任何涉及的对象为nil,则抛出异常。
@distinctUnionOfSets
NSSet *values = [setOfobjectsSets valueForKeyPath:@"@distinctUnionOfSets.value"];
返回结果同理于NSArray。
据官方文档说明,目前还不支持自动以操作符。
KVC与容器类(集合代理对象)
当然对象的属性可以是一对一的,也可以是一对多。属性的一对多关系其实就是一种对容器类的映射。如果有一个名为numbers的数组属性,我们可以使用valueForKey:@"numbers"来获取,这个是没问题的,但KVC还能使用更灵活的方式管理集合。——集合代理对象
ElfinsArray.h @interface ElfinsArray : 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", (unsigned long)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" ) }
问题来了,ElfinsArray中并没有定义elfins属性,那么elfins数组从何而来?valueForKey:有如下的搜索规则:
按顺序搜索getVal、val、isVal,第一个被找到的会用作返回。
countOfVal,或者objectInValAtIndex:与valAtIndexes其中之一,这个组合会使KVC返回一个代理数组。
countOfVal、enumeratorOfVal、memberOfVal。这个组合会使KVC返回一个代理集合。
名为val、isVal、val、isVal的实例变量。到这一步时,KVC会直接访问实例变量,而这种访问操作破坏了封装性,我们应该尽量避免,这可以通过重写+accessInstanceVariablesDirectly返回NO来避免这种行为。
ok上例中我们实现了第二条中的特殊命名函数组合:
- (NSUInteger)countOfElfins; - (id)objectInElfinsAtIndex:(NSUInteger)index;
这使得我们调用valueForKey:@"elfins"时,KVC会为我们返回一个可以响应NSArray所有方法的代理数组对象(NSKeyValueArray),这是NSArray的子类,- (NSUInteger)countOfElfins决定了这个代理数组的容量,- (id)objectInElfinsAtIndex:(NSUInteger)index决定了代理数组的内容。本例中使用的key是elfins,同理的如果key叫human,KVC就会去寻找-countOfHuman:
可变容器呢
当然我们也可以在可变集合(NSMutableArray、NSMutableSet、NSMutableOrderedSet)中使用集合代理:
这个例子我们不再使用KVC给我们生成代理数组,因为我们是通过KVC拿到的,而不能主动去操作它(insert/remove),我们声明一个可变数组属性elfins。
ElfinsArray.h @interface ElfinsArray : NSObject @property (strong ,nonatomic) NSMutableArray *elfins; - (void)insertObject:(id)object inNumbersAtIndex:(NSUInteger)index; - (void)removeObjectFromNumbersAtIndex:(NSUInteger)index; @end ElfinsArray.m #import "ElfinsArray.h" @implementation ElfinsArray - (void)insertObject:(id)object inElfinsAtIndex:(NSUInteger)index { [self.elfins insertObject:object atIndex:index]; NSLog(@"insert %@\n", object); } - (void)removeObjectFromElfinsAtIndex:(NSUInteger)index { [self.elfins removeObjectAtIndex:index]; NSLog(@"remove\n"); } @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 ( ) }
上例中,我们通过调用
- mutableArrayValueForKey: - mutableSetValueForKey: - mutableOrderedSetValueForKey:
KVC会给我们返回一个代理可变容器delegateElfins,通过对代理可变容器的操作,KVC会自动调用合适KVC方法(如下):
//至少实现一个insert方法和一个remove方法
- insertObject:inValAtIndex: - removeObjectFromValAtIndex: - insertVal:atIndexes: - removeValAtIndexes:
间接地对被代理对象操作。
还有一组更强大的方法供参考
- replaceObjectInValAtIndex:withObject: - replaceValAtIndexes:withVal:
我认为这就是KVC结合KVO的结果。这里我尝试研究下了文档中的如下两个方法,还没有什么头绪,知道的朋友可否告诉我下
- willChange:valuesAtIndexes:forKey: - didChange:valuesAtIndexes:forKey:
KVO和容器类
要注意,对容器类的观察与对非容器类的观察并不一样,不可变容器的内容发生改变并不会影响他们所在的容器,可变容器的内容改变&内容增删也都不会影响所在的容器,那么如果我们需要观察某容器中的对象,首先我们得观察容器内容的变化,在容器内容增加时添加对新内容的观察,在内容移除同时移除对该内容的观察。
既然容器内容数量改变和内容自身改变都不会触发容器改变,此时对容器属性施加KVO并没有效果,那么怎么实现对容器变化(非容器改变)的观察呢?上面所介绍的代理容器能帮到我们:
//我们通过KVC拿到容器属性的代理对象 NSMutableArray *delegateElfins = [ElfinsArray mutableArrayValueForKey:@"elfins"]; [delegateElfins addObject:@"小精灵10"];
当然这样做的前提是要实现insertObject:inValAtIndex:和removeObjectFromValAtIndex:两个方法。如此才能触发observeValueForKeyPath:ofObject:change:context:的响应。
而后,我们就可以轻而易举地在那两个方法实现内对容器新成员添加观察/对容器废弃成员移除观察。
KVO的实现原理
写到这里有点犯困,估计广州的春天真的来了。对于KVO的实现原理就不花笔墨再描述了,网络上哪里都能找到,这里借网上一张图来偷懒带过。
在我们了解明白实现原理的前提下,我们可以自己来尝试模仿,那么我们从哪里下手呢?先来准备一个新子类的setter方法:
- (void)notifySetter:(id)newValue { NSLog(@"我是新的setter"); }
setter的实现先留空,下面再详细说,紧接着,我们直接进入主题,runtime注册一个新类,并且让被监听类的isa指针指向我们自己伪造的类,为了大家看得方便,笔者就不做封装了,所有直接写在一个方法内:
- (Class)configureKVOSubClassWithSourceClassName:(NSString *)className observeProperty:(NSString *)property { NSString *prefix = @"NSKVONotifying_"; NSString *subClassName = [prefix stringByAppendingString:className]; //1 Class originClass = [KVOTargetClass class]; Class dynaClass = objc_allocateClassPair(originClass, subClassName.UTF8String, 0); //重写property对应setter NSString *propertySetterString = [@"set" stringByAppendingString:[[property substringToIndex:1] uppercaseString]]; propertySetterString = [propertySetterString stringByAppendingString:[property substringFromIndex:1]]; propertySetterString = [propertySetterString stringByAppendingString:@":"]; SEL setterSEL = NSSelectorFromString(propertySetterString); //2 Method setterMethod = class_getInstanceMethod(originClass, setterSEL); const char types = method_getTypeEncoding(setterMethod); class_addMethod(dynaClass, setterSEL, class_getMethodImplementation([self class], @selector(notifySetter:)), types); objc_registerClassPair(dynaClass); return dynaClass; }
我们来看
//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实现:
- (void)notifySetter:(id)newValue { NSLog(@"我是新的setter"); struct objc_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]; [self willChangeValueForKey:propertyName]; //调用super的setter //1 void (*objc_msgSendSuperKVO)(void * class, SEL _cmd, id value) = (void *)objc_msgSendSuper; //2 objc_msgSendSuperKVO(&originClass, _cmd, newValue); [self didChangeValueForKey:propertyName]; }
我们轻而易举地让willChangeValueForKey:和didChangeValueForKey:包裹了对newValue的修改。
这里需要提的是:
//1处,在IOS8后,我们不能直接使用objc_msgSend()或者objc_msgSendSuper()来发送消息,我们必须自定义一个msg_Send函数并提供具体类型来使用。
//2处,至于objc_msgSendSuper(struct objc_super *, SEL, ...),第一个参数我们需要提供一个objc_super结构体,我们command跳进去来看看这个结构体:
/// Specifies the superclass of an instance. struct objc_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 Class class; #else __unsafe_unretained Class super_class; #endif /* super_class is the first class to search */ }; #endif
第一个成员receiver表示某个类的实例,第二个成员super_class代表当前类的父类,也就是这里接受消息目标的类。
工作已经完成了,可以随便玩了:
- (void)main { KVOTargetClass *kvoObject = [[KVOTargetClass alloc] init]; NSString *targetClassName = NSStringFromClass([KVOTargetClass class]); Class subClass = [self configureKVOSubClassWithSourceClassName:targetClassName observeProperty:@"name"]; object_setClass(kvoObject, subClass); [kvoObject setName:@"haha"]; NSLog(@"property -- %@", kvoObject.name); }
KVO验证笔者就懒得验了,有兴趣的朋友可以试试。最后,感谢!
参考文献
NSKeyValueObserving Protocol
Reference
iOS监听模式之KVO、KVC的高阶应用的更多相关文章
- iOS监听模式系列之键值编码KVC、键值监听KVO的简单介绍和应用
键值编码KVC 我们知道在C#中可以通过反射读写一个对象的属性,有时候这种方式特别方便,因为你可以利用字符串的方式去动态控制一个对象.其实由于ObjC的语言特性,你根部不必进行任何操作就可以进行属性的 ...
- iOS监听模式系列之关于delegate(代理,委托)的学习
首先,大家应该都明白的是委托是协议的一种,顾名思义,就是委托他人帮自己去做什么事.也就是当自己做什么事情不方便的时候,就可以建立一个委托,这样就可以委托他人帮自己去实现什么方法. 其次,我简单的总结了 ...
- iOS监听模式系列之通知中心
补充--通知中心 对于很多初学者往往会把iOS中的本地通知.推送通知和iOS通知中心的概念弄混.其实二者之间并没有任何关系,事实上它们都不属于一个框架,前者属于UIKit框架,后者属于Foundati ...
- iOS监听模式系列之IOS中的几中观察监听模式
本文介绍Objective C中实现观察者模式(也被称为广播者/监听者.发布/注册或者通知)的五种方法以及每种方法的价值所在. 该文章将包括: 1 手动广播者和监听者(Broadcaster and ...
- iOS监听模式系列之iOS开发证书、秘钥
补充--iOS开发证书.秘钥 iOS开发过程中如果需要进行真机调试.发布需要注册申请很多证书,对于初学者往往迷惑不解,再加上今天的文章中会牵扯到一些特殊配置,这里就简单的对iOS开发的常用证书和秘钥等 ...
- iOS监听模式系列之推送消息通知
推送通知 和本地通知不同,推送通知是由应用服务提供商发起的,通过苹果的APNs(Apple Push Notification Server)发送到应用客户端.下面是苹果官方关于推送通知的过程示意图: ...
- iOS监听模式系列之本地通知Notification
本地通知 本地通知是由本地应用触发的,它是基于时间行为的一种通知形式,例如闹钟定时.待办事项提醒,又或者一个应用在一段时候后不使用通常会提示用户使用此应用等都是本地通知.创建一个本地通知通常分为以下几 ...
- iOS监听模式系列之对APNs的认知与理解
前言: APNs 协议在近两年的 WWDC 上改过两次, 15 年 12 月 17 日更是推出了革命性的新特性.但在国内传播的博客.面试题里关于 APNs 的答案全都是旧的.错的. 导航: 对 APN ...
- iOS监听模式系列之NSNotificationCenter的简单使用
NSNotificationCenter 对于这个没必要多说,就是一个消息通知机制,类似广播.观察者只需要向消息中心注册感兴趣的东西,当有地方发出这个消息的时候,通知中心会发送给注册这个消息的对象.这 ...
随机推荐
- ejabberd编译更新脚本
ejabberd编译更新脚本 (金庆的专栏 2016.8) 用rebar编译ejabberd源码,然后复制编译所得beam文件到ejabberd安装目录, 调用ejabberdctl热更新. call ...
- 代码优化>>>Android ListView适配器三级优化详解
转载本专栏每一篇博客请注明转载出处地址,尊重原创.此博客转载链接地址:点击打开链接 http://blog.csdn.net/qq_32059827/article/details/52718489 ...
- UNIX网络编程——原始套接字的魔力【续】
如何从链路层直接发送数据帧 上一篇里面提到的是从链路层"收发"数据,该篇是从链路层发送数据帧. 上一节我们主要研究了如何从链路层直接接收数据帧,可以通过bind函数来将原始套接字绑 ...
- Libgdx教程目录
Libgdx教程 Note:本教程用的Libgdx 1.9.2版本,在教程更新完毕之前应该不会更新版本.之前的博客中也发表过Libgdx的内容,不过当时都从别人那里拷贝的,因此现在想做一个Libgdx ...
- Shell在大数据时代的魅力:从一道百度大数据面试题想到的点滴
对于在Linux下开发的同学来说,Shell可以说是一种基本功. 对于运维的同学来说,Shell可以说是一种必备的技能,而且应该要非常熟练的书写Shell.对于Release Team,软件配置管理的 ...
- Dynamics CRM2015 2015版本可用的OData Query Designer工具
2015后很多工具无法使用,包括2011版的OData Query Designer,这里介绍一款可用的工具,Dynamics XRM Tools for CRM 2015,下载地址:https:// ...
- Linux下使用两个线程协作完成一个任务的简易实现
刚解决了之前的那个Linux下Pthread库的问题,这次就来使用两个线程来协作,共同完成一个求和的任务. 打一下基础吧 本次需要使用到的知识点有: lpthread,编译的时候用到,相当于一个声明的 ...
- 【UNIX网络编程第三版】阅读笔记(一):代码环境搭建
粗略的阅读过<TCP/IP详解>和<计算机网络(第五版)>后,开始啃这本<UNIX网络编程卷一:套接字联网API>,目前linux下的编程不算太了解,在阅读的过程中 ...
- Ubuntu sudo 免密码之 sudoers 修改
Ubuntu sudo 免密码之 sudoers 修改 重要提示: 本文内容仅在虚拟机上实验通过.如果你不确信这个过程,请不要擅自改变/etc/sudoers.否则可能导致你的机器不可用.本文对由此产 ...
- Oracle 中PLSQL的ftp应用
CREATE OR REPLACE PACKAGE BODY ftp AS -- ----------------------------------------------------------- ...