Runtime梳理(二)KVO原理及实现

2018-11-12  本文已影响0人  飞奔的小鲨鱼

简单使用

@interface ViewController ()
@property (nonatomic, strong) Person * p1;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p1 = [[Person alloc] init];
    p1.name = @"Tom";
    self.p1 = p1;
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld
     |NSKeyValueObservingOptionNew context:nil];
    p1.name = @"Jack";
  // 没有self.p1 = p1; 需要将观察者移除,不然程序会奔溃
  // [p1 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 发生了变化 ---");
    }
}

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

通过上面的代码我们就完成了对p1对象的name属性的监听,name的改变都会通知观察者。

底层原理

KVO的全称Key-Value Observing,是我们俗称的观察者模式,官方的文档是这样描述的:

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

不难看出,KVO的实现基于isa-swizzling,当被观察对象属性发生变化时,原本指向被观察对象类的isa指针,指向了一个中间的类,并不是真正的被观察的类。最后也提到了,不要通过isa来判断类的继承关系,而是通过class方法来判断一个类的实例。

为了探究KVO的实现,我们重写了Person的description方法

-(NSString *)description{
    IMP setName = class_getMethodImplementation(object_getClass(self),
                                                              @selector(setName:));
    Class class = [self class];
    Class getClass = object_getClass(self);
    Class superClass = class_getSuperclass(getClass);
    return [NSString stringWithFormat:@"\n class -> %@ ,\
                                 \n object_getClass -> %@ , \
                                \n class_getSuperclass -> %@ ,\
                                \n setName -> %p",class,getClass,superClass,setName];
}

在添加观察者之前和之后分别做了打印:

NSLog(@"添加之前 :%@",p1);
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld
     |NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加之后 :%@",p1);

运行结果如下:

添加之前 :
 class -> Person ,                                          
 object_getClass -> Person ,                                           
 class_getSuperclass -> NSObject ,                                          
 setName -> 0x1048d7340
添加之后 :
 class -> Person ,                                          
 object_getClass -> NSKVONotifying_Person ,                                           
 class_getSuperclass -> Person ,                                          
 setName -> 0x1049dbc3d
name 发生了变化 ---

对比发现:

到此,有没有一种"拨开云雾见天明"的感脚,我们可以大胆的猜想一下它的实现流程:

在对p1对象name属性使用KVO后,在程序运行的过程中生成了一个NSKVONotifying_Person的类,这个类继承自Person类,把p1对象的isa指针指向了生成的NSKVONotifying_Person类,并且重写了setName方法,重写了class方法,隐藏了它真实的类型,使我们误以为还是原来的Person类。

官方文档中对KVO的实现细节避之不提,不过这也符合苹果的一贯作风,但是我们可以看到KVO通知观察者的方法有两种:
1.Automatic Change Notification
通过以下几种方法可以自动通知观察者,无需添加额外的代码

[p1 setName:@"Jack"];
[p1 setValue:@"Jack" forKey:@"name"];
[p1 setValue:@"Jack" forKeyPath:@"name"];

2.Manual Change Notification

In this case, you override the NSObject implementation of automaticallyNotifiesObserversForKey:. For properties whose automatic notifications you want to preclude, the subclass implementation of automaticallyNotifiesObserversForKey: should return NO. A subclass implementation should invoke super for any unrecognized keys. The example in Listing 2enables manual notification for the balance property, allowing the superclass to determine the notification for all other keys

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

To implement manual observer notification, you invoke [willChangeValueForKey:] before changing the value, and [didChangeValueForKey:] after changing the value

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

手动调用的话,在automaticallyNotifiesObserversForKey:方法返回NO,并且在set方法中,值改变之前调用[willChangeValueForKey:],值改变之后调用[didChangeValueForKey:]

- (void)setName:(NSString *)name{
    name = name;
}

- (void)willChangeValueForKey:(NSString *)key{
    NSLog(@"willChangeValueForKey: --- begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: --- end");
}

- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey: --- begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: --- end");
}

打印结果:

willChangeValueForKey: --- begin
willChangeValueForKey: --- end
didChangeValueForKey: --- begin
name 发生了变化 ---
didChangeValueForKey: --- end

不管是Automatic Change Notification也好还是Manual Change Notification也好,最后都调用了[willChangeValueForKey:],和[didChangeValueForKey:] 方法,唯一的区别就是Automatic Change Notification自动帮我们调用了这两个方法。而[didChangeValueForKey:] 方法内部应该是调用了监听者observeValueForKeyPath: ofObject:change:context:的方法,从而在被监听对象属性值发生改变的时候,通知观察者达到监听的目的。

现在对KVO的实现流程应该有了更加清楚地认识了吧?没有的话,请看下面的图:

KVO实现流程图.png

简单实现

首先,创建了一个NSObject的分类,添加了一个监听的方法,在这个方法里主要做了以下几件事情(以Person name为例):

@implementation NSObject (KVO)
- (void)xsy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath 
options:(NSKeyValueObservingOptions)options context:(void *)context{
 
    NSString * firstString = [[keyPath substringToIndex:1] uppercaseString];
    NSString * otherString = [[keyPath substringFromIndex:1] lowercaseString];
    NSString * setKeyPath  = [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
    SEL keySel = NSSelectorFromString(setKeyPath);
    Method keyMethod = class_getInstanceMethod([self class], keySel);
  
    // 获取当前对象的类
    Class oriClass = [self class];
    NSString * oriString = NSStringFromClass(oriClass);
    NSString * kvoString = [NSString stringWithFormat:@"XSYKVONotifying_%@",oriString];
    const char * kvoClassName = [kvoString cStringUsingEncoding:NSUTF8StringEncoding];
    Class kvoClass = objc_getClass(kvoClassName);
    if (!kvoClass) {
        kvoClass = objc_allocateClassPair([oriClass class], kvoClassName, 0);
        objc_registerClassPair(kvoClass);
        object_setClass(self, [kvoClass class]);
    Method method = class_getInstanceMethod([kvoClass class], keySel);
    Method instanceMethod = class_getInstanceMethod([oriClass class], @selector(class));
    Method classMethod = class_getClassMethod([oriClass class], @selector(class));
    
    // 重写set方法
    const char *types = method_getTypeEncoding(class_getInstanceMethod([Person class], @selector(setName:)));
    BOOL result = class_addMethod(student, @selector(setName:), (IMP)(xsy_setter), types);
    
    Method setUndefined = class_getInstanceMethod([oriClass class], @selector(setValue:forUndefinedKey:));
    Method valueUndefined = class_getInstanceMethod([oriClass class], @selector(valueForUndefinedKey:));
    
    BOOL result1 = class_addMethod(kvoClass, @selector(setValue:forUndefinedKey:), (IMP)(xsy_setValueForUndefinedKey), nil);
    if (!result1) {
        method_setImplementation(setUndefined, (IMP)(xsy_setValueForUndefinedKey));
    }
    
    BOOL result2 = class_addMethod(kvoClass, @selector(valueForUndefinedKey:), (IMP)(xsy_valueForUndefinedKey), nil);
    if (!result2) {
        method_setImplementation(valueUndefined, (IMP)(xsy_valueForUndefinedKey));
    }
    
    BOOL result3 = class_addMethod(kvoClass, @selector(class), (IMP)(xsy_class), nil);
    if (!result3) {
       // 重写class方法
        method_setImplementation(classMethod, (IMP)(xsy_class));
    }
   
   // 将观察者和被观察的对象关联
    objc_setAssociatedObject(self, (__bridge const void *)(observerKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

set方法的实现

static void xsy_setter(id self, SEL _cmd, id value){
    NSString * setterName = NSStringFromSelector(_cmd);
    // setName:
    NSString * keyName = [setterName substringFromIndex:3];
    NSString * getterName = [keyName substringWithRange:NSMakeRange(0, keyName.length-1)];
    NSString * firstL = [[getterName substringToIndex:1] lowercaseString];
    NSString * otherL = [getterName substringFromIndex:1];
    getterName = [NSString stringWithFormat:@"%@%@",firstL,otherL];
    
    id oldValue = [self valueForKey:getterName];

    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // 调用父类的方法
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperCasted(&superClass, _cmd, value);
  
   // 通知观察者回调
    id observer = objc_getAssociatedObject(self, (__bridge const void *)(observerKey));
    NSDictionary<NSKeyValueChangeKey,id> * change = oldValue ? @{NSKeyValueChangeNewKey : value, NSKeyValueChangeOldKey : oldValue} : @{NSKeyValueChangeNewKey : value};
    [observer observeValueForKeyPath:getterName ofObject:self change:change context:nil];
}

viewController

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [[Person alloc] init];
    self.p.name = @"小鲨鱼";
  
    [self.p xsy_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|
NSKeyValueObservingOptionOld context:nil];
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.number++;
    self.p.name = [NSString stringWithFormat:@"%@_%ld",@"小鲨鱼", self.number];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
              change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
              context:(void *)context{
    NSLog(@"ViewController ---- > observeValueForKeyPath   %@",change);
}
@end

这样我们在点击屏幕的时候就会看到控制台的打印,大致实现了基本流程。

总结

KVO的实现机制过程中,Runtime会动态的创建一个类,这个类以NSKVONotifying_类名命名,并且继承自类名,重写了class方法隐藏了真正类型,将被观察对象的isa指针指向了这个新创建的类,重写了被观察对象keyPath的set方法,通知观察者的回调方法,从而实现了keyPath值变化的监听。

参考:
官方文档
iOS底层原理总结 - 探寻KVO本质

上一篇下一篇

猜你喜欢

热点阅读