iOS的KVO底层实现

原创文章
声明:作者声明此文章为原创,未经作者同意,请勿转载,若转载,务必注明本站出处,本平台保留追究侵权法律责任的权利。
全栈老韩
全栈工程师,擅长iOS App开发、前端(vue、react、nuxt、小程序&Taro)开发、Flutter、React Native、后端(midwayjs、golang、express、koa)开发、docker容器、seo优化等。

概述:

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++]; 
}
  1. 添加观察
复制代码
/*
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方法来手动willChangedidChange

5.3. 监听NSMutableArraycount

  • 通过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,即_pisa指针指向了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"
)

暂无评论,快来发表第一条评论吧