三十二、KVO(原理及自定义)

本文由快学吧个人写作,以任何形式转载请表明原文出处。

一、资料准备

KVO官方文档

二、KVO的原理

1. 文档理解原理

文档有实现细节的阐述 :

翻译 :

KVO的自动观测是通过isa-swizzling技术完成的。

isa指向了一个类,这个类维护着一张表,这个表里面有这个类的方法的实现,和一些其他的数据。说白了,就是指类的结构中的bits->data(),也就是class_rw_t呗。

当为对象的属性注册了观察者之后,对象的isa指针就会被修改,指向一个中间类,而不是对象所属的真正类。所以isa指针的指向不一定就能反映对象的真实的类。

永远不要用isa指针来确定对象的类,而是利用class方法来确定对象的类。

总结 :

不要用isa的指向来判断对象的类,要用class方法。KVO在给对象的属性添加观察者的时候,对象的isa会指向一个中间类,而不是原来的类。

2. 验证原理

创建一个JDMan类,有一个属性jdName,创建两个VC,一个叫VC,一个叫JDVC,VC push 就会到JDVC。

并在JDVC中添加属性man,类型是JDMan。对man对这个对象的jdName属性进行观察。在添加观察者前,查看man的类,在添加观察者后,再查看man的类。

JDMan :

JDVC中打上断点,引入runtime头文件 :

运行,查看添加观察者之前和之后,self.man的isa是否发生了变化,指向了某个中间类。

验证了文档所说的,在给对象的属性添加了观察者之后,对象的isa就会指向一个中间类。

3. 中间类是什么

中间类的命名规范是 : NSKVONotifying_ + 实例的类名

  1. 为什么要生成中间类?
  2. 中间的类和原来的类的关系?
  3. 对象的isa是否一直指向中间类,或者什么时候对象的isa指回原来的类?
  4. 中间类在不被使用了之后,是否会销毁?

先看中间类和原来的类的关系。猜测是子类,因为KVO并未影响对象的setter方法,仅仅是监听。所以子类重写setter,在setter中添加监听的可能性最大。

JDVC中加入

scss 复制代码
- (void)allChildClassAndSelf:(Class)cls
{
    // 获取注册到内存的类的总数量,用来遍历所有的类
    // 参数1传NULL是获取所有已注册的类。参数2传0也是为了这个。
    int count = objc_getClassList(NULL, 0);
    // 获取所有已经注册的类,先申请类列表的空间,大小是count的大小
    Class *classes = (Class *)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    // 为了打印结果里清晰的显示父类是谁,先把父类加入到可变数组中,这样可变数组的第一个就肯定是父类了
    NSMutableArray *mutArr = [NSMutableArray arrayWithObject:cls];
    for (int i = 0; i < count; i++) {
        // 如果注册到内存的类的父类是cls,就添加到可变数组中
        if (cls == class_getSuperclass(classes[i])) {
            [mutArr addObject:classes[i]];
        }
    }
    // malloc的classes要自己释放内存
    free(classes);
    NSLog(@"cls自己和子类 : %@",mutArr);
}

JDVC代码 :

运行,结果如下 :

由此可以得出结论 :

证明了KVO在给对象的属性注册观察者之后,对象的isa指向了一个中间类,并且这个中间类是对象本身的类的子类。

那对于为什么要生成中间类,就有了新的猜测,中间类是不是就是对修改关闭和对扩展开放的应用?并且修改中间类,比修改类本身更方便,也更安全。那么就要看中间类的方法列表中都有什么方法。

JDVC中加入 :

ini 复制代码
- (void)allMethodsForClass:(Class)cls
{
    // 存储类的所有的方法的数量
    unsigned int count = 0;
    Method *mList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = mList[i];
        SEL mSEL = method_getName(method);
        IMP mIMP = class_getMethodImplementation(cls, mSEL);
        NSLog(@"类的所有方法 : %@--%p",NSStringFromSelector(mSEL),mIMP);
    }
    free(mList);
}

JDVC代码 :

运行,结果 :

看到重写了属性的set方法,由此可以得出,KVO观察的是属性的set方法,对没有set方法的实例变量是不会观察的。

为什么重写class方法?如下图 :

在添加观察者之后,对象调用class方法还是会得到对象的类是原有的类,而不是中间类,为了不影响外部的判断。

什么时候isa指回原来的类? 如下图 :

在移除观察者之后,让对象的isa指回原来的类。

中间类会在移除观察者之后就销毁吗?如下图 :

在VC中打印所有JDMan自己和所有的子类,对比push到JDVC和pop出JDVD前后,JDMan子类的变化。push到JDVC会创建观察者,pop出JDVC会注销观察者。

VC代码 :

JDVC代码 :

在VC没有push到JDVC前,也就是没有生成中间类之前 :

JDVC销毁,pop回到VC,已经添加过观察者,并且移除了。也就是生成过中间类了 :

由此可以得出结论 :

  1. KVO观察的是属性,因为中间类重写的是被观察属性的set方法,实例变量不自动生成set方法。
  2. 重写class是为了在class中让self.man调用class的时候不要受到影响,如果不重写,self.mam在被添加观察者之后,[self.man class]就会显示中间类。这也是为什么官方文档中说不要用isa去判断对象的类,而是用class。这里的重写class,就会让[self.man class]显示的是JDMan。
  3. 在remove(移除)观察者之后,被观察的对象的isa会指向原来的类。
  4. _isKVOA只是一个标识。
  5. 中间类在移除观察者之后不会被销毁。

三、自定义KVO

根据探索的原理,自定义一个KVO

首先确定的是KVO是基于KVC的。并且通过addObserver方法的源码也可以看到,KVO也是在NSObject的分类中实现的。所以创建一个NSObject分类。

提前说明,这只是为了真正的理解KVO的一个思想。所以代码并不完美,甚至冗余并且有很多缺陷,但是没有特大bug,拿来玩是可以的,不要真的用到项目中去。

.h文件

objectivec 复制代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (JDKVO)

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

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

- (void)jd_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end

NS_ASSUME_NONNULL_END

.m文件

scss 复制代码
#import "NSObject+JDKVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

// 拼接中间类的名字用,前缀
static NSString *const JDKVOPrefix = @"JDKVONotifying_";
// 做关联对象的key,这个key对应的value是一个可变数组,可变数组存储的是观察者和被观察的属性
static NSString *const JDKVOAssociateKey = @"JDKVOAssiciateKey";

// 保存观察者和一些信息
@interface JDInfo : NSObject

@property (nonatomic, weak) NSObject *observer;

@property (nonatomic, copy) NSString *keyPath;

@property (nonatomic, assign) NSKeyValueObservingOptions options;

- (instancetype)initWithObserver:(NSObject *)observer KeyPath:(NSString *)keyPath Options:(NSKeyValueObservingOptions)options;

@end

@implementation JDInfo

- (instancetype)initWithObserver:(NSObject *)observer KeyPath:(NSString *)keyPath Options:(NSKeyValueObservingOptions)options
{
    if (self = [super init]) {
        self.observer = observer;
        self.keyPath = keyPath;
        self.options = options;
    }
    return self;
}

@end

@implementation NSObject (JDKVO)

// 通过keyPath获取被观察属性的setter方法,c语言的写法
static NSString *cHuoQuSetter(NSString *keyPath)
{
    if (keyPath.length <= 0) return nil;
    // 主要就是将keyPath首字母大写,然后拼接一个set字符串,做法很多
    
    // 取keyPath字符串的首字母大写
    NSString *fStr = [[keyPath substringToIndex:1] uppercaseString];
    // 取keyPath除了首字母以外的所有字符串
    NSString *eStr = [keyPath substringFromIndex:1];
    
    // 拼接返回
    return [NSString stringWithFormat:@"set%@%@:",fStr,eStr];
}

// 通过keyPath获取被观察属性的setter方法,oc写法。
- (NSString *)ocHuoQuSetter:(NSString *)keyPath
{
    if(keyPath.length <= 0) return nil;
    
    // 把首字母大写,然后再换掉自己(首字母)的小写
    NSString *fBigStr = [keyPath stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[keyPath substringToIndex:1].capitalizedString];
    
    return [NSString stringWithFormat:@"set%@:",fBigStr];
}

// 判断是否有setter方法
- (BOOL)haveSetterMethod:(NSString *)keyPath
{
    // 获取类,因为self现在是某个对象,所以要拿到类
    Class class = object_getClass(self);
    // 获取setter方法
    NSString *selStr = [self ocHuoQuSetter:keyPath];
    SEL sSEl = NSSelectorFromString(selStr);
    Method sMethod = class_getInstanceMethod(class, sSEl);

    return sMethod ? YES : NO;
}

// 从setter方法中取出属性名
static NSString *getPropertyNameFromSetter(NSString *setterName)
{
    // 长相不对,返回nil。不是正经的setter方法的格式
    if (setterName.length <= 0 || ![setterName hasPrefix:@"set"] || ![setterName hasSuffix:@":"]) {
        return nil;
    }
    
    NSString *propertyName = [setterName substringWithRange:NSMakeRange(3, setterName.length-4)];
    NSString *pFinalName = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[propertyName substringToIndex:1].lowercaseString];
    return pFinalName;
}

// 中间类对keyPath的setter
// setter要做的事情 :
// 1. 给属性赋值。self现在是JDMan的对象,也就是外面调用的对象,所以要调用父类(中间类)的父类(JDMan)set方法。
// 2. 包装+回调给外部
// 包装 : 根据option 将newValue oldValue kind封装成change
// 回调给外部 : 让外部要响应observeValueForKeyPath方法
static void jd_setter(id self,SEL _cmd,id newValue)
{
    // 先要把原始的值拿出来保存一下,不然下面马上就更改属性的值,变成新值了,就不能取到旧有的值了
    // 拿到KeyPath
    NSString *keyPath = getPropertyNameFromSetter(NSStringFromSelector(_cmd));
    // 通过KVC拿到属性原有的值
    id oldValue = [self valueForKey:keyPath];
    
    // 1. 调用中间类的父类的set方法(SEL就是_cmd,是一样的,set的参数就是newValue)
    // 这就是定义一个jd_msgSendSuper函数,参数分别是指针(传个真地址进来),SEL,id。
    // 把objc_msgSendSuper这个函数直接赋值给自建的函数jd_msgSendSuper
    // objc_msgSendSuper在<objc/message.h>里面声明的,记得import这个头文件
    // struct objc_super sStruct就是objc_msgSendSuper的第一个参数,也就是jd_msgSendSuper的第一个参数
    void (*jd_msgSendSuper)(void *,SEL,id) = (void *)objc_msgSendSuper;
    struct objc_super sStruct =
    {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    jd_msgSendSuper(&sStruct,_cmd,newValue);
    
    // 2. 包装 + 回调给外部(既然是包装,给它定义一个类更好)
    // 取出关联对象中存储着observer和keyPath的数组
    NSMutableArray *mutArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey));
    
    // 循环一下拿出keyPath(属性)对应的info
    for (JDInfo *jdInfo in mutArr) {
        if ([jdInfo.keyPath isEqualToString:keyPath]) {
            
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                
                // 存值到change
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                if (jdInfo.options & NSKeyValueObservingOptionNew) {
                    [change setValue:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (jdInfo.options & NSKeyValueObservingOptionOld) {
                    if (oldValue) {
                        [change setValue:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                    else
                    {
                        [change setValue:@"" forKey:NSKeyValueChangeOldKey];
                    }
                }
                
                // 发送消息给回调函数,该干活了
                SEL obSEL = @selector(jd_observeValueForKeyPath:ofObject:change:context:);
                // Build Setting-->Enable Strict Checking of objc_msgSend Calls改为NO,不然报错。
                objc_msgSend(jdInfo.observer,obSEL,keyPath,self,change,NULL);
            });
        }
    }
}

// 重写中间类的class方法,因为要在对象调用class的时候返回的是原来的类,而只有注册观察者之后
// 对象调用的class才会找到这里,所以class中返回的应该是self的类的父类,也就是中间类的父类才是正确的
static Class jd_class(id self,SEL _cmd)
{
    return class_getSuperclass(object_getClass(self));
}

// 生成或者拿到中间类,因为要直接给中间类添加setter方法,所以要把keypath这个属性传进来
- (Class)getMediumClass:(NSString *)keyPath
{
    // 获取中间类
    NSString *oldClassStr = NSStringFromClass([self class]);
    NSString *newClassStr = [NSString stringWithFormat:@"%@%@",JDKVOPrefix,oldClassStr];
    Class newClass = NSClassFromString(newClassStr);
    
    // 如果没有生成过中间类,则创建并注册到内存
    if (!newClass) {
        
        // 创建新的类,先申请内存空间
        // 参数1是父类,2是新的类的名字,3是额外需要的存储空间
        newClass = objc_allocateClassPair([self class], newClassStr.UTF8String, 0);
        
        // 因为操作的主要是方法,方法在rw中就可以获取,所以没有必要先添加方法后注册
        // 直接先注册类,免得忘了
        objc_registerClassPair(newClass);
        
        // 给中间类添加要重写的class方法
        SEL classSEL = NSSelectorFromString(@"class");
        Method classM = class_getInstanceMethod([self class], classSEL);
        const char *classMT = method_getTypeEncoding(classM);
        class_addMethod(newClass, classSEL, (IMP)jd_class, classMT);
    }
    
    // 给中间类添加keypath的setter方法并重写
    // setter方法不要放在if里面,因为无论类是否存在,添加观察者的时候,观察的属性可能不一样,那么setter方法就不一样。
    // 反正是用class_addMethod添加,返回的是bool值,可以判断下,是否能成功添加。不能就代表已经添加过这个属性的setter方法了

    // 拿到keyPath的setter方法的sel
    SEL setterSel = NSSelectorFromString(cHuoQuSetter(keyPath));
    // 拿到keypath的setter方法的方法签名,因为只是重写,所以和原来的类的setter方法的签名一样
    Method setterM = class_getInstanceMethod([self class], setterSel);
    const char *setterMT = method_getTypeEncoding(setterM);
    // 添加setter方法到中间类,IMP本身要带有两个参数id self 和 SEL _cmd,而且IMP本身就是个func,所以用c写一个就行
    BOOL isAdd = class_addMethod(newClass, setterSel, (IMP)jd_setter, setterMT);
    
    if (isAdd) {
        NSLog(@"中间类没有设置过该属性的setter方法");
    }
    else
    {
        NSLog(@"中间类已经添加过该属性的setter方法了");
    }
    
    return newClass;
}

//自定义添加观察者
- (void)jd_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    
    // 1. 有setter方法的才可以进来(如果是_实例变量名,这里就会有问题了,暂时没考虑这个情况)
    if (![self haveSetterMethod:keyPath]) return;
    
    // 2. 如果已经添加了观察了,就不要再添加了
    NSMutableArray *mutArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey));
    if (mutArr && mutArr.count > 0) {
        for (JDInfo *hInfo in mutArr) {
            if ([hInfo.keyPath isEqualToString:keyPath]) {
                return;
            }
        }
    }
    // 3. 保存观察者及keyPath
    JDInfo *jdInfo = [[JDInfo alloc] initWithObserver:observer KeyPath:keyPath Options:options];
    // 如果从关联表中取不到可变数组再创建,有了就不要创建了,直接放jdInfo进去。
    if (!mutArr) {
        mutArr = [NSMutableArray arrayWithCapacity:1];
        // 还没有加到过当前对象的关联对象中
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey), mutArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mutArr addObject:jdInfo];
    
    // 4. 生成或者拿到中间类
    Class mediumClass = [self getMediumClass:keyPath];
    
    // 5. 更改对象的isa指向
    object_setClass(self, mediumClass);
}

// 移除观察者
- (void)jd_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    // 移除关联对象中的已经添加的JDInfo,否则如果移除之后,再添加同一个对象同一个属性,并且是self还未被销毁过的情况下,
    // 关联对象因self没被销毁过,也就不会被销毁,那么就会出现多次添加同一个对象同一个属性进入到数组中,
    // 那么在jd_setter方法中,通知回调方法的时候,遍历对比JDInfo.keyPath,就会发现两个相同的keyPath
    // 就会执行回调方法两次,添加同一对象同一属性越多,则执行回调的次数越多,严重的bug
    
    NSMutableArray *mutArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey));
    
    if (mutArr.count <= 0) {
        return;
    }
    
    for (JDInfo *jdInfo in mutArr) {
        if ([jdInfo.keyPath isEqualToString:keyPath]) {
            [mutArr removeObject:jdInfo];
        }
    }
    
    // 确定没有属性在被观察了,再改变isa指向,指回原来的类
    if (mutArr.count <= 0) {
        Class fatherClass = class_getSuperclass(object_getClass(self));
        object_setClass(self, fatherClass);
    }
}

@end
相关推荐
m0_7482389236 分钟前
webgis入门实战案例——智慧校园
开发语言·ios·swift
Legendary_0085 小时前
LDR6020在iPad一体式键盘的创新应用
ios·计算机外设·ipad
/**书香门第*/14 小时前
Laya ios接入goole广告,搭建环境 1
ios
wakangda20 小时前
React Native 集成 iOS 原生功能
react native·ios·cocoa
几维安全1 天前
如何保护你的 iOS 应用免受逆向工程攻击
macos·objective-c·cocoa
crasowas2 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store
ii_best2 天前
ios按键精灵脚本开发:ios悬浮窗命令
ios
Code&Ocean2 天前
iOS从Matter的设备认证证书中获取VID和PID
ios·matter·chip
/**书香门第*/2 天前
Laya ios接入goole广告,开始接入 2
ios
恋猫de小郭3 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui