美文网首页
OC底层-KVO探索

OC底层-KVO探索

作者: KG丿夏沫 | 来源:发表于2021-06-16 09:24 被阅读0次

    在iOS开发中,KVO的使用频率是非常高的,可能是间接使用也可能是直接使用,今天主要通过以下几点进行探索。

    KVO初探

    1、 首先通过简单的使用KVO进行分析苹果提供的KVO中的API每个参数所代表的含义以及怎么使用能够让API达到最优使用。
    下面看下今天第一份代码:
    KGPerson.h代码如下:

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface KGPerson : NSObject
    
    /// 用户姓名
    @property(nonatomic,copy) NSString *name;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    KGPerson.m代码如下:

    #import "KGPerson.h"
    
    @implementation KGPerson
    
    @end
    

    ViewController.m中代码如下:

    #import "ViewController.h"
    #import "KGPerson.h"
    
    @interface ViewController ()
    
    @property (nonatomic,strong) KGPerson *person;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        _person = [[KGPerson alloc] init];
        [_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionPrior context:NULL];
        
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@==%@==%@",keyPath,object,change);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        _person.name = @"KG";
    }
    
    @end
    

    以上就是我们经常使用的KVO的时候的常规写法,那么接下来先看下- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context这个方法中参数的含义,observer是观察者对象,也就是消息接受者;keyPath是路径,也就是我们需要观察的属性或者成员变量;optionscontext通过KVO我们可以看到苹果对于参数的一些解释,那么我们通过代码去观察下这些属性,首先看下options,这是一个枚举,如下:

    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew = 0x01,//值为1,指示更改字典应提供新的属性值(如果适用)。
        NSKeyValueObservingOptionOld = 0x02,//值为2,指示更改字典应包含旧属性值(如果适用)。
        NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,//值为4,如果指定,则应在观察者注册方法返回之前立即向观察者发送通知。也就是用户主要注册了监听,在值还没有改变前就发送一次消息
        NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08,值为8,是否应该在每次更改前后向观察者发送单独的通知,而不是更改后的单个通知。
    };
    

    补充:
    1、枚举值如果是这种按照1<<x位表示,那么就代表可以进行多选

    2、以上枚举值options存在以下几种情况:

    (1)、options=1:用户选定NSKeyValueObservingOptionNew

    (2)、options=2:用户选定NSKeyValueObservingOptionOld

    (3)、options=3:用户选定NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld

    (4)、options=4:用户选定NSKeyValueObservingOptionInitial

    (5)、options=5:用户选定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionNew

    (6)、options=6:用户选定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionOld

    (7)、options=7:用户选定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionOldNSKeyValueObservingOptionNew

    (8)、options=8:用户选定NSKeyValueObservingOptionPrior

    (9)、options=9:用户选定NSKeyValueObservingOptionPriorNSKeyValueObservingOptionNew

    (10)、options=10:用户选定NSKeyValueObservingOptionPriorNSKeyValueObservingOptionOld

    (11)、options=11:用户选定NSKeyValueObservingOptionPriorNSKeyValueObservingOptionOldNSKeyValueObservingOptionNew

    (12)、options=12:用户选定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionPrior

    (13)、options=13:用户选定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionPriorNSKeyValueObservingOptionNew

    (14)、options=14:用户选定NSKeyValueObservingOptionInitialNSKeyValueObservingOptionPriorNSKeyValueObservingOptionNewNSKeyValueObservingOptionOld
    然后我们通过上述补充中的方案编写代码去看下具体效果:

    方案1:(需要点击屏幕触发touchesBegan)

    image.png

    方案2:(需要点击屏幕触发touchesBegan)

    image.png

    方案3:(需要点击屏幕触发touchesBegan)

    image.png

    方案4:(不需要点击屏幕)

    image.png

    方案5:(不需要点击屏幕)

    image.png

    方案6:(不需要点击屏幕)

    image.png

    方案7:(不需要点击屏幕)

    image.png

    方案8:(需要点击屏幕触发touchesBegan)

    image.png

    方案9:(需要点击屏幕触发touchesBegan)

    image.png

    方案10:(需要点击屏幕触发touchesBegan)

    image.png

    方案11:(需要点击屏幕触发touchesBegan)

    image.png

    方案12:(不点击屏幕,打印第一行,点击屏幕触发touchesBegan打印后两行)

    image.png

    方案13:(不点击屏幕,打印第一行,点击屏幕触发touchesBegan打印后两行)

    image.png

    方案14:(不点击屏幕,打印第一行,点击屏幕触发touchesBegan打印后两行)

    image.png

    以上就是所有options的方案结果,可以根据自己项目中场景去选择相应的方案。

    接下来看下context参数,这个参数在KVO中解释的很通彻,主要就是来区分不同的类有相同的属性监听,也就是相同用的keyPath情况下,简化判断条件用的,而且这种判断更安全。下面看下我们常规判断和使用context判断的实例:

    常规观察不同对象的相同属性代码书写:(我们经常写的时候context传的是nil,这种写法虽然运行没有错,但是从代码严谨程度来说是错的,因为苹果在文档中明确指出,这块如果不使用,传入NULL

    image.png

    使用context观察不同对象的相同属性代码书写:

    image.png

    补充:

    • nil:在OC中表示一个指针的值为空,经常用来创建一个空对象,表示指针不指向任何内存空间。

    • NULL:在C中表示一个指针的值为空,表示指针不指向任何内存空间。

    到这里基本上对于KVO的使用,我想已经信手拈来了,那么我们再进一步了解下KVO的主动调用和被动调用。最后记得移除监听,一般都是在dealloc中进行监听的移除,如下:

    image.png

    2、我们在开发中有的时候,比如说成员变量的KVO监听,我们直接修改值是监听不到的,那么这时候我们通常会手动调用willChangeValueForKeydidChangeValueForKey来触发KVO监听。代码如下:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [_person willChangeValueForKey:@"_nikeName"];
        [_person setValue:@"KG" forKey:@"_nikeName"];
        [_person didChangeValueForKey:@"_nikeName"];
    }
    

    另外对于属性的监听,我们如果需要手动去触发KVO的回调,那么那么应该先重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法。具体如下:

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
        if (key isEqualToString:@"name") {
            return NO;
        }
        return YES;
    }
    

    对于属性我们手动调用除了以上方法重写,还需要主动调用willChangeValueForKeydidChangeValueForKey,具体写法如下:

    - (void)setName:(NSString *)name{
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
    

    到此我们对于KVO的基本使用就完成了,那么接下来那就看下苹果是如何去实现KVO的,原理是什么?请听下回分析。

    KVO原理探索

    1、话不多说,先看以下动图,我们看图说话。

    yzsrs-65wku.gif

    从以上动画我们可以看到,当我们对一个对象添加KVO属性观察的时候,系统会修改我们对象的isa指针,指向一个运行时动态创建的类NSKVONotifying_KGPerson,那么会有同学问了,你咋知道,我不告诉你是苹果给我说的,苹果当时是这么说的:

    
    The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
    
    When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
    
    You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
    

    经过我的翻译是这么说的:

    自动键值观察是使用称为isa-swizzling的技术实现的。
    
    该isa指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据。
    
    当观察者为对象的属性注册时,被观察对象的 isa 指针被修改,指向中间类而不是真正的类。因此,isa 指针的值不一定反映实例的实际类。
    
    您永远不应该依赖isa指针来确定类成员资格。相反,您应该使用该class方法来确定对象实例的类。
    
    image.png

    2、实际上这个是在苹果官方文档KVO中有解析。说的很明确,就是通过isa-swizzling技术实现的,简单点来说就是在运行时,使用runtime动态创建一个类,然后将对象的isa指针指向进行修改,让isa指向动态创建的类,那么这个动态创建的类是继承于哪个类呢?我们一起修改下代码,然后运行打印,具体代码以及结果如图所示:

    image.png

    我们分别在添加属性监听前以及添加监听后打印KGPerson这个类的以及它的所有子类,我们可以通过打印输出看到,当添加监听后,系统会动态创建一个继承于KGPerson类的子类NSKVONotifying_KGPerson。然后我们修改下KGPerson这个类代码如下:

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface KGPerson : NSObject{
        @public
        NSString *_nikeName;
    }
    
    @property (nonatomic,copy) NSString *name;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    修改完成后,我们在使用LGPerson类的地方进行如图所示的修改以及属性监听,然后看下效果:

    image.png

    从上面打印结果可以看出,对于成员变量的监听没有效果,对于属性的监听是能够监听到。然后我对以上代码进行修改,再次运行看下效果:

    image.png

    对于成员变量的属性监听走了,那么对此我们可以得出以下结论:

    KVO的原理包含以下两点:

    • 动态生成子类:NSKVONotifying_XXX

    • 观察的是setter方法

    3、下面我们看下动态创建的这个继承于KGPerson的子类NSKVONotifying_KGPerson中系统做了哪些操作?下面先看以下代码,然后我们进行分析:

    image.png

    通过以上代码运行结果,我们可以分析出动态创建的子类做了以下几个操作:

    • 重写观察的属性的setter方法

    • 重写class方法,这个方法返回的还是KGPerson类。

    • 重写dealloc方法,在执行销毁方法后,会将isa指针指会到KGPerson,而且动态创建的类会进行缓存。

    • 实现了_isKVOA方法

    4、对于上面第三条结论,我们进行验证下,修改下代码然后运行,结果如下:

    image.png

    我们在dealloc方法中进行监听的移除,然后移除完成后走断点,打印输出可以看到po object_getClassName(self.person)对象的isa又指会到KGPerson了,然后我们在上一个界面打印下KGPerson的所有子类,代码以及结果如下:

    image.png

    5、从上图再次证明了,上面第三条结论,当动态创建子类后,系统会默认缓存子类。到此我们了解了系统KVO运行的一个基本原理,流程如下:

    11.png

    当我们了解了KVO的原理后,那么我们是否可以自定义实现KVO呢?下面一起研究自定义KVO

    自定义模拟KVO

    1、首先我们先对系统的KVO方法进行分析,通过查看KVOapi我们可以看到,系统是对NSObject类进行了扩展,也就是通过分类来实现的。那么我们也同样通过分类来进行,比较如果想要对全局所有的类都能进行属性监听,对NSObject进行分类扩展是最好的,因为所有的类都是继承于NSObject的,那么下面我们先创建一个NSObject的分类,命名为KGKVO。具体代码如下,代码中有详细的注释:

    NSObject+KGKVO.h

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSObject (KGKVO)
    
    - (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    - (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    NSObject+KGKVO.m

    #import "NSObject+KGKVO.h"
    #import <objc/message.h>
    
    // 动态创建子类时的类名前缀
    static NSString *const kKGKVOPrefix = @"KGKVONotifying_";
    // 获取消息观察者需要的关键字
    static NSString *const kKGKVOAssiociateKey = @"kKGKVO_AssiociateKey";
    
    @implementation NSObject (KGKVO)
    
    - (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
        // 1、验证setter方法是否存在
        [self judgeSetterMethodFormKeyPath:keyPath];
        
        // 2、动态生成子类
        Class newClass = [self createChildClassWithKeyPath:keyPath];
        
        // 3、isa指向新创建的子类,isa_swizzling
        object_setClass(self, newClass);
        
        // 4、保存观察者
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
        // 获取到当前子类的父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
    
    #pragma mark -- 验证是否存在setter方法
    - (void)judgeSetterMethodFormKeyPath:(NSString *)keyPath{
        // 获取父类
        Class supperClass = object_getClass(self);
        // 生成setter方法
        SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
        // 根据SEL获取方法
        Method setterMethod = class_getInstanceMethod(supperClass, setterSEL);
        // 判断是否存在方法
        if (!setterMethod) {
            // 如果方法不存在,抛出异常
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"对不起老铁,当前%@没有setter方法",keyPath] userInfo:nil];
        }
    }
    
    #pragma mark -- 动态生成子类
    - (Class)createChildClassWithKeyPath:(NSString *)keyPath{
        // 获取当前类的类名
        NSString *oldClassName = NSStringFromClass([self class]);
        // 生成当前类的子类类名
        NSString *newClassName = [NSString stringWithFormat:@"%@%@",kKGKVOPrefix,oldClassName];
        // 根据生成的子类类名获取类
        Class newClass = NSClassFromString(newClassName);
        // 判断是否已经存在子类
        if (!newClass) {
            // 如果不存在,先申请类,第一个参数是需要传入父类,第二个参数是需要传入类名,第三个参数是需要申请的内存空间大小
            newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
            // 注册类
            objc_registerClassPair(newClass);
            // 添加class方法
            SEL classSEL = NSSelectorFromString(@"class");
            // 根据SEL获取Method
            Method classMethod = class_getInstanceMethod([self class], @selector(class));
            // 获取方法参数以及方法返回类型
            const char *classType = method_getTypeEncoding(classMethod);
            // 给类添加class方法
            class_addMethod(newClass, classSEL, (IMP)kg_class, classType);
        }
        // 创建setter方法SEL
        SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
        // 根据SEL获取Method
        Method setterMethod = class_getInstanceMethod([self class], setterSEL);
        // 获取方法参数以及返回值类型
        const char *type = method_getTypeEncoding(setterMethod);
        // 添加方法
        class_addMethod(newClass, setterSEL, (IMP)kg_setter, type);
        // 返回子类
        return newClass;
    }
    
    #pragma mark --重写dealloc
    //static
    
    #pragma mark --创建setter方法
    static void kg_setter(id self,SEL _cmd,id newValue){
        // 需要进行消息转发,调用msgSendSuper()函数,所以需要创建结构体对象
        struct objc_super superStruct = {
                .receiver = self,
                .super_class = class_getSuperclass([self class])
        };
        // 进行强转
        void (*kg_objc_msgSendSuper)(void *,SEL ,id) = (void *)objc_msgSendSuper;
        // 进行消息转发
        kg_objc_msgSendSuper(&superStruct,_cmd,newValue);
        // 然后拿到消息观察者
        id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
        // 然后将消息发送给观察者
        SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
        // 获取getter方法
        NSString *keyPath = getterFormSetter(NSStringFromSelector(_cmd));
        // 消息转发
        ((void (*)(id, SEL, NSString *, id, NSDictionary *, void *))(void *)objc_msgSend)(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
    }
    
    #pragma mark -- 获取类的父类
    Class kg_class(id self,SEL _cmd){
        return class_getSuperclass(object_getClass(self));
    }
    
    #pragma mark -- 从getter方法获取setter方法 keyPath->setKeyPath
    static NSString *setterSelector(NSString *getter){
        // 判断keyPath是否合法
        if (getter.length <= 0) {
            return nil;
        }
        // 取第一个字符并且转换成大写
        NSString *firstString = [[getter substringToIndex:1] uppercaseString];
        // 获取除第一个字符外的其它字符
        NSString *leaveString = [getter substringFromIndex:1];
        // 返回setter方法
        return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
    }
    
    #pragma mark -- 从setter方法获取getter方法
    static NSString *getterFormSetter(NSString *setter){
        // 判断setter方法时候合法
        if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
            return nil;
        }
        // 获取keyPath
        NSRange range = NSMakeRange(3, setter.length-4);
        // 获取getter方法名
        NSString *getter = [setter substringWithRange:range];
        // 获取第一个字符,并转换成小写
        NSString *firstString = [[getter substringToIndex:1] lowercaseString];
        // 返回getter方法
        return [setter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
    }
    
    @end
    

    2、然后在调用的地方如下图所示使用:

    image.png

    3、到这里我们仿照系统的KVO实现原理自定义实现了KVO,但是也只是仿照系统的实现对属性的观察,没有达到和系统KVO一样的完善,所以我们继续去完善这个自定义的KVO。目前存在的问题:

    • 如果说多个对象进行属性值观察,那么我们通过objc_setAssociatedObject来保存的observer就会出现错乱。

    • 如果一个对象同时需要观察多个属性,那么意味着我们要多次调用objc_setAssociatedObject这个函数,那么就会出现重复使用同一个关键值和关联策略去关联不同的值给对象,那么很容易造成
      内存泄露。

    4、那么接下来我们针对以上的问题进行完善自定义KVO,请看下回分析。

    自定义模拟KVO+完善

    1、首先我们针对多个属性进行观察,首先能够想到的就是通过数组去保存一些信息,然后从数组中取值然后判断做一系列的操作,那么那些信息我们怎么归类呢?在iOS中信息归类,我们无非就是结构体、对象、字典等等,但是在此处我们使用对象,定义如下对象:

    @interface KGInfo : NSObject
    
    /// 观察者对象
    @property (nonatomic,strong) NSObject *observer;
    /// 属性
    @property (nonatomic,copy) NSString *keyPath;
    /// 观察键值条件
    @property (nonatomic,assign) NSKeyValueObservingOptions options;
    /// 辨别标识符
    @property (nonatomic,assign) void * context;
    
    /// 初始化方法
    /// @param observer 观察者对象
    /// @param keyPath 属性
    /// @param options 观察键值条件
    /// @param context 辨别标识符
    - (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    @end
    
    @implementation KGInfo
    
    /// 初始化方法
    /// @param observer 观察者对象
    /// @param keyPath 属性
    /// @param options 观察键值条件
    /// @param context 辨别标识符
    - (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
        self = [super init];
        if (self) {
            self.observer   = observer;
            self.keyPath    = keyPath;
            self.options    = options;
            self.context    = context;
        }
        return self;
    }
    
    @end
    

    3、然后在之前的基础上做相应的修改:

    - (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
        // 1、验证setter方法是否存在
        [self judgeSetterMethodFormKeyPath:keyPath];
        
        // 2、动态生成子类
        Class newClass = [self createChildClassWithKeyPath:keyPath];
        
        // 3、isa指向新创建的子类,isa_swizzling
        object_setClass(self, newClass);
        
        // 4、先进行判断是否已经存在对象属性观察表了
        NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey));
        
        // 5、判断arr是否存在,如果不存在进行创建,类似懒加载
        if (!arr) {
            arr = [NSMutableArray array];
        }
        
        // 6、创建信息对象
        KGInfo *info = [[KGInfo alloc] initWithObserver:observer forKeyPath:keyPath options:options context:context];
        
        // 7、将对象信息添加到数组
        [arr addObject:info];
        
        // 7、保存观察属性信息
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
        NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
        if (arr.count <= 0) {
            return;
        }
        for (KGInfo *info in arr) {
            if ([info.keyPath isEqualToString:keyPath]) {
                [arr removeObject:info];
                objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                break;
            }
        }
        if (observer.copy <= 0) {
            // 获取到当前子类的父类
            Class superClass = [self class];
            object_setClass(self, superClass);
        }
    }
    
    - (Class)createChildClassWithKeyPath:(NSString *)keyPath{
        // 获取当前类的类名
        NSString *oldClassName = NSStringFromClass([self class]);
        // 生成当前类的子类类名
        NSString *newClassName = [NSString stringWithFormat:@"%@%@",kKGKVOPrefix,oldClassName];
        // 根据生成的子类类名获取类
        Class newClass = NSClassFromString(newClassName);;
        if (!newClass) {
            // 如果不存在,先申请类,第一个参数是需要传入父类,第二个参数是需要传入类名,第三个参数是需要申请的内存空间大小
            newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
            // 注册类
            objc_registerClassPair(newClass);
            // 添加class方法
            SEL classSEL = NSSelectorFromString(@"class");
            // 根据SEL获取Method
            Method classMethod = class_getInstanceMethod([self class], @selector(class));
            // 获取方法参数以及方法返回类型
            const char *classType = method_getTypeEncoding(classMethod);
            // 给类添加class方法
            class_addMethod(newClass, classSEL, (IMP)kg_class, classType);
        }
        // 创建setter方法SEL
        SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
        // 根据SEL获取Method
        Method setterMethod = class_getInstanceMethod([self class], setterSEL);
        // 获取方法参数以及返回值类型
        const char *setterTypes = method_getTypeEncoding(setterMethod);
        // 添加方法
        class_addMethod(newClass, setterSEL, (IMP)kg_setter, setterTypes);
        // 返回子类
        return newClass;
    }
    
    #pragma mark --创建setter方法
    static void kg_setter(id self,SEL _cmd,id newValue){
        // 获取getter方法
        NSString *keyPath = getterFormSetter(NSStringFromSelector(_cmd));
        // 获取旧值
        id oldValue = [self valueForKey:keyPath];
        // 需要进行消息转发,调用msgSendSuper()函数,所以需要创建结构体对象
        struct objc_super superStruct = {
                .receiver = self,
                .super_class = class_getSuperclass([self class])
        };
        // 进行消息转发
        ((void (*)(struct objc_super *,SEL,id))(void *)objc_msgSendSuper)(&superStruct,_cmd,newValue);
        
        // 然后拿到观察信息数组
        NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
        // 判断数组是否有值
        if (arr.count > 0) {
            // 循环拿出数组中的KGInfo对象
            for (KGInfo *info in arr) {
                // 判断是否是需要的info
                if ([info.keyPath isEqualToString:keyPath]) {
                    dispatch_async(dispatch_get_global_queue(0, 0), ^{
                        NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionary];
                        // 对新旧值进行处理
                        if (info.options & NSKeyValueObservingOptionNew) {
                            [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                        }
                        if (info.options & NSKeyValueObservingOptionOld) {
                            if (oldValue) {
                                [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                            }else{
                                [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                            }
                        }
                        // 然后将消息发送给观察者
                        SEL observerSEL = @selector(kg_observeValueForKeyPath:ofObject:change:context:);
                        // 消息转发
                        ((void (*)(id, SEL, NSString *, id, id, void *))(void *)objc_msgSend)(info.observer,observerSEL,keyPath,self,change,NULL);
                    });
                }
            }
        }
    }
    

    4、到此的话一个基础的KVO属性观察完成了,但是还是存在一些瑕疵,我们需要添加属性观察,然后跑到- (void)kg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context去判断监听返回的是哪个属性的监听等等一系列操作,那么到此处我们是否还能优化呢?当然是可以的,我们自然而然就想到了Block,函数式编程能够让我们的代码更加简洁,所以请看下回分析。

    自定义模拟KVO+函数式编程思想

    1、针对之前的代码比较繁琐,我们再次做一下优化,使用函数式编程的思想,去打破系统KVO繁琐的步骤,经过优化后代码如下:

    image.png image.png

    2、那么使用的时候就更加简单了,如下:

    image.png

    3、从书写上我们就可以看到,在哪添加的观察,就在那实现回调,逻辑来说更加清晰,而且去掉了繁琐的observerkeyPath的判断,让代码更加简洁。

    4、既然我们对自定义KVO都简化到这个程度了,那么我们是否还能再简化,直接实现自动销毁呢?不需要手动去销毁呢?因为经常会忘记手动remove监听,答案是肯定可以的,那么请看下一节分析。

    补充:

    • 在这里有一个细节性的东西,就是对observer的修饰符,为什么不用strong而是使用weak的原因在于,防止循环引用,因为如果不用weak去打断闭环,那么就是处于VC持有->person(持有)->arr(持有)->info(持有)->VC的情况。

    自定义模拟KVO+自动销毁

    1、我们发现每次需要添加属性观察都需要去手动移除,而且很多时候因为粗心大意,很容易忘记移除,那么程序运行就会立马报错奔溃,所以为了防止这种情况,我们需要去考量能否让它进行自动销毁,当对象释放的时候,自动移除属性观察呢?此时我们想到了对系统的方法dealloc进行method_swizzled,但是我们应该在什么时机下去做这个操作呢?目前来说我们经常用的是在load方法中,还有当前添加监听的时候,也就是addObserver的时候,但是选择哪个呢?首先我们去判断如果在addObserver的时候做方法Hook的话,怎么去判断是否已经Hook过了?所以我们决定在load方法中进行hook,但是因为NSObject是所有类的基类,这样每个类的load方法走的时候都会进行Hook,会造成混乱,到时候我们自己也不知道是否Hook成功,所以我们在整个app启动后,在load方法中只进行一次Hook,而且之后不会再进行Hook,所以我们想到了使用单利时的做法,通过dispatch_once,然这个操作只执行一次。

    2、当我们理清思路后,然后回头去修改代码,就变的很简单了,NSObject+KGKVO.h的优化如下:

    image.png

    直接去掉了监听移除的一系列处理,直接在Hook后的myDealloc方法中进行移除,简单而且严谨。

    3、当我们使用的时候只需要去添加属性监听,不需要去做额外的处理,也不需要去检查是否进行监听的移除了,只需要一句简简单单的代码调用,就完成了整个复杂的KVO监听,使用如下:

    image.png

    一行代码,搞定通过KVO监听属性值变化,而且也没有其他的配置选项,自动返回旧值、新值等等。

    总结

    到此对于KVO的探索基本完成了,但是里面还有很多细节性的东西需要自己去优化,比如:

    • 监听不同类型的属性,目前的代码中kg_setter方法返回值以及参数不一致会导致奔溃问题

    • 对于成员变量的监听实现等等

    如果对此感兴趣的同学可以一起探讨,或者去分析下FBKVOController

    相关文章

      网友评论

          本文标题:OC底层-KVO探索

          本文链接:https://www.haomeiwen.com/subject/fuwzeltx.html