iOS KVC、KVO、通知

本文详细介绍了Objective-C中的KVC(键值编码)、KVO(键值观察)及通知机制的原理与应用。KVC允许通过字符串间接访问对象属性;KVO提供了属性值更改时自动通知观察者的机制;通知机制则是一种广播方式,用于对象间的消息传递。

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

一、KVC

 KVC即是指 NSKeyValueCoding(键值编码),KVC提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的的属性方法或成员变量。不是直接调用getter 和 setter方法。通常我们使用valueForKey 来替代getter 方法,setValue:forKey来代替setter方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定

KVC底层实现过程:

当一个对象调用setValue方法时,方法内部会做出以下操作:

1.检查相应key值是否存在setter方法,如果存在调用setter方法

2.如果setter方法不存在,就在查找与key相同的名称并且带下划线的成员变量,如果有则直接给成员属性赋值。

3.如果没有找到_key 就会查找相同属性名称的key,如果有就直接赋值。

4.如果还没有找到则调用valueForUndefiedKey:和setValue:forUndefinedkey方法方法

这些方法的的实现都是抛出异常,我们根据需求去重写他们。

KVC 的使用场景:1.动态的取值和设值 2.model和字典的转化 3.用KVC来访问和修饰私有变量

下面给大家举个例子

这里有一个Person类,这个类有一个name属性,用property系统会自动实现其的setter和getter方法,可直接用点语法调用,否则需要手动实现setter和getter方法

Person 的 .h方法

@interface Person : NSObject
{
    NSString * _name;
}
//name的setter和getter方法
- (NSString *)name;
- (void)setName:(NSString *) name;
//用property系统会自动实现age的setter和getter方法,可直接用点语法调用
@property (nonatomic,assign) NSInteger age;
@end

Person 的.m方法

@implementation Person
-(NSString *)name {
    return _name;
}
- (void)setName:(NSString *)name {
    _name = name;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person * person1 = [[Person alloc] init];
    [person1 setValue:@"小明" forKey:@"name"];
    NSString * person1Name = [person1 valueForKey:@"name"];
    NSLog(@"person1Name = %@",person1Name);
    
    [person1 setValue:@"160" forKey:@"age"];
    NSString * person1Height = [person1 valueForKey:@"age"];
     NSLog(@"person1Height = %@",person1Height);
    
    Person * person2 = [[Person alloc] init];
    person2.name = @"小红";
    person2.age = 175.0;
    NSString * person2Name = person2.name;
    NSLog(@"person2Name = %@",person2Name);
    
    
    CGFloat person2Height = person2.age;
    NSLog(@"person2Height = %lf",person2Height);
    
}

看看下面的控制台,相信你会理解KVC

二、KVO

Cocoa Touch 框架中通知和 KVO 都实现了观察者模式。通知是由一个中心对象为所有观察者提供变更通知,KVO 是被观察的对象直接向观察者发送通知。

KVO是依赖于KVC,基于runtime机制实现的。

  1. 动态创建子类:为被观察对象的类创建一个NSKVONotifying_原类名的子类。
  2. 修改 isa 指针:将被观察对象的isa指针指向这个新创建的子类,使其在运行时变为该子类的实例。
  3. 重写被观察属性的 setter 方法:在子类中重写被观察属性的 setter 方法,插入通知逻辑。
  4. 触发通知:当属性值变化时,通过新的 setter 方法触发 KVO 通知。

当某个A类的属性对象第一次被观察时,系统就会在运行期间动态地创建该类的一个派生类B,B继承A,将A类的isa指针指向B类,在这个派生类B中重写被观察的属性的setter方法 ,重写的setter方法会在调用原setter方法前后,通知观察对象值的改变。

关键步骤详解:

1. 动态子类的创建

当你调用addObserver:forKeyPath:options:context:时,运行时会检查被观察对象的类是否有对应的 KVO 子类。如果没有,则创建一个:

  • 子类名称格式为NSKVONotifying_原类名(例如NSKVONotifying_Person
  • 这个子类继承自原类,并在运行时动态添加到系统中。
2. isa 指针的修改

KVO 通过修改被观察对象的isa指针,使其指向新创建的子类,从而改变对象的类型:这使得对象在运行时表现为子类的实例,但其实际数据结构仍保持不变。

3. setter 方法的重写

在新创建的子类中,运行时会重写被观察属性的 setter 方法,插入 KVO 通知逻辑:

// 伪代码:重写的setter方法
- (void)setName:(NSString *)newName {
    [self willChangeValueForKey:@"name"];
    [super setName:newName];
    [self didChangeValueForKey:@"name"];
}

这两个方法(willChangeValueForKey:didChangeValueForKey:)是触发 KVO 通知的核心。

4. 通知的触发

当属性值发生变化时,重写的 setter 方法会调用:

willChangeValueForKey::标记属性即将变化

  1. willChangeValueForKey::标记属性即将变化。
  2. 调用父类的原始 setter 方法:实际修改属性值。
  3. didChangeValueForKey::检查属性值是否真的变化,并触发观察者的observeValueForKeyPath:ofObject:change:context:方法。

四、KVO 的局限性

  1. 仅适用于属性:KVO 只能观察对象的属性(即通过@property声明的变量),无法直接观察普通成员变量。
  2. 依赖 setter 方法:必须通过 setter 方法修改属性才能触发 KVO。如果直接访问实例变量(如_name = @"newName"),KVO 不会触发。
  3. 内存管理风险:如果观察者在被观察对象之前释放,而没有正确移除 KVO 观察,会导致野指针异常。
  4. 性能开销:动态创建子类和修改 isa 指针会带来一定的性能开销,尤其在频繁添加 / 移除观察时。

五、与 KVC 的关系

KVO 依赖于 KVC(Key-Value Coding)的实现,因为:

  • KVO 通过键路径(KeyPath)定位要观察的属性。
  • willChangeValueForKey:didChangeValueForKey:内部使用 KVC 来获取属性的新旧值。

六、手动触发 KVO

你可以通过手动调用willChangeValueForKey:didChangeValueForKey:来触发 KVO 通知,即使属性值没有实际变化:

[person willChangeValueForKey:@"name"];
[person didChangeValueForKey:@"name"];
// 这会强制触发观察者的回调,即使name属性值未变

KVO提供了一种机制,当指定的被观察的对象的属性被修改后,KVO会自动通知响应的观察者。

直接上代码:这里以两个label为例,点击btn按钮,改变label1的text,通过KVO监听模式,使label2的text和label1一样。

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic,strong) UILabel * label1;
@property (nonatomic,strong) UIButton * button;
@property (nonatomic,strong) UILabel * label2;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createUI];

//(注册监听对象。anObserver指监听者,keyPath就是要监听的属性值,而context方便传输你需要的数据,它是个指针类型。 

  [self.label1 addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void) createUI {
    [self.view addSubview:self.label1];
    [self.view addSubview:self.label2];
    [self.view addSubview:self.button];
}

- (UILabel *)label1 {
    if (!_label1) {
        _label1 = [[UILabel alloc] initWithFrame:CGRectMake(50, 30, 200, 30)];
        _label1.text = @"我是测试label";
        _label1.backgroundColor = [UIColor cyanColor];
    }
    return _label1;
}

- (UILabel *)label2 {
    if (!_label2) {
        _label2 = [[UILabel alloc] initWithFrame:CGRectMake(50, 80, 200, 30)];
        _label2.text = @"我是测试label";
        _label2.backgroundColor = [UIColor cyanColor];
    }
    return _label2;
}

- (UIButton *)button {
    if (!_button) {
        _button = [[UIButton alloc] initWithFrame:CGRectMake(100, 120, 60, 30)];
        _button.backgroundColor = [UIColor redColor];
        [_button addTarget:self action:@selector(clickTheBtn) forControlEvents:UIControlEventTouchUpInside];
    }
    return _button;
}

- (void) clickTheBtn {
    NSInteger i = random() % 5 + 1;
    _label1.text = [NSString stringWithFormat:@"%ld",i];
}

//这里为了防止内存泄漏要移除观察者label1
- (void)dealloc {
    [_label1
     removeObserver:self forKeyPath:@"text"];
}

//实现监听方法。监听方法在Value(属性的值)发生变化的时候自动调用。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    //其中,object指被监听的对象。change里存储了一些变化的数据,比如变化前的数据,变化后的数据。
    if ([object isEqual:self.label1] && [keyPath isEqualToString:@"text"]) {
        _label2.text = _label1.text;
    }
}

效果如下:

三、通知

通知:是一种广播机制,在实践发生的时候,通过通知中心对象,一个对象能够为所有关心这个时间发生的对象发送消息,两者都是观察者模式,不同于在KVO是被观察者直接发送消息给观察者,是对象间的直接交互,通知则是两者都和通知中心对象交互,对象之间不知道彼此。

这里仍然以上述两个label为例,通过改变label1的text,使label2的text和label1的text一样

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic,strong) UILabel * label1;
@property (nonatomic,strong) UIButton * button;
@property (nonatomic,strong) UILabel * label2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createUI];
    //    Observer:监听者
    //    selector:监听者接到通知后要执行的方法
    //    name:通知的名称
    //    object:通知的发布者
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doSomeSthing) name:@"lableText" object:_label1];
}

- (void) createUI {
    [self.view addSubview:self.label1];
    [self.view addSubview:self.label2];
    [self.view addSubview:self.button];
}

- (UILabel *)label1 {
    if (!_label1) {
        _label1 = [[UILabel alloc] initWithFrame:CGRectMake(50, 30, 200, 30)];
        _label1.text = @"我是测试label";
        _label1.backgroundColor = [UIColor cyanColor];
    }
    return _label1;
}

- (UILabel *)label2 {
    if (!_label2) {
        _label2 = [[UILabel alloc] initWithFrame:CGRectMake(50, 80, 200, 30)];
        _label2.text = @"我是测试label";
        _label2.backgroundColor = [UIColor cyanColor];
    }
    return _label2;
}

- (UIButton *)button {
    if (!_button) {
        _button = [[UIButton alloc] initWithFrame:CGRectMake(100, 120, 60, 30)];
        _button.backgroundColor = [UIColor redColor];
        [_button addTarget:self action:@selector(clickTheBtn) forControlEvents:UIControlEventTouchUpInside];
    }
    return _button;
}

- (void) clickTheBtn {
    NSInteger i = random() % 5 + 1;
    _label1.text = [NSString stringWithFormat:@"%ld",i];
    //    notificationWithName:通知名称
    //    object:发布者
    //    userInfo:发布的信息
    [[NSNotificationCenter defaultCenter] postNotificationName:@"lableText" object:_label1 userInfo:nil];
}

- (void) doSomeSthing {
    _label2.text = _label1.text;
}
//移除通知的观察者
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

效果如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值