iOS的KVO底层实现
全栈老韩
全栈工程师,擅长iOS App开发、前端(vue、react、nuxt、小程序&Taro)开发、Flutter、React Native、后端(midwayjs、golang、express、koa)开发、docker容器、seo优化等。
概述:
KVO属于观察者模式,在很多地方都会用到,比如响应式编程。
- KVO只是观察属性的set方法,属性是对成员变量及其set、get方法的封装。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 0;
// _p.dog.age = a++;
// _p.dog.level = a++;
// 成员变量这样子不能被监听到
_p->_name = [NSString stringWithFormat:@"%d", a++];
}
- 添加观察
/*
NSKeyValueObservingOptionNew 返回新值
NSKeyValueObservingOptionOld 返回旧值
NSKeyValueObservingOptionInitial 注册的时候就会发一次通知,改变的时候也会发通知
NSKeyValueObservingOptionPrior 改变之前发一次,改变之后发一次
*/
- (void)viewDidLoad {
[super viewDidLoad];
_p = [[Person alloc] init];
// 添加观察者
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
观察者方法中有一个change,这个里面有一个kind的key,有以下几种:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval -= 3,
NSKeyValueChangeReplacement = 4,
};
概述:
KVO属于观察者模式,在很多地方都会用到,比如响应式编程。
1. KVO只是观察属性的set方法,属性是对成员变量及其set、get方法的封装。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 0;
// _p.dog.age = a++;
// _p.dog.level = a++;
// 成员变量这样子不能被监听到
_p->_name = [NSString stringWithFormat:@"%d", a++];
}
2. 添加观察者:
/*
NSKeyValueObservingOptionNew 返回新值
NSKeyValueObservingOptionOld 返回旧值
NSKeyValueObservingOptionInitial 注册的时候就会发一次通知,改变的时候也会发通知
NSKeyValueObservingOptionPrior 改变之前发一次,改变之后发一次
*/
- (void)viewDidLoad {
[super viewDidLoad];
_p = [[Person alloc] init];
// 添加观察者
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
观察者方法中有一个change,这个里面有一个kind的key,有以下几种:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval -= 3,
NSKeyValueChangeReplacement = 4,
};
3. KVO有两种方式触发
- 自动
通过@interface NSObject(NSKeyValueObservingCustomization)中的
/* Return YES if the key-value observing machinery should automatically invoke -willChangeValueForKey:/-didChangeValueForKey:, -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:, or -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: whenever instances of the class receive key-value coding messages for the key, or mutating key-value coding-compliant methods for the key are invoked. Return NO otherwise. Starting in Mac OS 10.5, the default implementation of this method searches the receiving class for a method whose name matches the pattern +automaticallyNotifiesObserversOf<Key>, and returns the result of invoking that method if it is found. So, any such method must return BOOL too. If no such method is found YES is returned.
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
默认YES来触发。
- 手动
即通过如下设置,手动触发
// KVO两种模式:1. 自动, 2. 手动
// 默认是自动模式
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 0;
// 手动触发KVO
[_p willChangeValueForKey:@"name"];
_p.name = [NSString stringWithFormat:@"%d", a++];
[_p didChangeValueForKey:@"name"];
}
4. 属性的依赖(依然可以observe)
- (void)viewDidLoad {
[super viewDidLoad];
_p = [[Person alloc] init];
// 添加观察者
[_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 0;
_p.dog.age = a++;
}
5. KVO监听的是set方法,对于像是容器来的比如NSMutableArray,则需要结合KVC。
监听容器类属性的方法有:
5.1. 例如新建NSMutableArray,然后通过set方法赋值给属性;(low的做法)
5.2. Apple提供了一个途径:
// 通过KVO观察容器属性的变化,利用KVC方法
NSMutableArray *tempArr = [_p mutableArrayValueForKey:@"arr"];
[tempArr addObject:@"objc"];
// [_p.arr addObject:@"1"];
这样就可以通过addObserver方法观察得到了,苹果的这种做法,通过断点,可以得知,tempArr其实并不是NSMutableArray类型,而是NSKeyValueNotifyingMutableArray,是NSMutableArray的子类,通过重写addObject方法来手动willChange和didChange。
5.3. 监听NSMutableArray的count
- 通过KVO
addObservercrash, 观察"@count"也会crash - 通过KVC
id count = [arr valueForKey:@"count"]会crash - 通过集合运算符
[arr valueForKey:@"@count"]可以成功获取到count
6. 如果要观察Person类中的Dog属性的age和level属性,
// 添加观察者
// [_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];
// [_p addObserver:self forKeyPath:@"dog.level" options:NSKeyValueObservingOptionNew context:nil];
// 如果观察dog的属性的话,可以直接观察dog,然后在person类中做修改
[_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 0;
_p.dog.age = a++;
_p.dog.level = a++;
}
在Person.m文件里:
// 返回一个容器,里面放字符串类型
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
// NSLog(@"%@", key);
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
NSArray *arr = @[@"_dog.level", @"_dog.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
}
return keyPaths;
}
7. 观察的实现原理
通过
// 添加观察者
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
会创建一个继承自Person的子类NSKVONotifying_Person,记住以下:
- 方法的调用和类型有关
- 成员变量和对象有关系
_p的类型从Person变成了子类NSKVONotifying_Person,即_p的isa指针指向了NSKVONotifying_Person
8. 消息发送
消息发送时,首先找类,然后找这个类Class中的方法SEL编号,对应着方法的实现,然后就能找到代码区。如果找不到,那么就会根据isa指针,找到父类,找父类的方法。
9. 子类继承父类
一个类继承父类的情况下,子类不会有父类的方法,如果子类调用的方法在子类中没有,那么就会通过isa指针去找父类的方法。子类重写父类的方法,实质上就是添加方法。
10 使用runtime重新实现系统的KVO
10.1 新建分类:NSObject+CSKV
@interface NSObject (CSKVO)
- (void)CS_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
#import "NSObject+CSKVO.h"
#import <objc/runtime.h>
@implementation NSObject (CSKVO)
- (void)CS_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
// 1. 自定义一个NSKVONotifying_Person子类
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [@"CS_" stringByAppendingString:oldClassName];
// 创建一个类
Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
// 注册该类
objc_registerClassPair(myClass);
// 2. 动态修改
object_setClass(self, myClass);
// 3. 添加setName方法!
class_addMethod(myClass, @selector(setName:), (IMP)haha, "v@:@"); // “v@:@”:v表示返回值void, @表示第一个参数是OC类型,:表示方法编号SEL,@表示最后一个参数是oc类型
}
// OC方法的调用里面有两个隐藏参数:id self, SEL _cmd
void haha(id self, SEL _cmd, NSString *newName) { // 这个地方如果只给参数:NSString *newName, 那么newName的值就知识CS_Person对象
NSLog(@"子类setname %@", newName);
// 修改name属性
// 通知外界willChange, didChange.
}
@end
10.2 添加观察者
[_p CS_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
10.3 修改属性
// _p.name = [NSString stringWithFormat:@"%d", a++];
// 上面等价于下面的消息机制
objc_msgSend(_p, @selector(setName:), [NSString stringWithFormat:@"%d", a++]);
11 数组NSMutableArray
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:1];
- 关于容量,数组在内存中,如果内存不够用了,容量会成倍(*2)的增加!(1, 2, 4, 8)
- 使用这种方式时,一般往往是认为大多数情况下,存储的容量只有1个。
[NSMutableArray array]更节约内存capacity:数组容量 -> 内存中的大小
12 使用runtime证明KVO中生成了NSKVONotifying_Person
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Person *p = [[Person alloc] init];
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
p.name = @"Hamry";
NSLog(@"======%@", [ViewController findSubClass:[p class]]);
}
+ (NSArray *)findSubClass:(Class)defaultClass {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组,其中包含给定对象
NSMutableArray *array = [NSMutableArray arrayWithObject:defaultClass];
// 获取所有已注册的类
Class *classes = (Class *)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
// 遍历
for (int i = 0; i < count; i++) {
if (defaultClass == class_getSuperclass(classes[i])) {
[array addObject:classes[i]];
}
}
free(classes);
return array;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"change: %@", change);
}
2019-07-23 11:56:06.303501+0800 MethodForwarding[66501:2118253] change: {
kind = 1;
new = Hamry;
}
2019-07-23 11:56:06.362926+0800 MethodForwarding[66501:2118253] ======(
Person,
"NSKVONotifying_Person"
)
发布于2024-01-31 04:03:11
浏览量48·
暂无评论,快来发表第一条评论吧