当前位置: 首页 > news >正文

iOS Runtime之 KVO

KVO(Key-Value Observing,键值观察)是 iOS 中一种对象间属性变化通知机制,允许一个对象(观察者)监听另一个对象(被观察者)的特定属性,当属性值发生变化时,观察者会收到回调通知。它是 Objective-C 动态特性的典型应用,广泛用于数据驱动 UI(如 MVVM 模式中 ViewModel 属性变化驱动 View 更新)、状态同步等场景。

一、KVO是啥

  • 定义:KVO 是一种基于 “键路径(Key Path)” 的观察者模式实现,通过监听对象的属性(或嵌套属性,如person.address.street)变化,实现对象间的解耦通信。
    优点:无需修改被观察者的代码,即可实现对其属性的监听(“无侵入式” 观察),避免了硬编码的回调依赖。
  • 与 Notification 的区别:
    KVO 是一对一的精准监听(观察者直接关联被观察者的属性);
    Notification 是一对多的广播机制(通过通知中心转发,不直接关联发送者)。
  • KVO 主要监听通过@property声明的属性,需满足:
    • 属性有对应的setter方法(KVO 通过拦截setter触发通知,直接修改实例变量不会触发 KVO);
    • 支持基本数据类型(int、float等,会被自动装箱为NSNumber)、对象类型(NSString、NSArray等);
    • 支持集合类型(NSMutableArray等),但需通过KVC的集合方法(如mutableArrayValueForKey:)修改,才能触发 KVO(直接调用addObject:不会触发)。

二、KVO 的实现原理

KVO 的底层依赖 Objective-C 的runtime 动态特性,核心是 “isa 指针交换(isa-swizzling)”,步骤如下:

  • 动态生成子类:当对象 A(被观察者)的某个属性首次被注册为 KVO 观察目标时,系统会在运行时动态生成一个 A 的子类(命名格式为NSKVONotifying_A),该子类继承自 A 的原类。
  • 重写 setter 方法:动态子类会重写被观察属性的setter方法,重写后的setter做三件事:
    调用原类的setter方法(确保属性值正常更新);
    触发willChangeValueForKey:(通知即将变化);
    触发didChangeValueForKey:(通知已变化,内部会调用观察者的observeValueForKeyPath:…)。
  • 修改 isa 指针:被观察者 A 的isa指针(指向对象的类)会被修改为指向动态生成的子类(NSKVONotifying_A),使得 A 的方法调用优先走动态子类的实现(从而触发setter中的通知逻辑)。
  • 隐藏动态子类:动态子类会重写class方法,返回原类(A.class),因此通过[A class]无法察觉子类的存在,保证对外透明。

三、KVO 的使用步骤

1. 定义被观察者类

// RPPerson.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name; // 被观察的属性
@property (nonatomic, assign) NSInteger age;
@end// RPPerson.m
@implementation RPPerson
@end

2. 注册观察者(观察者类中)

// ObserverViewController.h
#import "RPPerson.h"@interface ObserverViewController ()
@property (nonatomic, strong) RPPerson *person;
@end@implementation ObserverViewController- (void)viewDidLoad {[super viewDidLoad];self.person = [[RPPerson alloc] init];// 注册KVO:观察者为self,观察person的name属性[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];// options参数:指定是否需要新值(New)、旧值(Old)
}

3. 实现观察回调方法

// 当被观察属性变化时,此方法被调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {if ([keyPath isEqualToString:@"name"]) {// 从change字典中获取新旧值(根据options设置)NSString *oldName = change[NSKeyValueChangeOldKey];NSString *newName = change[NSKeyValueChangeNewKey];NSLog(@"name变化:%@ -> %@", oldName, newName);// 此处可更新UI或处理业务逻辑(如刷新界面)self.nameLabel.text = newName;} else {// 若观察多个属性,需区分处理;未处理的属性需调用父类方法[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];}
}

4. 移除观察者(关键!)

- (void)dealloc {// 必须在观察者销毁前移除,否则被观察者会继续发送通知,导致崩溃[self.person removeObserver:self forKeyPath:@"name"];
}

四、开发过程中需要注意的点

1. 注册与移除必须配对,避免崩溃

  • 若观察者已被释放(如dealloc),但未移除 KVO 注册,被观察者属性变化时会向已释放的观察者发送消息,导致野指针崩溃(EXC_BAD_ACCESS)。
  • 解决:在观察者的dealloc中必须调用removeObserver:forKeyPath:,且确保注册和移除的keyPath、context完全一致。

2. 避免重复注册 / 重复移除

  • 重复注册:同一观察者对同一属性多次注册,会导致回调方法被多次触发(一次变化,多次回调)。
    注册前检查是否已注册(可通过自定义标志位记录);
  • 重复移除:对未注册的观察者调用removeObserver,会直接崩溃(报错Cannot remove an observer … for the key path … because it is not registered as an observer.)。
    移除时确保只调用一次(如在dealloc中移除,避免在其他生命周期方法中重复调用)。

3. 观察的属性必须是 KVO 兼容的

  • 不触发 KVO 的情况:
    直接修改实例变量(如_name = @“new”,未调用setter);
    属性未生成setter(如@property (nonatomic) NSString *name但未实现setName:,或用@dynamic延迟实现但未实现)。
    解决:
    始终通过setter或 KVC 方法(setValue:forKey:)修改属性;
  • 若需观察实例变量,可手动触发 KVO;
// 手动触发name的KVO通知
[self.person willChangeValueForKey:@"name"];
_name = @"手动修改的名字"; // 直接修改实例变量
[self.person didChangeValueForKey:@"name"]; // 触发观察者回调

4. 正确处理context参数

context是一个void *类型的指针,用于区分不同的 KVO 注册(尤其在父类也使用 KVO 时),避免回调冲突。
用静态变量作为context,确保唯一性:

// 定义静态context
static void *RPPersonNameContext = &RPPersonNameContext;// 注册时传入
[self.person addObserver:self forKeyPath:@"name" options:0 context:RPPersonNameContext];// 回调中判断
- (void)observeValueForKeyPath:... context:(void *)context {if (context == RPPersonNameContext) {// 处理name变化} else {[super observeValueForKeyPath:...]; // 交给父类处理}
}

5. 线程安全问题

KVO 的回调方法(observeValueForKeyPath:…)默认在被观察者属性变化的线程中执行,若该线程是子线程,直接在回调中更新 UI 会导致崩溃。

- (void)observeValueForKeyPath:... {dispatch_async(dispatch_get_main_queue(), ^{self.nameLabel.text = newName; // UI操作必须在主线程});
}

五、关于KVO的一些其他补充

1. Swift 中 KVO 的限制与差异

限制:Swift 的class需继承自NSObject,且被观察的属性需用@objc dynamic修饰(确保属性具有动态性,允许 runtime 拦截setter):

class Person: NSObject {@objc dynamic var name: String = "" // 必须加@objc dynamic才能被KVO观察
}

替代方案:Swift 更推荐使用Property Observer(willSet/didSet),但它是类内部的回调,无法实现跨对象观察(与 KVO 的 “跨对象” 特性不同)。

2. KVO 的性能开销

KVO 的动态子类生成和setter拦截会带来一定性能开销,频繁变化的属性(如每秒几十次)使用 KVO 可能导致性能问题。
对高频变化的属性,可通过 “批量通知”(合并多次变化为一次通知)或改用其他轻量机制(如 Block 回调)。

3. KVO 与响应式编程

KVO 是 iOS 中 “响应式” 思想的早期实现,现代响应式框架(如 RxSwift、ReactiveCocoa)在此基础上扩展,支持更复杂的事件流组合(如过滤、映射、合并),但底层仍可能依赖 KVO 的机制。

4. KVO的三方库(实际开发项目中建议使用三方库)

  • KVOController(Facebook)
    Facebook出品质量放心,开源,轻量级封装,核心解决原生 KVO 的 “生命周期管理” 和 “回调分散” 问题。适用于需简化 KVO 使用流程的 OC/Swift 项目;对轻量、无依赖库有需求的项目。
    特点:

    • 自动管理观察者生命周期:无需手动调用removeObserver,内部会在观察者销毁时自动移除,避免崩溃;
    • 支持 Block 回调:用 Block 替代原生的observeValueForKeyPath:,回调逻辑与注册代码放在一起,代码更易读;
    • 支持context区分:通过context区分不同的观察,避免父类 / 子类 KVO 冲突;
    • 轻量无依赖:仅依赖系统框架,体积小,集成成本低。
  • ReactiveCocoa(RAC)
    老牌响应式编程框架,KVO 是其核心功能之一,通过 “信号(Signal)” 封装属性变化,支持复杂的事件流处理(过滤、映射、合并等)。适用于采用响应式编程思想的项目;需要处理复杂事件流(如多属性联动、防抖、节流)的场景。
    特点:

    • 响应式编程模型:将属性变化转化为 “信号”,可通过链式调用对信号进行处理(如filter、map、throttle);
    • 自动管理生命周期:信号会与观察者(如self)绑定,观察者销毁时自动取消订阅,避免内存问题;
    • 支持多属性组合观察:可同时观察多个属性,合并成一个信号处理(如 “当 A 和 B 都变化时触发”);
    • 跨平台支持:同时支持 OC 和 Swift(Swift 版本为 ReactiveSwift)。
  • RxSwift(Swift)
    Swift 中最流行的响应式框架,通过rx.observe扩展支持 KVO,结合 Swift 的类型安全特性,避免原生 KVO 的字符串keyPath风险。
    特点:

    • 类型安全:观察属性时通过KeyPath(而非字符串)指定,编译期检查keyPath正确性,避免拼写错误;
    • 响应式操作链:支持丰富的操作符(如map、filter、debounce),可对属性变化进行复杂处理;
    • 自动内存管理:通过Disposable管理订阅生命周期,配合DisposeBag可在对象销毁时自动取消订阅;
    • Swift 原生支持:完全基于 Swift 语法设计,使用体验更符合 Swift 习惯。
  • YYKVO(YYKit)
    YYKit 中的 KVO 组件,轻量级封装,支持 Block 和 Selector 回调,适合不想引入大型框架的项目。
    特点:

    • 双回调方式:同时支持 Block(简洁)和 Selector(兼容传统回调);
    • 自动移除观察:内部通过弱引用管理观察者,观察者销毁时自动移除,避免崩溃;
    • 轻量高效:代码量少,性能接近原生 KVO;
    • 依赖 YYKit:需集成 YYKit 框架(若项目已用 YYKit,可直接使用)。
http://www.dtcms.com/a/498985.html

相关文章:

  • ZigBee中的many-to-one和link status(1)
  • 【WRF-CMAQ第二期】WRF-CMAQ 测试案例安装与运行
  • 汕头seo网站排名免费制作网页的软件有哪些
  • 韩国设计网站推荐yandex搜索引擎入口
  • 2025年机器视觉软件平台哪个好?场景适配视角下的优质实例解析
  • 【C++/Lua联合开发】 (一) Lua基础知识
  • 从前序与中序遍历序列构造二叉树
  • 学习go语言
  • Linux中工作队列使用
  • 金融工程(一)
  • LeetCode 每日一题 2025/10/13-2025/10/19
  • C++ 面试基础考点 模拟题 力扣 38. 外观数列 题解 每日一题
  • 辽阳企业网站建设费用企业推广软文
  • 天津实体店网站建设深圳宝安区住建局官网
  • shell编程语言---sed
  • iframe实战:跨域通信与安全隔离
  • 购物网站的建设意义html可视化编辑软件
  • Bootstrap 字体图标
  • PVE 9.0 定制 Debian 13 镜像 支持 Cloud-Init 快速部署虚拟机【模板篇】
  • 长春建站模板搭建高端品牌包包都有哪些
  • ai周公解梦抖音快手微信小程序看广告流量主开源
  • 【无标题】大模型-高效优化技术全景解析:微调 量化 剪枝 梯度裁剪与蒸馏 下
  • 自动化信息交付:深度解析AI驱动的每日简报系统架构与实现
  • 做微信公众号第三网站男女做视频观看网站
  • 定时任务Quartz原理详解
  • Rethinking SSIM-Based Optimization in Neural Field Training
  • rocketmq和kafka的区别之顺序消费
  • 套路有*道龙激光-乐多刀销*游戏程序系统方案
  • Angular 2 数据显示
  • 如何快速做单页面网站怎么查网站建设是哪家公司