iOS进阶OC基础

KVO 本质 & 自定义实现

2020-06-18  本文已影响0人  xiaoyouPrince

KVO 本质 & 自定义实现

KVO 是什么?

Key-Value Observer 即键值观察者。作用为监听某个对象的某个属性的值发生改变,通知其观察者,观察者可以做出相应的处理。

KVO 的使用方法很简单,代码如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    XYPerson *p1 = [XYPerson new];
    XYPerson *p2 = [XYPerson new];
    
    // 1.设置监听
    [p2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    
    // 触发
    p1.age = 10;
    p2.age = 20;
    
    // 3.移除监听
    [p2 removeObserver:self forKeyPath:@"age"];
}

// 2.收到监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"keyPath = %@",keyPath);     // keyPath = age
    NSLog(@"object = %@",object);       // object = <XYPerson: 0x6000010d68b0>
    NSLog(@"change = %@",change);       // change = { kind = 1; new = 20;}
    NSLog(@"context = %@",context);     // context = (null)
}

内部实现原理

15919555920910.jpg
[self willChangeValueForKey:@"age"];  // 发生改变前
[super setAge:age];
[self didChangeValueForKey:@"age"]; // 发生改变之后,内部会实现对 observer 的回调
添加KVO之前: 
p2 : <XYPerson: 0x600002ce4530>
p2->isa : XYPerson

添加KVO之后:
p2 : <XYPerson: 0x600002ce4530>     // 因为运行时,修改了内部的 class 实现,屏蔽真实类
p2->isa : NSKVONotifying_XYPerson

移除KVO之后: 
p2 : <XYPerson: 0x600002ce4530>
p2->isa : XYPerson

自定义实现 KVO

系统 KVO 好用,但是其写法为固定的三段式,且代码比较分散(见上面 KVO 示例代码)。

熟悉了 KVO 的原理,就可以自己基于 runtime 来实现一个简单的 KVO。直接上代码

/// 一行代码添加KVO监听
/// @param key 需要被监听的key
/// @param options 一个混合选项,在 readonly 属性中与系统同效果
/// @param handler 监听回调,需注意 block 循环引用
- (void)xy_addObserverForKey:(NSString *)key
                     options:(NSKeyValueObservingOptions)options
             observerHandler:(xy_observerHandler)handler;

这个 KVO 可以实现一行代码添加 KVO 监听,回调直接在 block 中执行,且无需手动执行移除监听。监听关系会在内部通过 runtime 与 被观察对象进行绑定。当被监听对象销毁的时候,自行销毁。

具体实现如下:

- (void)xy_addObserverForKey:(NSString *)key options:(NSKeyValueObservingOptions)options observerHandler:(xy_observerHandler)handler
{
    // 1. 校验
    SEL setterSEL = [self selWithKey:key];
    Method setterMethod = class_getInstanceMethod(self.class, setterSEL);
    if (!setterMethod) {
        // 没有相应的 setter 方法实现。 如 WKWebView.title
        // 此处为 readonly 方法,具体实现可以看 Demo
        return;
    }
    
    // 2. 创建子类
    Class clz = object_getClass(self);
    NSString *clzName = NSStringFromClass(clz);
    if (![clzName containsString:clzPre]) {
        clz = [self creatKVOClassWithOriginalClassName:clzName];
        
        // 设置成子类
        object_setClass(self, clz);
    }
    
    // 3. 添加方法,仅添加一次
    if (![self hasSeleter:setterSEL]) {
        const char *types = method_getTypeEncoding(setterMethod);
        BOOL s = class_addMethod(clz, setterSEL, (IMP)kvo_setter, types);
        NSLog(@"%d",s);
    }
    
    // 4. 保存KVO相关内容
    KVOInfomation *info = [[KVOInfomation alloc] initWithObserver:self key:key opeitons:options handler:handler];
    NSMutableArray *infos = objc_getAssociatedObject(self, xy_infosKey);
    if (!infos) {
        infos = [NSMutableArray array];
        objc_setAssociatedObject(self, xy_infosKey, infos, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [infos addObject:info];
}

第一步校验此 Key 是否为 readonly 的属性,如果是 readonly 属性则没有 setter 方法,就无法通过创建子类监听 setter 然后代用父类 setter 的方式实现。系统的实现应该是内部做了兼容,当加载到被监听的 Key 时候主动调用了监听器方法。

/// 通过key获取父类的 set 方法
/// @param key 监听的key
- (SEL)selWithKey:(NSString *)key{
    // 父类是否存在此set方法
    NSString *selName = [NSString stringWithFormat:@"%@%@%@",@"set",[self xy_firstCharUpper:key],@":"];
    SEL sel = NSSelectorFromString(selName);
    return sel;
}
- (NSString *)xy_firstCharUpper:(NSString *)string
{
    if (string.length == 0) return string;
    NSMutableString *stringM = [NSMutableString string];
    [stringM appendString:[NSString stringWithFormat:@"%c", [string characterAtIndex:0]].uppercaseString];
    if (string.length >= 2) [stringM appendString:[string substringFromIndex:1]];
    return stringM;
}

第二步创建子类,先判断当前对象是否已经是子类,如果是首次就创建子类并将当前对象指向其子类类型。

/// 通过本类创建KVO监听的子类
- (Class)creatKVOClassWithOriginalClassName:(NSString *)originalClassName{
    
    NSString *originalClzName = originalClassName;
    
    NSString *newClassName = [clzPre stringByAppendingString:originalClzName];
    if (objc_getClass(newClassName.UTF8String)) {
        return NSClassFromString(newClassName);
    }
    
    Class newClass = objc_allocateClassPair(self.class, [newClassName UTF8String], 0);
    
    // 重写class 方法,指向父类,隐藏kvo子类
    const char *types = method_getTypeEncoding(class_getInstanceMethod(newClass, @selector(class)));
    class_addMethod(newClass, @selector(class), (IMP)kvo_class, types);
    
    objc_registerClassPair(newClass);
    
    return newClass;
}

// 重写子类的 class 实现,屏蔽其子类的存在
static Class kvo_class(id self, SEL _cmd)
{
    return class_getSuperclass(object_getClass(self));
}

第三步给子类添加被监听Key的 setter 方法实现,其内部调用父类的 setter 方法,并执行监听回调

static void kvo_setter(id self, SEL _cmd, id newValue)
{
    // 给父类发送修改值的消息
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [self getterFromSetterName:setterName];
    if (!getterName) {
        NSLog(@" ----- 原来类中没有 getter 方法");
    }
    
    id oldValue = [self valueForKey:getterName];
    
    // 调用父类setter
    // 构造 objc_super 的结构体
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    
    // 对 objc_msgSendSuper 进行类型转换,解决编译器报错的问题
    void (* objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ,传入结构体、方法名称,和参数等
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // 回调 handler - 依据options
    NSMutableArray *infos = objc_getAssociatedObject(self, xy_infosKey);
    for (KVOInfomation *info in infos) {
        if ((info.observer == self) && ([info.key isEqualToString:getterName])) {
            NSDictionary *dict = @{
                @"observeredObj": info.observer,
                @"key": info.key,
                NSKeyValueChangeNewKey: newValue?:@"",
                NSKeyValueChangeOldKey: oldValue?:NSNull.null
            };
            info.handler(dict);
        }
    }
}

第四步保存添加的监听信息,当监听到某个 Key 发生回调之后,可以去内部缓存中去调用回调。

关于监听关系类的设计如下,其属性均为 readonly, 规避了一些循环引用问题。

@interface KVOInfomation : NSObject
/** observer: 被观察对象 */
@property (nonatomic, readonly,weak)       NSObject * observer;
/** key */
@property (nonatomic, readonly,copy)         NSString *key;
/** options */
@property (nonatomic, readonly,assign)       NSKeyValueObservingOptions options;
/** key */
@property (nonatomic, readonly,copy)         xy_observerHandler handler;

- (instancetype)initWithObserver:(id)observer key:(NSString *)key opeitons:(NSKeyValueObservingOptions)options handler:(xy_observerHandler)handler;

@end

代码就写到这里了,有兴趣的可以看一下全部 Demo

祝大家学习进步~

上一篇下一篇

猜你喜欢

热点阅读