iOS自定义KVO(一)

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

自定义KVO的思路

在自定义KVO之前,我们首先需要分析,系统的KVO是如何实现的,它的思路是什么?

我们自定义一个Person类,然后查看一下KVO方法的来源:

发现addObserver方法来自于NSObject的分类@interfaceNSObject(NSKeyValueObserverRegistration),是针对NSObject的拓展;

image.png

其回调方法来自于NSObject的分类@interfaceNSObject(NSKeyValueObserving)

image.png

那么我们就可以仿照系统的做法,我们给NSObject添加一个自定义的KVO分类,同样声明添加监听删除三个方法:

image.png

为了与系统方法进行区分,我们添加了自定义KVO的前缀ck;那么声明过后,我们如何实现这三个方法呢?这个代码我们应该如何去写? 根据我们之前的文章对系统KVO的分析,我们可以大致得出自定义KVO的步骤如下:

  1. 因为KVO是监听属性的变化,那么我们可以在setter方法中进行判断键值等问题;成员变量没有setter方法 ;
  2. 动态生成一个子类
  3. 修改isa的指向,指向子类
  4. 保存观察者

验证是否存在setter方法代码如下:

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有当前%@的setter",keyPath] userInfo:nil];
    }
}

#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];
}
复制代码

自定义KVO动态生成子类

动态生成一个子类的流程分为三步:

  1. 申请类
  2. 添加方法
  3. 注册类

申请类

申请类之前,我们首先要把类创建出来,根据旧类的类名,传建一个新类:

NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"CKKVONotifying_%@",oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重复创建生成新类
if (newClass) return newClass;
复制代码

接下来,我们就能去申请类了:

newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
复制代码

添加方法

添加方法时,我们主要添加classsetter方法;

  • 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)ck_class, classTypes);
复制代码
  • 添加setter方法
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)ck_setter, setterTypes);
复制代码
/// 子类重写imp
static void ck_setter(id self,SEL _cmd,id newValue){
	NSLog(@"setter 方法");
}

// 直接获取父类的实现
Class ck_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
复制代码

注册类

objc_registerClassPair(newClass);
复制代码

自定义KVO的setter方法

目前我们自定义的KVO已经能够触发setter方法,也就是ck_setter能够被调用了,那么我们如何给ck_setter方法添加实现呢?

说到底我们还是要改变原始类也就是Person类的属性值,那么就需要给Person也就是父类setter方法发送消息:

/// 子类重写imp
static void ck_setter(id self,SEL _cmd,id newValue){
    // 给父类发送消息
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    void (*ck_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    ck_msgSendSuper(&superStruct,_cmd,newValue);

    // 通知观察者 (在最开始的时候要使用关联对象保存观察者)
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCKKVOAssiociateKey));
    [observer ck_observeValueForKeyPath:keyPath ofObject:self change:@{keyPath:newValue} context:NULL];
}

#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];
}
复制代码

目前我们已经简单实现了监听,我们来验证一下:

ck_addObserver方法实现如下 image.png

接下来验证一下监听逻辑:

image.png

我们已经初步实现了属性变化监听逻辑;

但是,这还不完美,我们如果要观察多个属性呢?如何移除观察者呢?我们下回继续分析......

猜你喜欢

转载自juejin.im/post/7016994912612646942