三十二、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
相关推荐
HarderCoder1 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
叽哥11 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户091 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan1 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918412 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa