KVC, KVO 作为一种魔法贯穿日常Cocoa开发,笔者原先是准备写一篇对其的全面总结,可网络上对其的表面介绍已经够多了,除去基本层面的使用,笔者跟大家谈下平常在网络上没有提及的KVC, 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(@"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

(

)

}

上例中,我们通过调用

- 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);

}

聊聊 KVC 和 KVO 的高阶应用的更多相关文章

  1. Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化

    承接上文:Java函数式编程:一.函数式接口,lambda表达式和方法引用 这次来聊聊函数式编程中其他的几个比较重要的概念和技术,从而使得我们能更深刻的掌握Java中的函数式编程. 本篇博客主要聊聊以 ...

  2. iOS监听模式之KVO、KVC的高阶应用

    KVC, KVO作为一种魔法贯穿日常Cocoa开发,笔者原先是准备写一篇对其的全面总结,可网络上对其的表面介绍已经够多了,除去基本层面的使用,笔者跟大家谈下平常在网络上没有提及的KVC, KVO进阶知 ...

  3. 聊聊React高阶组件(Higher-Order Components)

    使用 react已经有不短的时间了,最近看到关于 react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡.一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低 ...

  4. 迈向高阶:优秀Android程序员必知必会的网络基础

    1.前言 网络通信一直是Android项目里比较重要的一个模块,Android开源项目上出现过很多优秀的网络框架,从一开始只是一些对HttpClient和HttpUrlConnection简易封装使用 ...

  5. 11. KVC And KVO

    1. KVC And KVO  的认识 KVC/KVO是观察者模式的一种实现  KVC全称是Key-value coding,翻译成键值编码.顾名思义,在某种程度上跟map的关系匪浅.它提供了一种使用 ...

  6. c#语言-高阶函数

    介绍 如果说函数是程序中的基本模块,代码段,那高阶函数就是函数的高阶(级)版本,其基本定义如下: 函数自身接受一个或多个函数作为输入. 函数自身能输出一个函数,即函数生产函数. 满足其中一个条件就可以 ...

  7. swift 的高阶函数的使用代码

    //: Playground - noun: a place where people can play import UIKit var str = "Hello, playground& ...

  8. JavaScript高阶函数

    所谓高阶函数(higher-order function) 就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数. 下面的例子接收两个函数f()和g(),并返回一个新的函数用以计算f(g ...

  9. iOS - 详细理解KVC与KVO

    详细理解KVC与KVO 在面试的时候,KVC与KVO有些时候还是会问到的,并且他们都是Objective C的关键概念,在这里我们先做一个简单地介绍: (一)KVC: KVC即指:NSKeyValue ...

随机推荐

  1. 今天把登陆注册给改成tab了

    真是解决了一个心头大患,本来以为必须请外包公司的工程师才做,自己研究了下也给解决了. 多亏去年做原型时学习了smarty.css.html这些最基本的网站前端开发的技术.FTP上传下载.linux的基 ...

  2. 怎样在Win7 64位旗舰版安装Python+Eclipse开发环境

    原地址:http://www.cnblogs.com/balian/archive/2011/06/19/2084632.html 自从上周抛弃了WinXP转而安装了Win7,64位后,尝试安装Pyt ...

  3. 前端性能优化(三)——传统 JavaScript 优化的误区

    注:本文是纯技术探讨文,无图无笑点,希望您喜欢 一.前言 软件行业极其缺乏前端人才这是圈内的共识了,某种程度上讲,同等水平前端的工资都要比后端高上不少,而圈内的另一项共识则是--网页是公司的脸面! 几 ...

  4. XXX is not in the sudoers file.This incident will be reported

    在fefora 10安装xz解压软件时,sudo make install 时,报错: 原因未知,在网上搜索为:权限问题,解决办法如下(来自百度): 解决方法如下: >.进入超级用户模式.也就是 ...

  5. PYTHON--CLASS

    class Robot: population = 0 def __init__(self, name): self.name = name print("(Initializing {0} ...

  6. Android 给ListView设置Adapter

    Adapter: class MyAdapter extends BaseAdapter { private List<Person> personList; public MyAdapt ...

  7. 【网络流24题】No. 17 运输问题 (费用流)

    [题意] W 公司有 m 个仓库和 n 个零售商店.第 i 个仓库有ai 个单位的货物:第 j 个零售商店需要b j 个单位的货物. 货物供需平衡,即SIGMA(A)=SIGMA(B). 从第 i 个 ...

  8. USB otg 学习笔记

    1 USB OTG的工作原理 OTG补充规范对USB2.0的最重要的扩展是其更具节能性的电源管理和允许设备以主机和外设两种形式工作.OTG有两种设备类型:两用OTG设备(Dualrole device ...

  9. 備份Sqlite DB到XML文件:

    转载请注明出处:http://blog.csdn.net/krislight 项目中遇到备份与还原App数据的需求,需要把DB数据备份到一个XML文件中,然后保存到SD卡上,还原的时候直接从XML文件 ...

  10. 【Java基础01】Java InputStream的read方法

    JDK关于InputStream中的read方法的描述: (1) read() :  从输入流中读取数据的下一个字节,返回0到255范围内的int字节值.如果因为已经到达流末尾而没有可用的字节,则返回 ...