OC底层原理4:KVO(本质、动态生成子类、如何手动触发)

一、KVO的本质

1. 创新一个新类

利用runtime的API动态生成一个子类,并且让实例对象的isa指针指向这个全新的子类。

swift 复制代码
self.person1 = [[GSPerson alloc] init];
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person.age changed"];
NSLog(@"类对象 -> %@", object_getClass(self.person1));

输出:类对象 -> NSKVONotifying_GSPerson

系统创建了一个以NSKVONotifying_开头的子类,实例对象的isa指针指向这个子类。

2. 实现逻辑

当修改实例对象的属性(调用set方法),会调用Foundation的_NSSetxxxValueAndNotify函数,函数内部会调用:

  • willChangeValueForKey:
  • super setXXX:

  • didChangeValueForKey:(方法内部会触发监听器的监听方法observeValueForKeyPath:ofObject:change:context:)
less 复制代码
NSLog(@"添加observer之前---setAge:方法的调用位置:%p", [self.person1 methodForSelector:@selector(setAge:)]);
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person.age changed"];
NSLog(@"添加observer之后---setAge:方法的调用位置:%p", [self.person1 methodForSelector:@selector(setAge:)]);

输出:
添加observer之前---setAge:方法的调用位置:0x1022317e8
添加observer之后---setAge:方法的调用位置:0x1078e7f48

当person1添加了KVO之后,setAge:方法的调用位置发生变化,后面的调用位置是Foundation类的_NSSetIntValueAndNotify方法。

疑问:_NSSetIntValueAndNotify方法里面做了什么?

验证方法:增加监听方法和触发KVO的点击事件

objectivec 复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"%@.%@ is changed, info: %@, value:\n%@", object, keyPath, context, change);
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person1.age = 20;
}

当触发了KVO,输出内容如下:

ini 复制代码
-[GSPerson willChangeValueForKey:]
-[GSPerson setAge:]
didChangeValueForKey: -- begin
<GSPerson: 0x600003bc9690>.age is changed, info: Person.age changed, value:
{
kind = 1;
new = 20;
old = 0;
}
didChangeValueForKey: -- end

发现触发了3个方法:willChangeValueForKey:、setAge、didChangeValueForKey:,说明_NSSetIntValueAndNotify函数里面调用了这几个方法,并且,在didChangeValueForKey:方法里面调用了监听方法observeValueForKeyPath:ofObject:change:context:

二、手动触发KVO

  1. 只有调用了setAge:才会触发KVO,如何在不调用setAge:方法的前提下触发KVO呢?

答:调用willChangeValueForKey: 和 didChangeValueForKey:方法,传入对应的key即可。

csharp 复制代码
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
  1. 直接修改成员变量(_age)会触发KVO吗?

不会,直接修改成员变量并没有调用setAge:方法。

三、附加知识点

  1. 获取类对象的所有方法

遍历类对象的方法列表

ini 复制代码
#import <objc/runtime.h>

- (void)printMethodNamesOfClass:(Class)cls {
    NSMutableString* methodNames = [NSMutableString string];
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    for (NSUInteger i = 0; i < count; i ++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        [methodNames appendFormat:@"%@, ",methodName];
    }
    NSLog(@"%@的所有方法:%@", cls, methodNames);
}

[self printMethodNamesOfClass:object_getClass(self.person1)];

输出:NSKVONotifying_GSPerson的所有方法:setAge:, class, dealloc, _isKVOA
  1. 为什么NSKVONotifying_GSPerson类要重写class方法?

直接返回GSPerson类,以屏蔽KVO的内部实现,隐藏NSKVONotifying_GSPerson类。

正因为这样,当self.person1添加了KVO,如果用[self.person1 class]方法获取类对象,发现输出的是GSPerson类对象,只有使用运行时方法object_getClass(self.person1)才能拿到正确的类NSKVONotifying_GSPerson。

相关推荐
钟智强3 小时前
Flutter 前端开发中的常见问题全面解析
android·前端·flutter·ios·前端框架·dart
YungFan7 小时前
iOS26适配指南之UITabBarController
ios·swift
iReaShare7 小时前
如何在 iPad 上批量删除应用:5 种简单方法
ios
Digitally10 小时前
如何将 iPhone 备份到云端:完整指南
ios·iphone
2501_9159184110 小时前
iOS App 安全加固全流程:静态 + 动态混淆对抗逆向攻击实录
android·ios·小程序·https·uni-app·iphone·webview
瓜子三百克11 小时前
RxSwift的介绍与使用
ios·swift·rxswift
2501_9159090613 小时前
iOS如何查看电池容量?理解系统限制与开发者级能耗调试方法
android·ios·小程序·https·uni-app·iphone·webview
Magnetic_h1 天前
【iOS】方法与消息底层分析
笔记·学习·macos·ios·objective-c·cocoa
MaoJiu1 天前
Flutter与原生端的通信
flutter·ios