使用Runtime自定义KVO,原理浅析

一、介绍

什么是KVO?全称key-value-observer,键值观察,观察者设计模式的另一种实现。其作用是通过观察者监听属性值的变化而做出函数回调。

 

二、原理

KVO基于Runtime机制实现,使用了isa的混写技术

监听者监听类A的某一个属性的变化,系统会动态为类A创建一个子类NSKVONotifying_A,并将类A的isa指针重新指向该子类

系统会重写类A的setter方法。( 赋值前后分别调用willChangeValueForKey和didChangeValueForKey跟踪新旧值 )

当类A的属性发生改变时,系统通知监听者,调用observeValueForKey:ofObject:change:context方法即可

 

三、图示

使用Runtime自定义KVO,原理浅析

 

三、基本实现

Student类

//  Created by 夏远全 on 2019/10/12.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end

ViewController类

-(void)test_objc_KVO {
    
    //创建对象
    self.stu = [[Student alloc] init];
    self.stu.name = @"张三";
    
    //注册观察者
    [self.stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    
    //修改值
    self.stu.name = @"李四";
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@\n", object, keyPath, change);
    
}

断点结果:监听前后,stu对象的类类型发生了变化,Student -> NSKVONotifying_Student

使用Runtime自定义KVO,原理浅析

打印结果

2019-10-13 11:22:01.251412+0800 运行时[18494:3545181] 被观测对象:<Student: 0x600003d9f3c0>, 被观测的属性:name, 值的改变: {
    kind = 1;
    new = "\U674e\U56db";
}

 

四、自定义

(1)思想:  

  • 添加监听方法
  • 创建一个子类
  • 改写父类isa指针
  • 关联观察者
  • 重写setter方法
  • 给父类发送setter消息
  • 给观察者发送observeValueForKeyPath:ofObject:change:context:消息

(2)实现

Student类

//  Created by 夏远全 on 2019/10/12.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;

///自定义的监听方法
-(void)xyq_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

@end

NS_ASSUME_NONNULL_END
//
//  Student.m
//  运行时
//
//  Created by 夏远全 on 2019/10/12.
//

#import "Student.h"
#import <objc/message.h>

@implementation Student

void setterMethod(id self, SEL _cmd, NSString *name) {
    
    //5、调用父类的方法
    struct objc_super superClass =  {
        self,
        class_getSuperclass([self class])
    };
    objc_msgSendSuper(&superClass, _cmd, name);
    
    
    //6、通知观察者调用observeValueForKeyPath:ofObject:change:context:
    id observer = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
    NSString *methodName = NSStringFromSelector(_cmd);
    NSString *key = getValueKey(methodName);
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:name}, nil);
    
}

/// 通过set方法截取属性名
NSString *getValueKey(NSString *setter){
    
    //去掉setter
    if ([setter hasPrefix:@"set"]) {
        setter = [setter stringByReplacingOccurrencesOfString:@"set" withString:@""];
    }
    //去掉:
    if ([setter hasSuffix:@":"]) {
        setter = [setter stringByReplacingOccurrencesOfString:@":" withString:@""];
    }
    //首字母小写
    NSString *key = [setter stringByReplacingOccurrencesOfString:[setter substringToIndex:1] withString:[[setter substringToIndex:1] lowercaseString] options:0 range:NSMakeRange(0,1)];
    return key;
}


- (void)xyq_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    
    //1、生成子类
    const char * clazz = class_getName([self class]);
    NSString *className = [NSString stringWithCString:clazz encoding:NSUTF8StringEncoding];
    NSString *subClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
    Class subClass = objc_getClass(subClassName.UTF8String);
    subClass = objc_allocateClassPair([self class], [subClassName UTF8String], 0);
    objc_registerClassPair(subClass);
    
    //2、isa指向子类
    object_setClass(self, subClass);
    
    //3、关联观察者
    objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //4、重写set方法
    NSString *setNameStr = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
    SEL setSelector = NSSelectorFromString(setNameStr);
    class_addMethod(subClass, setSelector, (IMP)setterMethod, "v@:@");
}

@end

ViewController类

-(void)test_objc_KVO {

    //创建对象
    self.stu = [[Student alloc] init];
    self.stu.name = @"张三";
    
    //注册观察者
    [self.stu xyq_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    
    //修改值
    self.stu.name = @"李四";
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"被观测对象:%@, 被观测的属性:%@, 值的改变: %@\n", object, keyPath, change);
    
}

 

断点结果:监听前后,stu对象的类类型发生了变化,Student -> NSKVONotifying_Student

使用Runtime自定义KVO,原理浅析

打印结果

2019-10-13 11:35:17.929772+0800 运行时[18720:3562649] 被观测对象:<NSKVONotifying_Student: 0x6000002e0420>, 被观测的属性:name, 值的改变: {
    name = "\U674e\U56db";
}

 

五、扩展

这个案例只是浅浅的探究了一下实现原理,其他这个还有更大的应用。

我们可以给NSObject创建一个分类NSObject(KVO),实现各种属性的监听实现。

具体操作自己动手去吧,不在本文做演示。

上一篇:Java之Lombok


下一篇:javascript-AngularJS获取器/设置器