iOS底层探索 -- KVO探索

iOS底层探索 -- KVO探索

IOS小彩虹2021-08-20 5:21:06250A+A-

前言

  上一篇学习了KVC键值编码的查找原理,而KVO(Key-Value Observing)在开发中也是用的比较多。本篇我们深入底层探索一下KVO的底层原理。

1. KVO初探

首先,先看一下,平常我们是怎么写KVO进行键值观察的

如在某个类中,有一个 LGPerson 类型的属性person,在这个类中对personname属性进行观察,代码如下:


[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

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

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

这些都是我很熟悉的,但是要注意的是:添加的观察者,一定要及时移除,否则,当对象释放后,会造成野指针等问题。

接下来看一下KVO的一些细节问题。

1.1 context 的作用

查看添加观察者API

addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context

前面三个参数,我们都很熟悉,那么最后一个void *类型的上下文context,有什么用呢?

在平常的开发中,我们习惯的给传NULL,那么我们思考一个问题,

假如在一个类中,要对多个对象的同名属性进行观察,

比如:LGStudent继承自LGPerson,而我们要在同一个类中对这个两个属性的name进行观察,我们会怎么做呢?

//  添加观察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

//  监听变化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LGViewController - %@",change);
    if (object == self.person) {
        if ([keyPath isEqualToString:@"name"]) {
            // 逻辑
        }
    } else if(object == self.student){
        if ([keyPath isEqualToString:@"name"]) {
            // 逻辑
        }
    }
    
}

//  移除
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.student removeObserver:self forKeyPath:@"name"];
}

这时我们就需要在监听变化的方法中,写很多判断条件,然后来处理逻辑,这样会很繁琐。

通过查看文档,其实我们可以在添加观察者的时候,为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析,这是一个更加安全更加便利的方式

static void *PersonAccountBalanceContext      = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;




- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

1.2 自动观察

在开发中,我们也会经常遇到一种情况,比如:需求频繁改动,导致我们对某个属性的观察频繁的删除,然后重新,很是繁琐。

其实,我们可以在被观察的类中(比如上面对self.personname观察,可以写在LGPerson中),写下面的代码,

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}

这个方法默认为YES,我们可以设置为NO,此时添加的观察者会失效,在被观察属性发生变化是,需要手动通过两个方法(willChangeValueForKey:didChangeValueForKey:)进行观察

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

// 对 person 的 name 进行观察
...

// 当 name 发生改变时
[self.person willChangeValueForKey:@"name"];
self.person.name  = @"null";
[self.person didChangeValueForKey:@"name"];

我们还可以通过下面的方式,对某个Key判断,进而设置自动还是手动,设置为NO的将不会被观察。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

也可以在被观察属性的setter方法中,调用这两个方法,进行手动观察

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

1.3 多个因素影响

在开发中,也会遇到进度条的应用场景,而当前进度的占比,是受两个因素控制(当前下载量和总下载量)的,

比如下面的示例:

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end

#import "LGPerson.h"

@implementation LGPerson

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

@end

[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

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

    NSLog(@"LGViewController - %@",change);
    
}

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

在添加观察者时,不光有正常的三部,还需要添加一个keyPathsForValuesAffectingValueForKey方法。

1.4 可变数组的观察

在对可变数组进行观察时,对数组进行修改时,不能通过调用addObject方法添加元素,应该通过下面的方式。

    // 数组变化不能通过这种方式
    // [self.person.dateArray addObject:@"1"];
    // KVO 建立在 KVC
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];

因为KVO 是基于KVC的,而KVO的观察是通过setter,可变数组的的获取是通过mutableArrayValueForKey方法,不是像其他的setValeu:forKey方法。

2. KVO 原理分析

首先,我们定义一个LGPersonLGPerson中定义一个nickName的属性。 然后对其观察。

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;


- (void)sayHello;
- (void)sayLove;

@end

#import "LGPerson.h"

@implementation LGPerson
- (void)setNickName:(NSString *)nickName{
    _nickName = nickName;
}


- (void)sayHello{
    
}
- (void)sayLove{
    
}

@end

在添加观察者前后断点调试,

分别打印 self.person的类,如下:

发现在添加之后,self.person的类发生了变化,变成了NSKVONotifying_LGPerson

查看官方文档,KVO底层,是对ias进行了swizzling。使对象的isa由原来的类指向了派生出来的NSKVONotifying_xxx

Automatic key-value observing is implemented using a technique called isa-swizzling.

那么LGPersonNSKVONotifying_LGPerson是什么关系呢?

我们通过RunTime API打印LGPerson父类的所有子类。

- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

在添加观察前后,打印结果如下:

由此可见,NSKVONotifying_LGPerson同样继承自LGPerson的元类。

我们知道,对一般类型的属性进行观察时,是观察的这个属性的setter,当添加观察者时,会动态创建一个NSKVONotifying_xxx的类,那么这个类中是否会对元类中的方法进行重写呢?

打印方法列表如下:

从打印结果可以看出,动态创建的类中,重写了setNickName:classdelloc_isKVOA

然后在观察者销毁时,将isa指向原来的类,在delloc方法中,打印self.person类:

那么,动态生成的NSKVONotifying_xxx是否释放了呢?

答案是否定的,因为动态子类,创建成本太高,销毁了会很浪费,而不销毁方便下次快捷的使用

小结:

 1: 动态生成子类 : NSKVONotifying_xxx
 2: 观察的是 setter
 3: 动态子类重写了很多方法 setNickName (setter) class dealloc _isKVOA
 4: 移除观察的时候 isa 指向回来
 5: 动态子类不会销毁(创建成本太高,不释放,方便下次使用)

3. 自定义 KVO 思路

系统的KVONSObject的一个分类NSObject(NSKeyValueObserving),凡是继承自NSObject的类,都可以使用KVO

那么接下来,尝试自定义一个简单的KVO

首先,可以自定义一点添加观察者的方法,在这个方法中

1. 动态创建 NSKVONotifying_xxx 类。为了防止错误,我们可以先检测被观察的 keyPath 是否有 setter 
2. 交换 isa 的指向,指向 NSKVONotifying_xxx
- (void)ll_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的指向 : LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    //  4: 保存信息(保存信息,方便拿到观察者)
    LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

动态创建子类

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];
    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 : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
    //  2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
    
    //  2.3.3 : 添加dealloc
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);
    
    return newClass;
}

static void lg_dealloc(id self,SEL _cmd){
    
}

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    //  5: 信息数据回调
    //  拿到观察者
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 对新旧值进行处理
                if (info.options & LGKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & LGKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                //  2: 消息发送给观察者
                SEL observerSEL = @selector(lg_observeValueForKeyPath:ofObject:change:context:);
                objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
            });
        }
    }
}

Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

自定义移除观察者这方法

- (void)ll_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 * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    //  将指针指回
    if (observerArr.count<=0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

自定义监听方法

- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{
    
}

4. KVO 函数式编程

在自定义添加KVO时,我们可以加入函数式编程的思想,在添加观察者的时候,定义一个回调block,直接在被观察属性发生变化时,调用block,将其传回。这样就不用在写监听方法了。

对上面的lg_setter修改,将发送消息,改成回调block

- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    // 4: 保存信息
    LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 5: 信息数据回调
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            // 回调block
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

// 添加观察者
[self.person ll_addObserver:self forKeyPath:@"nickName" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        NSLog(@"%@-%@",oldValue,newValue);
    }];

其实,还可以将观察者的指针指向原来的类的操作放到手动添加的delloc的实现中,这样就可以自动释放,不需要再调用移除方法

static void lg_dealloc(id self,SEL _cmd){
    Class superClass = [self class];
    object_setClass(self, superClass);
}
小结
1. 验证是否存在 setter ,不让实例变量尽量
2. 动态生成子类
    2.1 动态开辟一个新类 (NSKVONotifying_xxx)
    2.2 注册类
    2.3 添加 class 方法,setter 方法,dealloc 方法
    2.4 使用关联对象,保存观察者
3. 修改 ISA 是指向,指向动态生成的类
4. 在重写的 setter 中
    4.1 消息转发给父类,调用父类的 setter ,给一种什么也没干的假象
    4.2 通过发送消息,调用监听方法(observeValueForKeyPath:)
        或者通过响应式编程,回调block

5. FBKVO 简单分析

上面我们通过自定义KVOKVO底层原理有了一个系统的了解,其中肯定存在问题,而在一些开源网站上,有很多大牛自己封装的KVO

接下来,简单的了解一下FBKVO

FBKVO封装了一个FBKVOController的中间层,添加了函数式编程的思想,可以通过下面的形式调用。

[self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(lg_observerAge)];
[self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
}];
[self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
}];

FBKVOController中,统一添加观察者,统一处理,统一销毁,如下图,解决了循环引用的问题

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示 | 支付宝红包
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们