iOS 探索KVO三(自定义KVO)

前言

前两篇KVO的文章介绍了一些KVO的使用方式和KVO的实现原理,这篇文章我们来自定义KVO;

自定义KVO

自定义KVO首先要有个大概思路,根据KVO原理来进行操作:

自定义KVO源码请移步github点击下载

1.添加通知即:

- (void)xz_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

1.1.验证是否有setter方法:没有不然进来

因为KVO是基于setter方法监听的,所以需要判断,类中是否有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];
    }
}

1.2.动态生成子类

这里需要注意几个点:

1.类为动态生成的,这里需要同时添加几个类方法,class,setter方法等,

     1.1:申请类

     1.2:注册类

     1.3:添加class方法

     1.4  添加set方法

2.消息转发

     2.1首先消息转发给父类,在外部使用self.person.nickName = @”Alan“这里实际类调用的是XZKVONotifying_类,所以需要让XZPerson对象中的值变化,所以需要转发给父类;

     2.2:获取观察者观察者,并使用观察者进行消息转发

 
static NSString *const kXZKVOPrefix = @"XZKVONotifying_";
static NSString *const kXZKVOAssiociateKey = @"kXZKVO_AssiociateKey";


//调用生成新类
Class newClass = [self createChildClassWithKeyPath:keyPath];


#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    // 2.1 判断是否有了
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kXZKVOPrefix,oldClassName];// XZKVONotifying_XZPerson
    Class newClass = NSClassFromString(newClassName);
    if (newClass) {
        return newClass;
    }
    /**
     * 如果内存不存在,创建生成
     * 参数一: 父类
     * 参数二: 新类的名字
     * 参数三: 新类的开辟的额外空间
     */

    // 2.1 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 注册类
    objc_registerClassPair(newClass);
    // 2.3.1 添加class方法
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getClassMethod([self class], @selector(class));
    const char *classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)xz_class, classType);
    // 2.3.2 添加setter方法 setNickname
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getClassMethod([self class], setterSEL);
    const char *setterType = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)xz_setter, setterType);
    
    return newClass;
}
static void xz_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    
   //4:消息转发: 转发给父类
    //改变父类的值---可以强制类型转换
    void (*xz_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    /**
     newvalue 这里修改了子类的的值,父类值是没有改变的
     使用 objc_msgSendSuper给父类的setter方法发送消息修改值
    */
    // 回调给外界
    
    struct objc_super superStruct = {
        .receiver       = self,
        .super_class    = [self class]
    };
    //4.1转发给父类
    xz_msgSendSuper(&superStruct,_cmd,newValue);
    
    //4.2 获取观察者观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kXZKVOAssiociateKey));
    //4.3消息发送观察者
    SEL observerSel = @selector(observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    objc_msgSend(observer,observerSel,keyPath,self,@{keyPath:newValue},NULL);
}

//因为系统KVO中调用class时返回的是XZPerson类所以这里也返回父类指针节点
Class xz_class(id self,SEL _cmd){
    //这里返回父类的isa
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;
        
    }
    //setter 值为setNickName
    NSRange range = NSMakeRange(3, setter.length-4);
    //去掉set和属性首字母
    NSString *getter = [setter substringWithRange:range];
    //首字母需要小写
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    //转换为nickname
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

1.3.isa指向:XZKVONotifying_XZPerson

    // 3: isa 指向 isa_swizzling
    object_setClass(self, newClass);

1.4 保存观察者

    //4.保存观察者
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kXZKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

源码为:

- (void)xz_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{

    // 1: 验证setter
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa 指向 isa_swizzling
    object_setClass(self, newClass);
    //4.保存观察者
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kXZKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

2.消息转发:响应回调使用原有系统回调即可(这里直接使用系统的,直接转发消息了)

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

3.移除通知

       移除通知,指回父

- (void)xz_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    // 指回给父类
    Class superClass = [self class];
    object_setClass(self, superClass);
}

外部调用 

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[XZPerson alloc] init];
      [self.person xz_addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    // Do any additional setup after loading the view.
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@",self.person.nickName);
    self.person.nickName = @"alan";
}

#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

- (void)dealloc{
    [self.person xz_removeObserver:self forKeyPath:@"nickName"];
}

输出结果:

总结

这篇文章实现了KVO的简单的自定义,只是通过监听单独的属性.但是还是有缺陷的:如监听多个属性呢,没有实现监听的自动销毁等;下篇文章进行自定义KVO进阶。

希望对大家有用处,欢迎大家点赞+评论,关注我的CSDN,我会定期做一些技术分享!未完待续。。。

 

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/105864940