KVO is the official observer mode implementation in iOS, and today we’ll take a closer look at it.

1. KVO implementation of the system

The iOS system’s KVO API relies on NSObject classification:

// API @interface NSObject(NSKeyValueObserving) - (void) observeforkeypath :(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; @ the end / / registration/removing API @ interface NSObject (NSKeyValueObserverRegistration) - (void) addObserver: (NSObject *) the observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE (macos (10.7), the ios (5.0), watchos (2.0), tvos (9.0)); - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; @endCopy the code

When Person (addObserver: XXXXX) performs the kVO listener attribute, the following implementation will occur:

  1. KVO relies on setter methods: By its very nature, KVO listens for setter methods, so objects that need to be observed have member variables and setter methods that can be observed through KVO.
  2. ** Isa Swizzling: ** The system dynamically registers a dynamic subclassNSKVONotifying_PersonThe isa pointer to the Person object points to the dynamic newClass
  3. NewClass overrides key methodsDynamic subclasses need to override setter, -class, dealloc, _isKVO methods
    1. In the setter, we’re going to do old, new value, and then we’re going to call the setter method of originalClass
    2. – In class: hide NSKVONotifying_Person", returns directlyClass_getSuperclass, which is the Person class
    3. Dealloc: and make sure isa Swizzling overwrites the Person object to change back to Person
    4. _isKVO: indicates whether the flag is KVOClass
  4. When the object person is removed, the KVO listener (removeObserverIsa Swizzling points the Person object to the original class
  5. Dynamic subclasses are not deregistered when listener on object Person is completely removed!! Create dynamic subclasses repeatedly to prevent repeated registration listening!!

Context in addObserver prevents keyPath from having the same name, which can be distinguished by environment variables

In addition, the setter can manually trigger KVO with willChangeValueForKey: and didChangeValueForKey: if you use self->_xxx = xx directly

2. Rewrite the KVO

According to the logic of KVO of the system, we can simply customize a set of logic to rewrite KVO according to the implementation logic of the system. The implementation is not perfect. There are also the following problems:

  1. The listening object has insufficient keyPath content
  2. Dealloc cannot be released automatically
  3. Dynamic subclasses cannot be released automatically
@interface NSObject (LGKVO)

typedef void(^LGKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block;
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
Copy the code
#import "NSObject+LGKVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

static NSString *const kLGKVOPrefix = @"LGKVONotifying_";
static NSString *const kLGKVOAssiociateKey = @"kLGKVO_AssiociateKey";

// 中间对象, 防止强引用 observer
@interface LGKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) LGKVOBlock  handleBlock;
@end

@implementation LGKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.handleBlock  = block;
    }
    return self;
}
@end

@implementation NSObject (LGKVO)

#pragma mark - add observer
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block{
    //1. 判断 keyPath 是否是 KVC
    [self judgeSetterMethodFromKeyPath:keyPath];
    //2. 注册动态子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3. isa swizzling
    object_setClass(self, newClass);
    //4. 使用关联对象将 observer 与 self 对象绑定!!!
    LGKVOInfo *info = [[LGKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];

    NSMutableArray *observersArr = objc_getAssociatedObject(self, (__bridge  const void *)kLGKVOAssiociateKey);
    if (!observersArr) {
        observersArr = [NSMutableArray arrayWithCapacity:1];
        [observersArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void *)kLGKVOAssiociateKey, observersArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}

// 必须提前强制退出, 否则有风险 -> 因为关联对象持有了外部的 observer, 这里是强引用
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (observerArr.count <= 0) {
        return;
    }
    
    for (LGKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge  const void *)kLGKVOAssiociateKey, observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    // isa 指回给父类
    if (observerArr.count<=0) {
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

// BOOL responder = [self respondsToSelector:setterSEL];
-(void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
    // 1. 判断keyPath 构造的 setter 是否拥有实现
    NSString *setterStr = setterForGetter(keyPath);
    
    SEL setterSEL = NSSelectorFromString(setterStr);
    
    Class cls = object_getClass(self);
    Method setterMethod = class_getInstanceMethod(cls, setterSEL);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
    }
}

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    // 注册动态子类
    // 1. 获取对象自己的Class
    Class oldCls = object_getClass(self);
    NSString *oldClsName = NSStringFromClass(oldCls);
    NSString *newClsName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClsName];

    Class newCls = NSClassFromString(newClsName);
    if (newCls) {
        return newCls;
    }
    
    // 2. 分配资源
    newCls = objc_allocateClassPair(oldCls, newClsName.UTF8String, 0);
    // 3. 注册 newCls 到 runtime 中
    objc_registerClassPair(newCls);
    
    {
        // 3.1 添加 setter 方法
        NSString *setterStr = setterForGetter(keyPath);
        SEL setterSEL = NSSelectorFromString(setterStr);
        Method method = class_getInstanceMethod(oldCls, setterSEL);
        const char *types = method_getTypeEncoding(method);
        class_addMethod(newCls, setterSEL, (IMP)lg_setter, types);
    }
    
    {
        // 3.2 添加 -class 方法
        SEL classSEL = NSSelectorFromString(@"class");
        Method method = class_getInstanceMethod(oldCls, classSEL);
        const char *types = method_getTypeEncoding(method);
        class_addMethod(newCls, classSEL, (IMP)lg_class, types);
    }

//    { 自动释放
//        // 3.3: newClass 添加dealloc -> 中间类创建一个 dealloc 方法!!
//        SEL deallocSEL = NSSelectorFromString(@"dealloc");
//        Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
//        const char *deallocTypes = method_getTypeEncoding(deallocMethod);
//        class_addMethod(newCls, deallocSEL, (IMP)lg_dealloc, deallocTypes);
//    }
    return newCls;
}

// dealloc 时, 将 isa 指针重置
//static void lg_dealloc(id self,SEL _cmd){
//    Class superClass = [self class];
//    object_setClass(self, superClass);
//}

static void lg_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"lg_setter self: %@, _cmd: %@, newValue: %@", self, NSStringFromSelector(_cmd), newValue);
    void (*lg_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    void (*lg_msgSend)(void *, SEL, id, id, id, id) = (void *)objc_msgSend;

    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    
    lg_msgSendSuper(&superStruct, _cmd, newValue);
    
    // 1. 获取 observer, 调用 observerxxx方法
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));

    // 5: 信息数据回调
    for (LGKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

static Class lg_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
    if (getter.length <= 0) { return nil;}
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end
Copy the code

The open source scheme FBKVOController has a good transformation of the system’s KVO, which will be analyzed later.

In addition, gnustep version of KVO in gnustep-base-1.28.0 can also be referred to below

3. FBKVOController uses intermediary mode to encapsulate system KVO

FBKVOController typically uses the following API to observe properties of an object:

#pragma mark - lazy - (FBKVOController *)kvoCtrl{ if (! _kvoCtrl) { _kvoCtrl = [FBKVOController controllerWithObserver:self]; } return _kvoCtrl; } / / call methods [self kvoCtrl observe: the self. The person keyPath: @ "name" options: (NSKeyValueObservingOptionNew) block: ^ (id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { NSLog(@"****%@****",change); }];Copy the code

Among them [FBKVOController controllerWithObserver: self] will be a weak reference pointer to hold the self, which is the observer,

Encapsulate key information inside FBKVOController as FBKVOInfo:

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
Copy the code

The decibel is the KVOInfo observer and the callback message with the object observed, which is then handed to the global mediator singleton _FBKVOSharedController for management:

  [[_FBKVOSharedController sharedController] observe:object info:info];
Copy the code

Using singleton _FBKVOSharedController call system apis addObserver: forKeyPath: options: context:, Observer is _FBKVOSharedController sharedInstance!! The observed object is object!! Through the context configured to KVOInfo will distinguish, internal implementation observeValueForKeyPath:… Method concentration processing!!

KVO automatic removeObserver:

There is an FBKVOController associated object (or manually created member variable) that is bound to the observed. So when the dealloc of the AssociatedObject AssociatedObject is called, it must be the dealloc of the observed object, and the unobserved contents of the dealloc of the AssociatedObject can be automatically called removeObserver: methods

The following knowledge points are worth learning:

  1. The mediator pattern
  2. When the associated object/member property is released, the dealloc of the monitored object is released

reference

Juejin. Cn/post / 684490… Github.com/facebookarc…