KVC 与 KVO 全面总结

本文深入探讨了KVC与KVO的高级应用,包括消息传递、容器操作、与容器类的结合等。通过具体实例展示了如何使用KVC进行灵活的属性操作,并解释了KVO的实现原理及其在容器类观察中的应用。文章旨在为开发者提供更深层次的理解和实践指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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(@"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);
}

内容概要:本文详细探讨了基于MATLAB/SIMULINK的多载波无线通信系统仿真及性能分析,重点研究了以OFDM为代表的多载波技术。文章首先介绍了OFDM的基本原理和系统组成,随后通过仿真平台分析了不同调制方式的抗干扰性能、信道估计算法对系统性能的影响以及同步技术的实现分析。文中提供了详细的MATLAB代码实现,涵盖OFDM系统的基本仿真、信道估计算法比较、同步算法实现和不同调制方式的性能比较。此外,还讨论了信道特征、OFDM关键技术、信道估计、同步技术和系统级仿真架构,并提出了未来的改进方向,如深度学习增强、混合波形设计和硬件加速方案。; 适合人群:具备无线通信基础知识,尤其是对OFDM技术有一定了解的研究人员和技术人员;从事无线通信系统设计开发的工程师;高校通信工程专业的高年级本科生和研究生。; 使用场景及目标:①理解OFDM系统的工作原理及其在多径信道环境下的性能表现;②掌握MATLAB/SIMULINK在无线通信系统仿真中的应用;③评估不同调制方式、信道估计算法和同步算法的优劣;④为实际OFDM系统的设计和优化提供理论依据和技术支持。; 其他说明:本文不仅提供了详细的理论分析,还附带了大量的MATLAB代码示例,便于读者动手实践。建议读者在学习过程中结合代码进行调试和实验,以加深对OFDM技术的理解。此外,文中还涉及了一些最新的研究方向和技术趋势,如AI增强和毫米波通信,为读者提供了更广阔的视野。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值