iOS 设计模式(五)-KVO 详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 设计模式(五)-KVO 详解相关的知识,希望对你有一定的参考价值。

参考技术A KVO 的全称是 Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发 KVO 的监听方法来通知观察者。KVO 是在 MVC 应用程序中的各层之间进行通信的一种特别有用的技术。
KVO 和 NSNotification 都是 ios 中观察者模式的一种实现。
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet 。

先创建一个类,作为要监听的对象。

监听实现

KVO 主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用 KVO 实现最为合适。斯坦福大学的 iOS 教程中有一个很经典的案例,通过 KVO 在 Model 和 Controller 之间进行通信。如图所示:

KVO 触发分为自动触发和手动触发两种方式。

如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发 KVO:

如果是监听集合对象的改变,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO。集合对象包含 NSArray 和 NSSet 。

普通对象属性或是成员变量使用:

NSArray 对象使用:

NSSet 对象使用:

observationInfo 属性是 NSKeyValueObserving.h 文件中系统通过分类给 NSObject 添加的属性,所以所有继承于 NSObject 的对象都含有该属性;
可以通过 observationInfo 属性查看被观察对象的全部观察信息,包括 observer 、 keyPath 、 options 、 context 等。

注册方法 addObserver:forKeyPath:options:context: 中的 context 可以传入任意数据,并且可以在监听方法中接收到这个数据。

context 作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。
KVO 只有一个监听回调方法 observeValueForKeyPath:ofObject:change:context: ,我们通常情况下可以在注册方法中指定 context 为 NULL ,并在监听方法中通过 object 和 keyPath 来判断触发 KVO 的来源。

但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对象的 name 属性进行观察。问题:
当 name 发生改变时,应该由谁来处理呢?
如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?
这时候通过使用 context 就可以很好地解决这个问题,在注册方法中为 context 设置一个独一无二的值,然后在监听方法中对 context 值进行检验即可。

苹果的推荐用法:用 context 来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为 context 的值。可以为整个类设置一个 context ,然后在监听方法中通过 object 和 keyPath 来确定被观察属性,这样存在继承的情况就可以通过 context 来判断;也可以为每个被观察对象属性设置不同的 context ,这样使用 context 就可以精确的确定被观察对象属性。

context 优点:嵌套少、性能高、更安全、扩展性强。
context 注意点:

KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet 。(注意:如果直接对集合对象进行操作改变,不会触发 KVO。)

可以在被观察对象的类中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 方法来控制 KVO 的自动触发。

如果我们只允许外界观察 person 的 name 属性,可以在 Person 类如下操作。这样外界就只能观察 name 属性,即使外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发 KVO。

也可以实现遵循命名规则为 + (BOOL)automaticallyNotifiesObserversOf<Key> 的方法来单一控制属性的 KVO 自动触发,<Key> 为属性名(首字母大写)。

使用场景:
使用 KVO 监听成员变量值的改变;
在某些需要控制监听过程的场景下。比如:为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。

由于 KVO 的本质,重写 setter 方法来达到可以通知所有观察者对象的目的,所以只有通过 setter 方法或 KVC 方法去修改属性变量值的时候,才会触发 KVO,直接修改成员变量不会触发 KVO。
当我们要使用 KVO 监听成员变量值改变的时候,可以通过在为成员变量赋值的前后手动调用 willChangeValueForKey: 和 didChangeValueForKey: 两个方法来手动触发 KVO,如:

NSKeyValueObservingOptionPrior (分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在 willChangeValueForKey: 和 didChangeValueForKey: 的时候进行的。
如果注册方法中 options 传入 NSKeyValueObservingOptionPrior ,那么可以通过只调用 willChangeValueForKey: 来触发改变前的那次 KVO,可以用于在属性值即将更改前做一些操作。

有时候我们可能会有这样的需求,KVO 监听的属性值修改前后相等的时候,不触发 KVO 的监听方法,可以结合 KVO 的自动触发控制和手动触发来实现。

例如:对 person 对象的 name 属性注册了 KVO 监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发 KVO,可以在 Person 类中如下这样实现,将 name 属性值改变的 KVO 触发方式由自动触发改为手动触发。

有些情况下我们想手动观察集合属性,下面以观察数组为例。
关键方法:

需要注意的是,根据 KVC 的 NSMutableArray 搜索模式:

有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
比如我们想要对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。以下有两种方法可以解决这个问题。

以上两个方法可以同时存在,且都会调用,但是最终结果会以 keyPathsForValuesAffectingValueForKey: 为准。

以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你希望 Department 类有一个 totalSalary 属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题。

有时候我们难以避免多次注册和移除相同的 KVO,或者移除了一个未注册的观察者,从而产生可能会导致 Crash 的风险。
三种解决方案: 黑科技防止多次添加删除KVO出现的问题

我们在对象添加监听之前分别打印对象类型

我们看到,添加监听后,使用 object_getClass 方法获取model类型时获取到的是 NSKVONotifying_DJModel 。

这里就产生了几个问题:

从打印结果可以看出, NSKVONotifying_DJModel 是 DJModel 的子类,说明我们添加了监听之后动态创建了一个 DJModel 的子类 NSKVONotifying_DJModel ,并将对象 DJModel 的类型更改为了 NSKVONotifying_DJModel 。

我们从源码看出,实例对象调用 class 方法会返回 isa 指针,类对象调用 class 方法会返回自己,通过 object_getClass 方法获取对象的类型也会返回 isa 指针。从源码上看model对象添加监听之后使用 class 和使用 object_getClass 方法获取到的类型应该是一样的,但是这里却不同,我们猜测在添加了监听之后在 NSKVONotifying_DJModel 中重写了 class 方法。
我们打印一下添加监听前后 class 方法的 IMP 地址来确认是否重写了 class 方法。

从打印结果可以看出,添加监听之后 class 方法的地址改变了,这验证了我们之前的猜想, NSKVONotifying_DJModel 类中重写了 class 方法。

我们监听对象时调用了 set 方法,我们对监听前后的 set 方法单独分析。
我们再添加监听前后分别打印 setName 方法的 IMP 地址。

通过打印结果可以看出 setName 方法也在 NSKVONotifying_DJModel 中被重写了,我们再使用lldb来看下 setName 具体是什么

第一个地址打印的是添加监听前 setName 方法的 IMP 地址,第二个打印的是添加监听后 setName 方法的 IMP 地址。
这里看出添加监听前 setName 对应的具体方法就是 setName ,但是添加监听后, setName 对应的鸡头方法却变成了 _NSSetObjectValueAndNotify 函数。
下面我们就来研究一下 _NSSetObjectValueAndNotify 函数。

从上面与KVO相关的方法中我们可以看出,每一种数据类型都对应了一个 setXXXValueAndNotify 函数。
不过这些函数的具体实现没有公布,所以内部构造这里还是不清楚。
但是我们知道,在调用 `setXXXValueAndNotify 函数的过程中会调用另外两个方法。

测试后得出了以下几个结论:

我们还可以利用这两个方法手动触发 observeValueForKeyPath 方法:

所以我们判断在 _NSSetObjectValueAndNotify 函数内部,在调用原来的 set 方法之前插入了 willChangeValueForKey 方法,在调用原来的 set 方法之后插入了 didChangeValueForKey 方法,并根据初始化时的枚举值决定调用 observeValueForKeyPath 的时机。

(1)添加监听时,会动态创建一个监听对象类型的子类,并将监听对象的 isa 指针指向新的子类。
(2)子类中重写了 class 和监听属性的 set 方法。
(3)重写 class 方法是为了不将动态创建的类型暴露出来。
(4)重写 set 方法是将 set 方法的具体实现替换成了与属性类型相关的 __NSSetXXXValueAndNotify 函数。
(5)在 __NSSetXXXValueAndNotify 函数内部在 set 方法前后分别插入了 willChangeValueForKey 和 didChangeValueForKey 这两个方法。
(6)根据添加监听时的枚举值决定调用 observeValueForKeyPath 的具体时机。

ios开发runtime学习五:KVC以及KVO,利用runtime实现字典转模型

一:KVC和KVO的学习

#import "StatusItem.h"
/*
 1:总结:KVC赋值:1:setValuesForKeysWithDictionary实现原理:遍历字典,得到所有的key,value值,再利用kvc, setVaue forkey来为value赋值 2: [item setValue:@"来自即刻笔记" forKey:@"source"],内部的底层实现,
 1.首先去模型中查找有没有setSource,找到,直接调用赋值 [self setSource:@"来自即刻笔记"]
 2.去模型中查找有没有source属性,有,直接访问属性赋值  source = value
 3.去模型中查找有没有_source属性,有,直接访问属性赋值 _source = value
 4.找不到,就会直接报错 setValue:forUndefinedKey:报找不到的错误
  当系统找不到就会调用这个方法,报错- (void)setValue:(id)value forUndefinedKey:(NSString *)key可以重写此方法更改key值
 3:KVC四个方法:利用kvc可以访问成员变量和属性,setValue,value为属性值,forKey,key为属性名,forKeyPath为键值路径,例如在model中有如下属性定义:
    @property (nonatomic, strong) BankAccount *account;
    keyPath:
    [zhangSan setValue:@150 forKeyPath:@"account.balance"];
 
 - (id)valueForKey:(NSString *)key;
 - (id)valueForKeyPath:(NSString *)keyPath;
 - (void)setValue:(id)value forKey:(NSString *)key;
 - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
 
 4:KVO:键值观察机制,用于对属性的value值的改变做监听:用法:
 @interface BankAccount : NSObject
 @property (nonatomic, assign) NSInteger balance;
 @end
 
 @interface Person : NSObject
 @property (nonatomic, strong) BankAccount *account;
 @end
 
 @implementation Person
 - (instancetype)init {
 ...
 // 注册Observer:
 [self.account addObserver:self
 forKeyPath:@"balance"
 options:NSKeyValueObservingOptionNew |
 NSKeyValueObservingOptionOld
 context:nil];
 ...
 }
 
 - (void)dealloc {
 // 不要忘了removeObserver
 [self.account removeObserver:self forKeyPath:@"balance"];
 }
 
 // 属性变化的回调方法:
 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 if ([keyPath isEqualToString:@"balance"]) {
 NSLog(@"Balance was %@.", change[NSKeyValueChangeOldKey]);
 NSLog(@"Balance is %@ now.", change[NSKeyValueChangeNewKey]);
 }
 }
 @end
 
 - (void)testKVO {
 Person *zhangSan = [[Person alloc] initWithName:@"ZhangSan" andBalance:20];
 // 无论是用点语法还是KVC的方法都会触发回调:
 zhangSan.account.balance = 150;
 [zhangSan setValue:@250 forKeyPath:@"account.balance"];
 }

 
 */
@interface StatusItem ()
@property (nonatomic,copy)NSString *hello;
@end
@implementation StatusItem

// 模型只保存最重要的数据,导致模型的属性和字典不能一一对应

+ (instancetype)itemWithDict:(NSDictionary *)dict
{
    StatusItem *item = [[self alloc] init];
    
    // KVC:把字典中所有值给模型的属性赋值
    [item setValuesForKeysWithDictionary:dict];
    
    // 拿到每一个模型属性,去字典中取出对应的值,给模型赋值
    // 从字典中取值,不一定要全部取出来
    // MJExtension:字典转模型 runtime:可以把一个模型中所有属性遍历出来
    // MJExtension:封装了很多层
//    item.pic_urls = dict[@"pic_urls"];
//    item.created_at = dict[@"created_at"];
    
    // KVC原理:
    // 1.遍历字典中所有key,去模型中查找有没有对应的属性
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
        
        // 2.去模型中查找有没有对应属性 KVC
        // key:source value:来自即刻笔记
        // [item setValue:@"来自即刻笔记" forKey:@"source"]
        [item setValue:value forKey:key];
        
        
    }];
    
    return item;
}


// 重写系统方法? 1.想给系统方法添加额外功能 2.不想要系统方法实现
// 系统找不到就会调用这个方法,报错
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    
}



@end

二:利用runtime实现字典转模型

#import "ViewController.h"
#import "NSDictionary+Property.h"
#import "StatusItem.h"
#import "NSObject+Model.h"
@interface ViewController ()

@end
/*
 总结:1:项目中的文件都保存在mainBundle里,读取项目中的本地信息:[[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; 得到本地路径path,再看项目中的文件根节点是字典还是数组,再从中读取本地路径filer:dictionaryWithContentsOfFile读取 。若是获取网络端的路径:dictionaryWithContentsOfUrl
 */
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    
    // 获取文件全路径
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    
    // 文件全路径
    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    
    // 设计模型,创建属性代码 => dict
//    [dict[@"user"] createPropertyCode];
    
    // 字典转模型:KVC,MJExtension
    StatusItem *item = [StatusItem modelWithDict:dict];
    
    
}

@end
#import <Foundation/Foundation.h>
// 字典转模型
@interface NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict;

@end
#import "NSObject+Model.h"
#import <objc/message.h>
/*
 
总结:MJExtension字典转模型的底层核心实现:runtime实现字典转模型。
 
 1:因为模型model都继承NSObject,所以可以给系统类写分类进行拓展,子类继承NSObject,也就是继承了扩展的方法。所以模型转字典的方法考虑给NSObject写一个分类进行方法的拓展
 2:在分类中若是对象方法,self指的是调用该方法的对象,类方法中self指的是调用该方法的类。方法的设计:类方法简单粗暴,直接用类去调用,字典转模型方法获得所转换的模型,不用创建对象,并且会将调用方法的类作为参数传进方法中(对象方法也如此)
 
 3:原理:runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
 1:先获取模型中所有成员变量 key
 参数意义:
 // 获取哪个类的成员变量
 // count:成员变量个数 int *类型
 unsigned int count = 0;
 // 获取成员变量数组
 Ivar *ivarList = class_copyIvarList(self, &count);
 
 注意:1:int *count ,此count的类型为int *类型,当作为参数的时候,需要传入一个int *类型的指针,指针里存放的都是内存地址,也就是将地址作为参数传递,当方法执行完毕后,系统会拿到*count 进行赋值
 int a = 2;
 int b = 3;
 int c = 4;
 int arr[] = {a,b,c};
 int *p = arr;
 p[0];
 NSLog(@"%d %d",p[0],p[1]);
 
     2: 获取类里面属性
        class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
 
        获取类里面所有方法
        class_copyMethodList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)// 本质:创建谁的对象
 
     3:获取模型中所有成员变量 key,Ivar:成员变量 以下划线开头,相当于一个数组
 // 获取哪个类的成员变量
 // count:成员变量个数
 unsigned int count = 0;
 // 获取成员变量数组
 Ivar *ivarList = class_copyIvarList(self, &count);
 
 4:具体实现:获取成员变量名字:ivar_getName(ivar),属于c语言字符串,所以要进行UTF8编码  获取成员变量类型:ivar_getTypeEncoding(ivar)
 // 获取成员变量名字
 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
 // 获取成员变量类型
 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
 
 注意:1:字符串替换:stringByReplacingOccurrencesOfString  2:字符串截取:substringFromIndex:包括该index   substringToIndex:不包括该index 3:前后缀:hasPrefix前缀,hasSuffix:后缀 4:是否包含某个字符串:containString 5:字符串转换为Class:NSClassFromString: // 获取类
 Class modelClass = NSClassFromString(ivarType);
 
 value = [modelClass modelWithDict:value];

  6:一般某个方法接受传递进来的参数的时候,要判断参数是否为空,为nil或是为空值,给某个值赋值的时候,也要判断该值是否存在:
 // 给模型中属性赋值
 if (value) {
 [objc setValue:value forKey:key];
 }


 
 */




@implementation NSObject (Model)


// Ivar:成员变量 以下划线开头
// Property:属性
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    id objc = [[self alloc] init];
    
    // runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
    // 1.获取模型中所有成员变量 key
    // 获取哪个类的成员变量
    // count:成员变量个数
    unsigned int count = 0;
    // 获取成员变量数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 获取成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        // 获取key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 去字典中查找对应value
        // key:user  value:NSDictionary
        
        id value = dict[key];
        
        // 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
        // 并且是自定义对象才需要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            // 字典转换成模型 userDict => User模型
            // 转换成哪个模型

            // 获取类
            Class modelClass = NSClassFromString(ivarType);
            
            value = [modelClass modelWithDict:value];
        }
        
        // 给模型中属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
        
    return objc;
}


void test(int *count){
    *count = 3;
}

@end

 

以上是关于iOS 设计模式(五)-KVO 详解的主要内容,如果未能解决你的问题,请参考以下文章

iOS------通知代理kvo 详解

iOS中的 观察者模式 之 KVO

[深入浅出Cocoa]详解键值观察(KVO)及其实现机理

ios设计模式—kvo观察者模式

iOS底层探索之KVO—KVO简介

iOS KVO详解