【iOS】KVC与KVO

文章目录

KVC设值流程

当我们调用setValue:forKey:设置属性时,底层执行流程如下:

  • 【第一步】先查找是否有三种setter方法,顺序为set<Key>: -> _set<Key> -> setIs<Key>
    • 如果有任意一个setter方法,则直接设置属性的value
    • 如果都没有的话,直接进入第二步
  • 【第二步】如果第一步中的setter方法都没有,就查找accessInstanceVariablesDirectly是否返回YES
    • 如果返回yes,则查找间接访问的时机变量进行赋值,查找顺序为_<key> -> _is<Key> -> <key> -> is<Key>
      • 如果找到其中任意一个实例变量,则赋值
      • 如果都没有就进入第三步
    • 如果返回NO,直接进入第三步
  • 【第三步】如果setter方法或者实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

KVC取值流程

  • 【第一步】首先查找 getter 方法,按照如下顺序查找get<Key>,<key>,is<Key>,_get<Key>

    • 如果找到,则调用该方法取得返回值,并进入【第五步】。
    • 如果没有找到,则进入【第二步】。
  • 【第二步】如果第一步中的 getter 方法没有找到,KVC 会查找数组访问方法,按照以下规则判断是否支持 NSArray 风格的集合访问:countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes:

    • 如果找到了 countOf<Key>,并且找到了 objectIn<Key>AtIndex:<key>AtIndexes: 其中之一,则 KVC 会创建一个 NSArray 风格的集合代理对象。该代理对象会将接收到的 NSArray 相关消息,转换为对原对象的:countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes:,get<Key>:range:

    等方法的调用。

    • 其中 get<Key>:range: 是可选的批量访问方法,如果对象实现了它,代理对象会在合适的时候使用它。

    • 如果没有满足数组集合访问方法的要求,则进入【第三步】。

  • 【第三步】如果没有找到数组访问方法,KVC 会继续查找 Set 集合访问方法:countOf<Key>,enumeratorOf<Key>,memberOf<Key>:

    • 如果这三个方法都找到了,则 KVC 会返回一个 NSSet 风格的集合代理对象。该代理对象会将接收到的 NSSet 相关消息,转换为对原对象的:countOf<Key>,enumeratorOf<Key>,memberOf<Key>:

    等方法的调用。

    • 如果仍然没有找到,则进入【第四步】。
  • 【第四步】如果以上方法都没有找到,KVC 会检查类方法:

    复制代码
    + accessInstanceVariablesDirectly
    • 如果该方法返回 YES,则 KVC 会按照以下顺序直接查找实例变量:_<key>,_is<Key>,<key>,is<Key>

    • 如果找到对应实例变量,则直接读取该实例变量的值,并进入【第五步】。

    • 如果 + accessInstanceVariablesDirectly 返回 NO,或者没有找到对应实例变量,则进入【第六步】。

  • 【第五步】根据取得的值的类型,返回不同结果:

    • 如果取得的是对象指针,则直接返回该对象。

    • 如果取得的是 NSNumber 支持的标量类型,例如int,float,double,BOOL,NSInteger,NSUInteger则 KVC 会将其包装成 NSNumber 对象返回。

    • 如果取得的是结构体等非对象类型,例如:CGRect,CGPoint,CGSize,NSRange则 KVC 会将其包装成 NSValue 对象返回。

  • 【第六步】如果以上步骤全部失败,KVC 会调用:

    复制代码
    valueForUndefinedKey:

    默认实现会抛出:

    复制代码
    NSUndefinedKeyException

    异常。

KVC使用场景

  1. 动态设值和取值
  • 可以通过setValue:forKey:valueForKey:
  • 也可以通过路由的方式setValue:forKeyPath:valueForKeyPath:
  1. 通过KVC反问和修改私有变量
  • 日常开发中对于类的私有属性,外部定义的对象是没办法直接访问的,但是对于KVC而言,可以访问和修改任何私有属性
  1. 多值操作(model和字典互转化)

具体实现如下

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

NS_ASSUME_NONNULL_BEGIN

@interface BaseModel : NSObject
// 字典 → 模型
- (instancetype)initWithDictionary:(NSDictionary *)dict;
+ (instancetype)modelWithDictionary:(NSDictionary *)dict;

// 模型 → 字典
- (NSDictionary *)toDictionary;

// key 映射(可选)
+ (NSDictionary *)modelCustomPropertyMapper;

// 数组元素类型(可选)
+ (NSDictionary *)modelContainerPropertyGenericClass;


@end

NS_ASSUME_NONNULL_END

//实现文件

#import "BaseModel.h"
#import <objc/runtime.h>

@implementation BaseModel

#pragma mark - 初始化

- (instancetype)initWithDictionary:(NSDictionary *)dict {
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

+ (instancetype)modelWithDictionary:(NSDictionary *)dict {
    return [[self alloc] initWithDictionary:dict];
}

#pragma mark - KVC 核心

- (void)setValue:(id)value forKey:(NSString *)key {

    if (!key) return;

    // 1. key 映射
    NSDictionary *mapper = [[self class] modelCustomPropertyMapper];//获取映射表
    NSString *propertyKey = mapper[key] ?: key;//看看映射表中有没有对应的映射

    objc_property_t property = class_getProperty([self class], propertyKey.UTF8String);//判断类中是否真的有这个属性
    if (!property) return;

    const char *attrs = property_getAttributes(property);//获取属性的编码字符串
    NSString *attrStr = [NSString stringWithUTF8String:attrs];//将C字符串转换为NSString

    // 2. 对象类型处理
    if ([attrStr hasPrefix:@"T@"]) {
        // 获取类名 @"ClassName"
        NSRange start = [attrStr rangeOfString:@"@\""];
        if (start.location != NSNotFound) {//属性是OC类型
            NSRange end = [attrStr rangeOfString:@"\"" options:0 range:NSMakeRange(start.location+2, attrStr.length-start.location-2)];
            NSString *clsName = [attrStr substringWithRange:NSMakeRange(start.location+2, end.location-start.location-2)];
            Class cls = NSClassFromString(clsName);
            // 3. 嵌套模型
            if ([value isKindOfClass:[NSDictionary class]] && [cls isSubclassOfClass:[BaseModel class]]) {
                value = [[cls alloc] initWithDictionary:value];
            }
        }

        // 4. 数组模型
        if ([value isKindOfClass:[NSArray class]]) {
            NSDictionary *generic = [[self class] modelContainerPropertyGenericClass];
            Class itemCls = generic[propertyKey];

            if (itemCls) {
                NSMutableArray *arr = [NSMutableArray array];
                for (id obj in value) {
                    if ([obj isKindOfClass:[NSDictionary class]]) {
                        id model = [[itemCls alloc] initWithDictionary:obj];
                        [arr addObject:model];
                    } else {
                        [arr addObject:obj];
                    }
                }
                value = arr;
            }
        }
    }

    [super setValue:value forKey:propertyKey];
}

#pragma mark - 容错

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    // 忽略未知 key(避免 crash)
}

- (void)setNilValueForKey:(NSString *)key {
    // 防止基本类型为 nil crash
}

#pragma mark - 模型 → 字典

- (NSDictionary *)toDictionary {
    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList([self class], &count);

    NSMutableDictionary *dict = [NSMutableDictionary dictionary];

    for (int i = 0; i < count; i++) {
        const char *name = property_getName(properties[i]);
        NSString *key = [NSString stringWithUTF8String:name];

        id value = [self valueForKey:key];
        if (!value) continue;

        // 嵌套模型
        if ([value isKindOfClass:[BaseModel class]]) {
            value = [value toDictionary];
        }

        // 数组模型
        if ([value isKindOfClass:[NSArray class]]) {
            NSMutableArray *arr = [NSMutableArray array];
            for (id obj in value) {
                if ([obj isKindOfClass:[BaseModel class]]) {
                    [arr addObject:[obj toDictionary]];
                } else {
                    [arr addObject:obj];
                }
            }
            value = arr;
        }

        dict[key] = value;
    }

    free(properties);
    return dict;
}

#pragma mark - 默认实现

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{};
}

+ (NSDictionary *)modelContainerPropertyGenericClass {
    return @{};
}

@end

如果我们想使用这个方法,模型就需要继承自BaseModel,并且重写对应的\+ (NSDictionary *)modelCustomPropertyMappermodelContainerPropertyGenericClass

使用:

objc 复制代码
UserModel* model = [UserModel modelWithDictionary:dict];
  1. 修改一些系统空间的内部属性

在日常开发中,我们知道很多UI控件的内部都是由多个UI控件组合而成,这些内部控件是没有提供访问的API的,这里就可以使用KVC。

  1. 使用KVC实现高阶消息传递

普通情况,我们只能对单个对象发送消息,但是KVC可以允许我们对整个数组整体调用,会将消息发送给数组中的每一个元素

KVO

前言

  • KVC是键值编码,在对象创建完成之后可以动态的给对象属性赋值。
  • KVO是键值观察,提供监听机制,当指定对象的属性被修改之后,则对象会收到通知。

KVO与NSNotification的区别

  • 相同点
    • 都是观察者模式,用于监听
    • 都能实现1对多
  • 不同点
    • KVO只能用于监听对象属性的变化,并且属性名都是通过NSString来查找,编译器不会检查输入是否错误
    • NSNotification的发送通知操作我们可以控制,KVO是由系统控制
    • KVO可以记录新旧值变化

基本使用

  • 注册观察者addObserver:forKeyPath:options:context
  • 实现KVO回调observeValueForKeyPath:ofObject:change:context
  • 移除观察者removeObserver:forKeyPath:context

对于context参数,可以用来区分不同对象的同名属性,从而在KVO的回调中可以直接使用context进行区分,可以大幅度提高性能,以及代码的可读性。

objc 复制代码
static void *ZYLPersonNickNameContext = &ZYLPersonNickNameContext;

- (void)runKVODemo {
    self.kvoPerson = [ZYLPerson new];
    self.kvoPerson.nickName = @"oldName";

    [self.kvoPerson addObserver:self
                     forKeyPath:@"nickName"
                        options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                        context:ZYLPersonNickNameContext];

    self.kvoPerson.nickName = @"newName";
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if (context == ZYLPersonNickNameContext) {
        NSLog(@"KVO keyPath: %@", keyPath);
        NSLog(@"old: %@", change[NSKeyValueChangeOldKey]);
        NSLog(@"new: %@", change[NSKeyValueChangeNewKey]);
        return;
    }

    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

即使移除观察者

移除观察者前必须确定当前对象是观察者,否则会抛出NSRangeException。

观察者的注册和移除需要成对出现,避免重复注册之类的问题。如果旧的观察者没有释放,那么就会出现重复通知的问题。但是如果旧的观察者释放了的话,KVO系统中还保留着一条旧的观察关系,但是这个旧的观察关系指向了一个可能已经释放的观察者,出出现类似于野指针的崩溃。

自动触发与手动触发

  • 自动开关
objc 复制代码
+ (BOOL)automicallyNotifiesObserversForKey:(NSString*)key {
	return YES;返回
}
  • 手动开关:当自动开关关闭的时候,可以通过手动开关监听
objc 复制代码
- (void)setNickName:(NSString *)nickName {
  [self willChangeValueForKey:@"nickName"];
  _nickName = nickName;
  [self didChangeValueForKey:@"nickName"];
}

KVO实现一对多

示例如下:

objc 复制代码
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) CGFloat currentData;
@property (nonatomic, assign) CGFloat totalData;

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {//一个系统方法,设置依赖
  NSSet* keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
  if ([key isEqualToString:@"currentProcess"]) {
    NSArray* affectingKeys = @[@"totalData", @"currentData"];
    keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
  }
  return keyPaths;
}

- (CGFloat)currentProcess {
  if (self.totalData <= 0) {
    return 0;
  }
  return self.currentData / self.totalData;
}

- (void)runKVODemo2 {
  self.kvoPerson = [[ZYLPerson alloc] init];
  self.kvoPerson.totalData = 100;
  self.kvoPerson.currentData = 0;
  [self.kvoPerson addObserver:self
                   forKeyPath:@"currentProcess"
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:ZYLPersonCurrentProcessContext];
}

KVO观察可变数组

在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组

objc 复制代码
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"0"];

KVO底层探索

  • KVO是使用isa-swizzling技术实现的
  • isa指向了一个类,内部维护者一张表,这个表中有着类的方法是实现以及一些其他数据(bits->data)即class_rw_t
  • 当对象的属性被注册为观察者之后,对象的isa就会被修改指向一个中间类。(所以尽量不要使用isa找类,而是使用class方法)

验证

我们打上断点验证如下:

可以发现两次输出的self.kvoPerson属性所属的类是不同的,self.kvoPerson的isa指针指向了一个中间类。

中间类是啥

可以发现中间类的命名规范是:NSKVONotifying_实例的类名

我们先猜测中间类是原类的子类,因为KVO并未影响对象的setter方法,只是添加了监听,所以猜测是子类重写了setter方法,在其中添加了监听。

通过添加allChildClassAndSelf:方法,可以发现KVO再给对象属性注册观察者之后,对象的isa指向了一个中间类,并且这个中间类是对象本身的类的子类。

我们通过调用 allMethodsForClass:方法可以发现,KVO重写的是属性的set方法对于没有set方法的实例变量是不会观察的。

另外,我们可以发现中间类还重写了class方法,这是为了确保后续对象调用class方法还是会得到对象原有的类。

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

objc 复制代码
- (void)dealloc {
  @try {
    [self.kvoPerson removeObserver:self forKeyPath:@"currentProcess" context:ZYLPersonCurrentProcessContext];
    [self allChildClassAndSelf:[ZYLPerson class]];
  } @catch (__unused NSException *exception) {
  }

  @try {
    [self.kvoPerson removeObserver:self forKeyPath:@"nickName" context:ZYLPersonNickNameContext];
  } @catch (__unused NSException *exception) {
  }

}

我们在移除观察者之后再次调用allChildClassAndSelf:方法,发现中间类一旦生成,没有移除、没有销毁,还在内存中。这是基于虚重用的想法。

总结

  • 实例对象isa的指向在注册KVO观察者之后,由原有类更改为指向中间类
  • 中间类重写了观察属性的setter方法classdealloc_isKVOA方法
  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类
  • 中间类从创建后,就一直存在内存中,不会被销毁

自定义KVO

具体代码这里就不粘贴了,大概讲解一下流程:

  • 注册观察者&响应
    • 验证是否存在sette方法
    • 保存信息
    • 动态生成子类,重写class、setter方法
    • 在子类的setter方法中向父类发送消息
    • 让观察者响应
  • 移除观察者
    • 更改isa指向原有类
    • 重写子类的dealloc方法
相关推荐
YJlio8 小时前
Windows Internals 10.5.3:ETW 架构详解,从事件产生到性能分析的完整链路
windows·笔记·python·stm32·嵌入式硬件·学习·架构
SkyXZ~8 小时前
Mac上使用VScode优雅开发STM32
vscode·stm32·macos
在学了加油8 小时前
DenseNet121学习笔记
笔记·学习
智者知已应修善业8 小时前
【用一片74LS139和一片74Ls00,设计带高电平有效使能输入端的3线-8线译码器】2023-10-16
驱动开发·经验分享·笔记·硬件架构·硬件工程
Brilliantwxx8 小时前
【C++】初步认识STL(3)
开发语言·c++·笔记·算法
90后的晨仔8 小时前
《swiftUI进阶 第10章:现代状态管理(iOS 17+)》
ios
浩浩的科研笔记8 小时前
一篇教人如何写综述的顶刊论文—Literature review as a research methodology: An overview and guidelines 逐句精度
笔记
是上好佳佳佳呀10 小时前
【前端(十)】CSS 过渡与动画笔记
前端·css·笔记
叶小鸡17 小时前
Java 篇-项目实战-苍穹外卖-笔记汇总
java·开发语言·笔记