OC归纳总结 — (8)OC底层之KVO

iOS 中KVO是官方的观察者模式的实现, 今天来仔细研究一下相关的内容.

1. 系统的KVO实现

iOS 中系统的KVO的API是依赖NSObject的分类方法:

// 通知回调方法API
@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end

// 注册/移除API
@interface NSObject(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)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), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
复制代码

当Person类的对象person进行kvo监听属性(addObserver:xxxxx)时, 会有如下实现过程:

  1. KVO依赖setter方法: KVO的本质会监听setter方法, 因此需要被观察的对象有成员变量, 以及setter方法, 就能通过KVO观察.
  2. **isa swizzling: **系统动态注册一个动态子类NSKVONotifying_Person, 后面用 newClass代替, 将person对象的isa指针指向动态newClass
  3. newClass 重写关键方法 : 动态子类需要重写 setter, -class, dealloc, _isKVO 方法
    1. setter中, 处理 old, new value, 然后调用 originalClass 的 setter 方法
    2. -class中: 隐藏 “NSKVONotifying_Person的实现, 直接返回 class_getSuperclass`, 也就 Person类
    3. dealloc: 中判断, 并确保isa swizzling 重写 person对象改回 Person
    4. _isKVO: 标记是否是KVOClass
  4. 当对象person被移除kvo监听(removeObserver)时, 会通过 isa swizzling 将 person对象指向原来的类
  5. 当对象person的监听全部被移除时 — 动态子类不会被注销!!! 防止后续重复注册监听需要重复创建动态子类!!!

addObserver中的context作用: 防止keyPath 同名, 可以用环境变量区分

另外 setter 中如果直接使用self->_xxx = xx可以使用willChangeValueForKey:didChangeValueForKey: 手动触发KVO

2. 重写KVO

根据系统的KVO的逻辑, 我们可以根据系统的实现逻辑, 简单自定义一套逻辑重写KVO, 实现并不完美, 还有以下问题:

  1. 监听的对象keyPath的内容不够
  2. dealloc中无法自动释放
  3. 动态子类无法自动释放
@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
复制代码
#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
复制代码

开源方案 FBKVOController 对系统的KVO进行很好的改造, 后面进行分析.

另外 gnustep-base-1.28.0中有GNUStep版本的KVO也可以参考以下

3. FBKVOController 利用中介者模式封装系统KVO

FBKVOController 中对需要观察某个对象的属性, 通常用以下API:

#pragma mark - lazy
- (FBKVOController *)kvoCtrl{
    if (!_kvoCtrl) {
        _kvoCtrl = [FBKVOController controllerWithObserver:self];
    }
    return _kvoCtrl;
}

// 调用方法
[self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
		NSLog(@"****%@****",change);
}];
复制代码

其中[FBKVOController controllerWithObserver:self]会弱引用持有 self指针, 也就是 observer,

在FBKVOController内部将关键信息封装成FBKVOInfo:

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
复制代码

此时两个关键内容, 分贝是KVOInfo 观察者以及回调信息object 被观察者, 然后交给全局的中介者单例_FBKVOSharedController管理:

  [[_FBKVOSharedController sharedController] observe:object info:info];
复制代码

使用单例_FBKVOSharedController调用系统API addObserver:forKeyPath:options:context:, 其中 observer就是 _FBKVOSharedController sharedInstance!!! 被观察对象就是object!!! 通过context配置成KVOInfo来区分, 内部实现observeValueForKeyPath:...方法集中处理!!!

在被观察对象dealloc时, KVO自动removeObserver:的实现:

有一个FBKVOController关联对象(或者手动创建的成员变量)与被观察者是绑定的. 因此当关联对象AssociatedObject的dealloc调用时, 一定是被观察对象的dealloc调用, 此时在关联对象的deallocunobserve观察的内容可以自动调用removeObserver:...方法

有以下知识要点值得学习:

  1. 中介者模式
  2. 关联对象/成员属性释放时, 监听被观察者对象的dealloc释放

参考

juejin.cn/post/684490…
github.com/facebookarc…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享