首页 > 其他分享 >iOS开发基础127-深入探讨KVO

iOS开发基础127-深入探讨KVO

时间:2024-07-17 19:30:22浏览次数:9  
标签:name KVO void iOS context 127 self change

一、基础

KVO(Key-Value Observing,键值观察)是 Cocoa 提供的一种机制,它允许我们观察属性的变化并做出响应。这种机制非常强大,广泛应用于各种编程场景,如数据绑定、状态变化监控等。在深入了解 KVO 之前,我们先从 KVO 的基本概念开始,然后逐步探讨其深层次应用和一些使用实践的注意事项。

一、KVO 基本概念

1. 定义和原理

KVO 是一种基于观察者设计模式的实现,它允许一个对象观察另一个对象的属性变化。当被观察的属性发生变化时,观察者会从“被通知”状态转变到“更新”状态。例如,当一个 model 的属性发生变化时,ViewModel 可以观察到变化并相应地更新视图。

  • 被观察者:拥有要被观察的属性,一旦属性发生变化,它会通知所有的观察者。被观察者通常是一个 NSObject 的子类。
  • 观察者:注册监听被观察者的属性变化,并在变化时获取通知,通过重写 observeValueForKeyPath:ofObject:change:context: 方法做出相应处理。

2. 基本用法

首先,我们定义一个模型对象 User,并添加对其 name 属性的观察。

Model:User

@interface User : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation User
@end

观察变化

@interface MyObserver : NSObject
@end

@implementation MyObserver

- (void)startObservingUser:(User *)user {
    [user addObserver:self
           forKeyPath:@"name"
              options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
              context:NULL];
}

- (void)stopObservingUser:(User *)user {
    [user removeObserver:self forKeyPath:@"name"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Name changed from %@ to %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
    }
}

@end

使用示例

User *user = [[User alloc] init];
MyObserver *observer = [[MyObserver alloc] init];
[observer startObservingUser:user];

user.name = @"NewName"; // This will trigger the KVO notification

二、KVO 的深入特性

1. 自动和手动 KVO

自动 KVO:系统自动支持的大多数属性,遵循简单的属性管理(即使用 @property 定义的属性),不需要手动实现属性的观察。

手动 KVO:需要在属性更新时手动触发 KVO 通知,通常用于更复杂的数据结构或自定义的观察逻辑。

手动实现 KVO

@interface ManualKVOUser : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation ManualKVOUser

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

@end

2. KVO 的依赖键路径

在某些情况下,一个属性的值依赖于其他属性的值。可以重载 keyPathsForValuesAffectingValueForKey: 来声明这种关系。

示例

假设我们有一个类 Rectangle,它有宽度和高度属性。我们希望当宽度或高度发生变化时,面积属性也会发生变化。

@interface Rectangle : NSObject
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, readonly) CGFloat area;
@end

@implementation Rectangle

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    if ([key isEqualToString:@"area"]) {
        return [NSSet setWithObjects:@"width", @"height", nil];
    }
    return [super keyPathsForValuesAffectingValueForKey:key];
}

- (CGFloat)area {
    return self.width * self.height;
}

@end

3. KVO 和集合对象

KVO 也支持集合对象的观察,包括 NSArrayNSSetNSDictionary。可以观察集合对象的内容变化,而不仅仅是对集合对象的指针变化。

示例

观察 NSMutableArray 的内容变化:

@interface ListWatcher : NSObject
@end

@implementation ListWatcher

- (void)observeList:(NSMutableArray *)list {
    [list addObserver:self
           toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:0]
                   forKeyPath:@"@count"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:NULL];
}

- (void)dealloc {
    [list removeObserver:self forKeyPath:@"@count"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"@count"]) {
        NSLog(@"Array count changed: %@", change);
    }
}

@end

三、KVO 的注意事项

  1. 移除观察者:不要忘记在适当的时候移除观察者,否则会导致崩溃。通常在 dealloc 方法中移除。
  2. ** KVO Context**:在复杂项目中,应为每个 KVO 观察提供唯一的上下文,以避免因不同模块监听同一键路径而产生的冲突。
  3. 线程安全:确保在合适的线程上下文中添加和移除观察者,以避免数据竞争和崩溃。
  4. KVO 错误处理:在 observeValueForKeyPath:ofObject:change:context: 方法中处理未知的 keyPath,避免意外的行为:
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"expectedKeyPath"]) {
        // 处理特殊观察逻辑
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

四、实践中的 KVO 和 RAC 的结合

RAC 可以大大简化和增强 KVO 的使用,使代码更具表达力和可维护性。

结合 RAC 的 KVO

User *user = [[User alloc] init];
RACSignal *nameSignal = [user rac_valuesForKeyPath:@"name" observer:self];

[nameSignal subscribeNext:^(id  _Nullable newName) {
    NSLog(@"Name changed to %@", newName);
}];

二、底层

KVO(Key-Value Observing,键值观察)的底层实现机制在 Objective-C 中相当复杂,但理解其原理能够帮助我们更好地使用和调试 KVO。KVO 的实现主要依赖于 Objective-C 的运行时特性和动态方法解析机制。

1、KVO 背后的原理

当你对一个对象的某个属性添加观察者后,实际上发生了以下几步:

  1. 动态创建子类:运行时系统会为被观察的对象动态创建一个新的子类,并将该对象的 isa 指针指向这个子类。
  2. 重写属性的 setter 方法:在这个新创建的子类中,重写被观察属性的 setter 方法,以便在属性发生变化时,通知所有的观察者。
  3. 维护观察者列表:这个子类还会维护一个观察者列表,记录所有对该属性添加了观察的观察者。
  4. 触发通知:当属性的 setter 方法被调用时,会触发 KVO 通知,通知所有的观察者,调用 observeValueForKeyPath:ofObject:change:context: 方法。

二、KVO 的具体实现步骤

1. 动态创建子类

当你调用 addObserver:forKeyPath:options:context:,运行时系统会动态创建一个子类,该子类的类名并不是静态的,而是通过拼接生成的。例如,如果你对一个 Person 对象添加观察,系统可能会生成一个名为 NSKVONotifying_Person 的子类。

2. 修改 isa 指针

被观察的对象的 isa 指针会被修改为这个新生成的子类。isa 指针用于指向对象所属的类,通过动态改变 isa 指针,可以改变对象的方法实现。

3. 重写属性的 setter 方法

新的子类会重写被观察属性的 setter 方法,以便在属性值被修改后进行通知。该 setter 方法大概如下:

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

willChangeValueForKey:didChangeValueForKey: 方法用于通知系统即将发生一个变化和变化已经发生,系统会在这两个方法之间发送通知给所有的观察者。

三、KVO 的底层实现详解

1. 动态创建子类

addObserver:forKeyPath:options:context: 方法被调用时,系统首先检查当前类是否已经有一个对应的 KVO 子类。如果没有,系统会动态创建一个子类并重写相关方法。

// 示例代码,展示动态创建 KVO 子类的步骤
Class originalClass = object_getClass(myObject);
NSString *kvoClassName = [NSString stringWithFormat:@"NSKVONotifying_%@", NSStringFromClass(originalClass)];

// 检查是否已经有这个类
Class kvoClass = NSClassFromString(kvoClassName);
if (!kvoClass) {
    // 创建子类
    kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);

    // 重写 setter 方法
    SEL setterSelector = @selector(setName:);
    Method setterMethod = class_getInstanceMethod(originalClass, setterSelector);
    const char *types = method_getTypeEncoding(setterMethod);
    class_addMethod(kvoClass, setterSelector, (IMP)kvo_setter, types);

    // 注册新类
    objc_registerClassPair(kvoClass);
}

// 修改 isa 指针
object_setClass(myObject, kvoClass);

2. 重写 setter 方法

重写的 setter 方法不仅会更改属性值,还会确保在属性变化前后发送通知,触发观察者的回调:

void kvo_setter(id self, SEL _cmd, NSString *newName) {
    // 获取旧值
    NSString *oldName = [self name];

    // 通知即将更改
    [self willChangeValueForKey:@"name"];
    
    // 执行原始的 setter 方法
    void (*originalSetter)(id, SEL, NSString *) = (void (*)(id, SEL, NSString *))class_getMethodImplementation(object_getClass(self), _cmd);
    originalSetter(self, _cmd, newName);
    
    // 通知已经更改
    [self didChangeValueForKey:@"name"];
}

注意,这里有一个重要的调用 class_getMethodImplementation,它获取了原始类的 setter 方法实现并调用它以确保属性值的实际改变。

3. 两步通知

willChangeValueForKey:didChangeValueForKey: 方法会触发 KVO 通知。系统会在这两个方法调用之间发送通知给所有注册的观察者。核心实现如下:

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"]; // 通知即将更改值
    [super setName:name]; // 调用原始的 setter 方法执行实际更改
    [self didChangeValueForKey:@"name"]; // 通知值已经更改
}

四、KVO 的使用细节

1. 自动 KVO 与手动 KVO

如前所述,大多数情况下,KVO 是自动的。但在一些特殊情况或自定义对象中,你可能希望手动管理 KVO 通知。可以通过调用 willChangeValueForKey:didChangeValueForKey: 手动触发 KVO 通知。

- (void)setCustomName:(NSString *)name {
    [self willChangeValueForKey:@"customName"];
    _customName = name;
    [self didChangeValueForKey:@"customName"];
}

2. Dictionary Observing

在 KVO 中,用字典可以包含多个信息。change 字典包含了变化的信息,如新旧值:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Old value: %@", change[NSKeyValueChangeOldKey]);
        NSLog(@"New value: %@", change[NSKeyValueChangeNewKey]);
    }
}

五、注意事项

1. 内存管理

释放观察者:在对象销毁前,务必移除所有的观察者,否则可能会引发崩溃。

- (void)dealloc {
    [observedObject removeObserver:self forKeyPath:@"name"];
}

可以利用 dealloc 方法中执行清理操作,确保对象在销毁时妥善移除所有观察者。但要特别注意现代 Objective-C 允许在 dealloc 方法中直接调用 release,因此需要小心处理。

2. 线程安全

在多线程环境中,添加和移除观察者时需要确保线程安全性。可以考虑使用同步机制来保护这些操作。

3. KVO Context

在实践中,通常会为不同的 KVO 监听提供唯一的上下文指针,以避免由于监听同一键路径引起的冲突。上下文指针通常使用静态指针变量。

static void *NameChangeContext = &NameChangeContext;

[observedObject addObserver:self
                 forKeyPath:@"name"
                    options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                    context:NameChangeContext];

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if (context == NameChangeContext) {
        // handle change
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

理解 KVO 的底层实现原理,可以帮助我们更有效地利用这一强大的机制。KVO 通过动态创建子类、重写 setter 方法和维护观察者列表,为我们提供了一种简洁而高效的观察属性变化的方式。安全的使用 KVO,包括管理内存、调试和避免线程问题,对于构建稳定和高效的应用至关重要。

标签:name,KVO,void,iOS,context,127,self,change
From: https://www.cnblogs.com/chglog/p/18308139

相关文章

  • 在 PowerShell 中Get-WmiObject Win32_PhysicalMemory,SMBIOSMemoryType 是一种用于描
    在PowerShell中Get-WmiObjectWin32_PhysicalMemory,SMBIOSMemoryType是一种用于描述系统中物理内存类型的属性。数字26表示特定的内存类型,具体为DDR4内存。每种内存类型在SMBIOS(SystemManagementBIOS)规范中都有一个对应的数字码,用来标识不同类型的内存。以下是一些常见......
  • iOS开发基础125-深入探索SDWebImage
    SDWebImage是一个流行的用于处理图像下载和缓存的库,广泛用于iOS开发中,提供了一系列方便的API来下载和缓存图像,以提高应用的性能和用户体验。以下是对其进行详细介绍和分析,包括其原理和底层实现。一、SDWebImage的主要功能图像下载和缓存:图像下载:使用异步方式从网络上下......
  • iOS开发基础124-RunLoop实现卡顿检测
    利用RunLoop实现卡顿检测的基本思路是通过监听RunLoop的状态变化来判断主线程的执行时长。如果RunLoop在某个状态停留的时间超过了预设的时间阈值,则认为发生了卡顿。在具体实现中,可以利用CFRunLoopObserver来监听RunLoop的状态变化,并记录时间差。一、卡顿检测的基本原......
  • iOS开发基础122-RunLoop
    深入探讨RunLoop的底层实现需要了解CoreFoundation框架中的CFRunLoop以及与RunLoop工作机制紧密相关的操作系统底层API。这些底层实现主要涉及到事件源、定时器和线程的调度机制。本文将深入剖析RunLoop的底层结构及其运行流程。一、RunLoop底层数据结构涉及RunLo......
  • iOS开发基础123-自动释放池
    自动释放池(AutoreleasePool)是Objective-C中用于管理内存的一个重要机制,它帮助开发者简化内存管理的工作。自动释放池的核心概念是将对象放入池中,在某个时刻由系统统一释放这些对象。这种机制在iOS和macOS的应用开发中广泛使用,尤其是在事件循环和线程运行时。为了深入理解其底层......
  • 基于java+springboot+vue实现的实验室管理系统(文末源码+Lw)127
    基于SpringBoot+Vue的实现的实验室管理系统(源码+数据库+万字Lun文+流程图+ER图+结构图+演示视频+软件包)系统功能:实验室管理系统管理员功能有个人中心,学生管理,教师管理,公告信息管理,知识库管理,实验课程管理,实验室信息管理,实验室预约管理,实验设备管理,采购记录管理,维修记录管理......
  • 基于java+springboot+vue实现的实验室管理系统(文末源码+Lw)127
     基于SpringBoot+Vue的实现的实验室管理系统(源码+数据库+万字Lun文+流程图+ER图+结构图+演示视频+软件包)系统功能:实验室管理系统管理员功能有个人中心,学生管理,教师管理,公告信息管理,知识库管理,实验课程管理,实验室信息管理,实验室预约管理,实验设备管理,采购记录管理,维修记录......
  • vue请求接口常用写法(axios)
    1.项目根目录下新建一个utils文件夹,并新建一个request.js文件(注意:是以axios方法请求的,所以需要先安装axios或cdn引入)安装:npmnpminstallaxios-Syarnyarnaddaxios-Scdn<scriptsrc="https://unpkg.com/axios/dist/axios.min.js"></script>&&配置代码imp......
  • iOS开发基础119-组件化
    一、引言组件化是将应用程序分解成多个独立模块的设计方法,这些模块可以单独开发、测试和维护。对于大型iOS项目,组件化能够提高开发效率、降低耦合、增加代码复用性,并且使项目更易维护。本文将详细介绍如何在iOS项目中实现组件化,包括本地组件管理和远程组件管理。二、为什么......
  • iOS开发基础120-通知与线程
    NSNotificationCenter是iOS和macOS开发中用于消息传递的机制,可以在多个对象之间实现解耦的事件通知。理解NSNotificationCenter的线程模型对正确使用这一工具至关重要。NSNotificationCenter的线程模型1.消息发送线程当你通过NSNotificationCenter发送消息时,消息会......