iOS-底层原理-自定义KVO

1.自定义KVO

1.上一篇博客了解了iOS 系统KVO的底层实现原理,那么这里进行自定义KVO,更好的理解原理和熟悉一些runtime的c方法的调用和功能,利用NSObject分类来实现

2.大体步骤如下

1.验证是否是属性,非属性不进行处理

2.保存观察者,分类多数时候用到关联对象

3.动态生成子类LGKVONotifying_本类名,前缀没什么限制
3.1 申请类
3.2 注册类
3.3 添加一些方法,这里主要添加 setter class
3.4 isa指向子类LGKVONotifying_本类名

4.子类LGKVONotifying_本类名的setter方法处理
4.1 给父类发送setter消息
4.2 给观察者发送回调消息

3.详细代码

3.1 注册观察者,外面调用者是Person类,observer是controller

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGNSKeyValueObservingOptions)options {
    
    // 1.验证是否存在setter方法:不让成员变量进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    // 2.保存观察者信息,用于发送消息
    
    // *****这里的知识点*****
    // 2.1 分类里面使用关联对象进行保存比较多
    // 2.2 LGKVOInfo数据模型里面是弱引用,关联对象是强引用,这样既没有对observer进行强引用,又达到了保存的目的
    NSMutableArray * infoArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (!infoArray) {
        infoArray = [NSMutableArray arrayWithCapacity:1];
    }
    LGKVOInfo * info = [[LGKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath options:options];
    [infoArray addObject:info];
    
    // 关联对象保存 观察者信息数组
    objc_setAssociatedObject(self, (__bridge  const void * _Nonnull)(kLGKVOAssiociateKey), infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 3.生成派生子类
    // 新类名为 LGKVONotifying_Person
    NSString * oldClassName = NSStringFromClass([self class]);
    NSString * newClassName = [NSString stringWithFormat:@"%@%@", kLGKVOPrefix, oldClassName];
    
    Class newClass = NSClassFromString(newClassName);
    
    // 这个if只会进来一次
    if (!newClass) {
        
        // 3.1申请类 开辟新的类 size 大小为0,根据父类创建名称为newClassName的新类,开辟空间为0,objc_allocateClassPair申请子类
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        
        // 3.2注册到内存
        objc_registerClassPair(newClass);
        
        // 3.3添加class方法 // 不写class setter方法的IMP会循环进入
        SEL classSel = @selector(class);
        Method classMethod = class_getInstanceMethod([self class], classSel);
        const char * classType = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSel, (IMP)lg_class, classType);
        
        // 3.4 isa 指向新的类 LGKVONotifying_本类名
        object_setClass(self, newClass);
    }
    
    // 3.3添加setter方法,这里只模拟iOS系统的 setter和class方法
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    Method method = class_getInstanceMethod([self class], setterSel);
    const char * type = method_getTypeEncoding(method);
    class_addMethod(newClass, setterSel, (IMP)lg_setter, type);
}

3.2 class方法IMP

// 子类LGKVONotifying_LGPerson class方法的IMP
Class lg_class(id self, SEL _cmd) { // 为了返回LGPerson
    return class_getSuperclass(object_getClass(self));
}

3.3 setter方法IMP


// 子类LGKVONotifying_LGPerson setter方法的IMP
static void lg_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"LGKVONotifying_LGPerson setter IMP newValue == %@", newValue);

    // 获取旧的值,设置新值之前获取旧值
    NSString * keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKeyPath:keyPath];
    NSLog(@"oldValue == %@", oldValue);

    
    // 1.给父类发送对应的setter方法
    // 通用封装解决依赖性,发送消息到父类,不用关注到底是哪个类
    void (*lg_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = [self class],
    };
    lg_msgSendSuper(&superStruct, _cmd, newValue);
    
    
    // 2.给observer发送回调消息
    // 2.1 找到keyPath对应的observer
    // 2.2 获取新值旧值发送给observer
    NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    for (LGKVOInfo * info in mArray) {
        NSMutableDictionary<NSKeyValueChangeKey,id> * change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath]) {
            if (info.options & LGNSKeyValueObservingOptionNew) {
                [change setObject:newValue forKey:NSKeyValueChangeNewKey];
            } else {
                if (oldValue) {
                    [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                }
                [change setObject:newValue forKey:NSKeyValueChangeNewKey];
            }

            id observer = info.observer;
            if (observer && [observer respondsToSelector:@selector(lg_observeValueForKeyPath:ofObject:change:)]) {
                [observer lg_observeValueForKeyPath:keyPath ofObject:info.observer change:change];
            }
        }
    }
}

3.4移除观察者

// 移除观察者

- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    
    // 删除对应的observer
    NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    for (LGKVOInfo * info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && [info.observer isEqual:observer]) {
            [mArray removeObject:info];
            NSLog(@"removeObject mArray == %@ count == %lu", mArray, (unsigned long)[mArray count]);
            break;
        }
    }
    
    // 对应的observer没有观察的属性了,isa指向父类
    BOOL isHadObserver = [self isKVOA:observer];
    if (isHadObserver == NO) {
        // 指回父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

// 判断该observer还有没有其他需要观察的属性
// 这个类似系统的isKVOA:方法,有几个属性同时观察,必须全部移除后isa指针才会指向父类
- (BOOL)isKVOA:(NSObject *)observer {

    NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));

    BOOL isHadObserver = YES;
    for (LGKVOInfo * info in mArray) {
        if ([info.observer isEqual:observer]) {
            isHadObserver = NO;
            break;
        }
    }
    
    NSLog(@"last mArray == %@ count == %lu", mArray, (unsigned long)[mArray count]);

    // 关联对象保存 观察者信息数组
    objc_setAssociatedObject(self, (__bridge  const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    return isHadObserver;
}

3.5其他辅助方法

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

// 从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];
}

// 从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];
}

3.6调用自定义KVO

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.view.backgroundColor = [UIColor whiteColor];
    [self createButton];
    
    
    self.person = [[Person alloc] init];
    [self.person lg_addObserver:self forKeyPath:@"nickName" options:LGNSKeyValueObservingOptionOld];
    [self.person lg_addObserver:self forKeyPath:@"name" options:LGNSKeyValueObservingOptionOld];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    count++;
    self.person.nickName = [NSString stringWithFormat:@"%lu", (unsigned long)count];
}

- (void)lg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change {
    NSLog(@"change == %@", change);
}

- (void)createButton {
    UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.backgroundColor = [UIColor redColor];
    button.frame = CGRectMake(100, 100, 100, 100);
    [button setTitle:@"remove" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonPressed:(UIButton *)button {
    [self.person lg_removeObserver:self forKeyPath:@"nickName"];
    [self.person lg_removeObserver:self forKeyPath:@"name"];
}

3.7输出

2021-12-17 17:51:24.740939+0800 KVO_18_Custom[92333:7137885] LGKVONotifying_LGPerson setter IMP newValue == 9
2021-12-17 17:51:24.741213+0800 KVO_18_Custom[92333:7137885] getNickName in Person
2021-12-17 17:51:24.741356+0800 KVO_18_Custom[92333:7137885] oldValue == 8
2021-12-17 17:51:24.741438+0800 KVO_18_Custom[92333:7137885] setNickName: in Person
2021-12-17 17:51:24.741637+0800 KVO_18_Custom[92333:7137885] change == {
    new = 9;
    old = 8;
}
2021-12-17 17:51:24.914475+0800 KVO_18_Custom[92333:7137885] LGKVONotifying_LGPerson setter IMP newValue == 10
2021-12-17 17:51:24.914663+0800 KVO_18_Custom[92333:7137885] getNickName in Person
2021-12-17 17:51:24.914760+0800 KVO_18_Custom[92333:7137885] oldValue == 9
2021-12-17 17:51:24.914813+0800 KVO_18_Custom[92333:7137885] setNickName: in Person
2021-12-17 17:51:24.914966+0800 KVO_18_Custom[92333:7137885] change == {
    new = 10;
    old = 9;
}
2021-12-17 17:51:25.107261+0800 KVO_18_Custom[92333:7137885] LGKVONotifying_LGPerson setter IMP newValue == 11
2021-12-17 17:51:25.107462+0800 KVO_18_Custom[92333:7137885] getNickName in Person
2021-12-17 17:51:25.107644+0800 KVO_18_Custom[92333:7137885] oldValue == 10
2021-12-17 17:51:25.107745+0800 KVO_18_Custom[92333:7137885] setNickName: in Person
2021-12-17 17:51:25.107955+0800 KVO_18_Custom[92333:7137885] change == {
    new = 11;
    old = 10;
}

4.一切看起来很完美,实则隐藏着大坑,当系统的KVO和自定义的KVO同时使用就会崩溃或者其他问题出现

4.1 混合调用

    [self.person lg_addObserver:self forKeyPath:@"nickName" options:LGNSKeyValueObservingOptionOld];
    [self.person lg_addObserver:self forKeyPath:@"name" options:LGNSKeyValueObservingOptionOld];
    
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];

4.2子类isa指向出现问题,而且会递归调用setter方法导致崩溃


iOS系统KVO和自定义KVO同时使用子类紊乱.jpeg

4.3解决方案
基于系统派生类自定义KVO,这种思路不需要创建自定义的派生类,代码实现上与自定义派生类KVO大同小异,先调用系统方法生成系统派生类,再修改系统派生类的setter方法IMP,IMP里面分别给父类和观察者发送消息

4.4基于系统派生类自定义KVO 具体代码,addObserver和removeObserver都是系统的,只改变setter的IMP

4.5注册KVO代码

- (void)addObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath changedBlock:(_EasyKVOChangedBlock)block {
    if (!observer || keyPath.length < 1) return;

    /* 使用系统方法获得派生类 */
    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

    /* 保存 block 信息 */
    [_tipsMap(self, _cmd) setObject:[block copy] forKey:[NSString stringWithFormat:@"_%@_%@_block", NSStringFromClass(object_getClass(self)), keyPath]];
    
    /*  改变setter方法 */
    NSString *format = [keyPath stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[keyPath substringToIndex:1] uppercaseString]];
    NSLog(@"format == %@", format);
    
    // NSString转SEL @selector
    SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", format]);

    Method setMethod = class_getInstanceMethod(object_getClass(self), setSel);
    
    // 交换NSKVONotifying_Person ---> setter方法的IMP,使其指向自定义setter方法的IMP
    // 也就是把系统的这个类的NSKVONotifying_Person setter方法IMP换成我们自己的IMP,这样系统的和自定义的KVO的setter方法全部指向了自定义的IMP
    // 优点是只改了 IMP,其他dealloc class _isKVOA方法完全没有动
    // 优点 addObserver removeObserver都是系统的方法,生命周期完全不用干涉
    // 缺点是 1.向父类发送setter消息没有改变,但是,对observer发送消息系统的也走了我们自定义的方法或者block回调
    // 如果addObserver系统的KVO那么,回调还是会走系统的,如果addObserver了自定义的KVO,那么都会走自定义的block,多少有点乱,一会这个回调一会那个回调
    // 系统的和自定义的method交换
    class_replaceMethod(object_getClass(self), setSel, (IMP)_setterFunction, method_getTypeEncoding(setMethod));
}

4.6自定义setter的IMP

void _setterFunction(id self, SEL _cmd, id newValue) {
    NSString *setterName = NSStringFromSelector(_cmd);
    if (setterName.length < 4) return;
    
    NSString *format = [setterName substringWithRange:NSMakeRange(3, setterName.length -4)];
    NSString *keyPath = [format stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[format substringToIndex:1] lowercaseString]];
    if (keyPath.length < 1) return;
    
    // 1.发送父类setter消息
    // 获取旧的值
    id oldValue = [self valueForKeyPath:keyPath];
    if (![oldValue isEqual:newValue]) {
        //调用父类setter
        struct objc_super supercls = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        void (* msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
        msgSendSuper(&supercls, _cmd, newValue);
    }
    
  //  发送观察者消息
    _EasyKVOChangedBlock block = (_EasyKVOChangedBlock)[_tipsMap(self, _cmd) objectForKey:[NSString stringWithFormat:@"_%@_%@_block", NSStringFromClass(object_getClass(self)), keyPath]];
    if (block) block(newValue, oldValue);
}

4.7移除观察者

- (void)removeObserver:(NSObject *)observer blockForKeyPath:(NSString *)keyPath {
    [self removeObserver:observer forKeyPath:keyPath];
    NSString *blockKeyName = [NSString stringWithFormat:@"_%@_%@_block", @"NSKVONotifying_", keyPath];
    NSMutableDictionary *tips = _tipsMap(self, _cmd);
    [tips removeObjectForKey:blockKeyName];
}

4.8观察者信息保存

NSMutableDictionary *_tipsMap(id self, SEL _cmd) {
    NSMutableDictionary * _tipsDic = objc_getAssociatedObject(self, &__EasyKVOTipsDic);
    if (!_tipsDic) {
        _tipsDic = [[NSMutableDictionary alloc] init];
        objc_setAssociatedObject(self, &__EasyKVOTipsDic, _tipsDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _tipsDic;
}

4.9总结,如果使用了这种自定义的KVO那么系统的KVO也会指向自定义的block回调,系统本身的回调不会执行,如果只使用系统的KVO则没有任何影响。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容