##一、KVO 初探
添加观察者
KVO 就是观察者,观察一个值的变化,通过以下代码来理解
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.student = [LGStudent shareInstance];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
代码里面的使用场景,可以根据自己的理解完善场景。
以上就是一个对象添加KVO的语言,很简单,需要注意的是 context 参数不填时,要写 NULL,不要写 nil,因为context 的类型是 void *,根据上面代码可以看到有两个对象,观察的都是同一个 keyPath ,在 observeValueForKeyPath 里要怎么区分呢?
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
}
这时 context 就起作用了,我们可以将 context 理解为tag,我们可以为每个keyPath都设一个key,以便快速定位观察。
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nick"];
}
移除观察者和不移除对比
- 添加移除移除动作
LGViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.student = [LGStudent shareInstance];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"null";
self.student.name = @"森海北语";
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nick"];
}
LGDetailViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
self.student = [LGStudent shareInstance];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.student.name = @"hello word";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGDetailViewController :%@",change);
}
- (void)dealloc{
[self.student removeObserver:self forKeyPath:@"name"];
}
打印:
从 LGViewController 可以进入 LGDetailViewController
LGViewController 界面里对name 的值做了两次改动,所以第一部分都是在 LGViewController 里面打印的,LGPerson 为 LGStudent 的父类;
然后进入 LGDetailViewController 之后对 name 添加了观察者并它做了值改变,所以第二部分在 LGDetailViewController 和LGViewController里都打印了 hello word;
第三部分是返回上一页到 LGViewController 此时就跟第一部分打印的一样,因为 LGViewController 中的 dealloc 并没有及时执行。
LGViewController -> LGDetailViewController -> LGViewController
- 没有添加移除动作
其他代码一样,只用将 LGDetailViewController 中的移除观察的方法注释掉,然后运行时按照上面操作再来一次
- (void)dealloc{
// [self.student removeObserver:self forKeyPath:@"name"];
}
打印结果:
然后在第三部分看到出现崩溃就是在返回LGViewController界面里,改变值是闪退了,理由是出现了野指针了
所以一定要移除观察!!!
实现观察者删除又添加需求
方法一:删除代码
方法二:可以使用这个 automaticallyNotifiesObserversForKey 自动来管理观察开关,这里以 LGPerson 为例
LGViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.student = [LGStudent shareInstance];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGViewController - %@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"null";
self.person.nick = @"Nil";
self.student.name = @"森海北语";
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nick"];
}
LGDetailViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
self.student = [LGStudent shareInstance];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.student.name = @"hello word";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"LGDetailViewController :%@",change);
}
- (void)dealloc{
[self.student removeObserver:self forKeyPath:@"name"];
}
LGPerson.m
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
打印结果:
当 automaticallyNotifiesObserversForKey 返回NO,就是关闭了与 LGPerson 相关的观察者,LGStudent 因为是继承 LGPerson,它自己没有属性用的也是父类的,所以 LGStudent 关于nime的观察者也被关闭了,但是在 willChangeValueForKey 和 didChangeValueForKey 之间的 nick 的值改变时,还能继续观察。
模拟进度条下载-要受观察的属性受多个其他属性影响
在LGPerson 添加几个属性 downloadProgress, writtenData,totalData,在 LGViewController 中进行观察
LGViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.person.writtenData = 0;
self.person.totalData = 100;
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 展开 - 折叠 -
self.person.writtenData += 10;
self.person.totalData += 20;
}
LGPerson.m
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
打印结果:
初始值 writtenData = 0;totalData = 100,当执行了 self.person.writtenData += 10; 时,downloadProgress = 10/100 = 0.100000; 当执行了 self.totalData = 100;时,,downloadProgress = 10/120 = 0.083333。
观察可变数组
在 LGPerson 属性列表中添加一个可变数组 dateArray
LGViewController.m
- (void)viewDidLoad {
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//数组变化
[self.person.dateArray addObject:@"1"];
}
LGPerson.m
- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
[self.dateArray insertObject:object atIndex:index];
}
-(void)removeObjectFromDateArrayAtIndex:(NSUInteger)index{
[self.dateArray removeObjectAtIndex:index];
}
打印结果:
啥也没有,这是为什么呢?数组的变化 KVO 普通的键值观察不走set的,对于可变数组是由特殊的处理的,KVO 建立在 KVC基础之上,所以改变值得地方需要改一下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//数组变化
[self.person.dateArray addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
}
打印:
KVO 原理探索分析
先给出探索代码
LGViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
LGPerson.m
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
打印结果:
从打印看,执行了个 KVO 之后生成了一个 NSKVONotifying_LGPerson 动态类,但是修改的是原对象的isa
那 LGPerson 与 NSKVONotifying_LGPerson 的关系是什么呢?
继承关系
验证:
LGViewController.m 需要做一下改动
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
打印结果:
我们添加了一个打印打印了 LGPerson 类及子类的方法,第一个打印是指注册 KVO之前 进行的,可以看大很正常,打印了类 LGPerson 和它的子类 LGStudent第二个打印是在添加了一个 KVO 之后打印的,这里可以看到 LGPerson 下面有两个子类,NSKVONotifying_LGPerson 就是那个 KVO 的过程中生成的动态类。
此时我们对 "name","nickName"进行了观察,然后在 touch 事件里对他做了,值改变
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"Janice";
self.person->name = @"ty";
}
打印结果:
从打印结果看出,此时被观察到的,只有 nickName 的值变化,那为什么name没有呢,这就是实例变量与属性,这两者的区别就是Setter方法,实例变量没有setter,所以 name 没有被观察到,也就是说,KVO 观察的其实就是setter方法
动态子类:NSKVONotifying_XXX
打印动态子类里的方法列表
LGViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[LGPerson class]];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********************");
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
打印结果:
KVO 的原理
- 1、动态生成子类:NSKVONotifying_XXX
- 2、观察的是 setter
- 3、动态子类重写了很多方法 setNickName(setter)、 class、dealloc、 _isKVO
- 4、移除观察的时候 isa 指向回来
- 5、动态子类不会销毁
手动开始观察
代码例子
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
自定义 KVO
代码实现
LGViewController.m
#import "LGViewController.h"
#import "LGPerson.h"
#import "NSObject+LGKVO.h"
#import <objc/runtime.h>
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person lg_addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
// [self.person lg_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"KC";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person lg_removeObserver:self forKeyPath:@"nickName"];
}
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@",NSStringFromSelector(sel));
}
free(methodList);
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
/// 注册类的总数
int count = objc_getClassList(NULL, 0);
/// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
/// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
@end
LGPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
+ (instancetype)shareInstance;
@end
NS_ASSUME_NONNULL_END
LGPerson.m
#import "LGPerson.h"
@implementation LGPerson
static LGPerson *_instance = nil;
+ (instancetype)shareInstance{
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
_instance = [[super allocWithZone:NULL] init] ;
}) ;
return _instance ;
}
- (void)setNickName:(NSString *)nickName{
NSLog(@"来到 LGPerson 的setter方法 :%@",nickName);
_nickName = nickName;
}
@end
NSObject+LGKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LGKVO)
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
NS_ASSUME_NONNULL_END
NSObject+LGKVO.m
#import "NSObject+LGKVO.h"
#import <objc/message.h>
static NSString *const kLGKVOPrefix = @"LGKVONotifying_";
static NSString *const kLGKVOAssiociateKey = @"kLGKVO_AssiociateKey";
@implementation NSObject (LGKVO)
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 1: 验证setter
[self judgeSetterMethodFromKeyPath:keyPath];
// 2: 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3: isa 指向 isa_swizzling
object_setClass(self, newClass);
}
#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老铁没有当前 %@ 的setter",keyPath] userInfo:nil];
}
}
#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 2.1 判断是否有了
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];// LGKVONotifying_LGPerson
Class newClass = NSClassFromString(newClassName);
if (!newClass) {
// 2.2 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.3 注册类
objc_registerClassPair(newClass);
// 2.4.1 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getClassMethod([self class], @selector(class));
const char *classType = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)lg_class, classType);
}
// 2.4.2 添加setter方法 setNickname
// 判断一下
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getClassMethod([self class], setterSEL);
const char *setterType = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterType);
return newClass;
}
static void lg_setter(id self,SEL _cmd,id newValue){
NSLog(@"来了:%@",newValue);
// 应该些什么? 内部 -> 相应点 newValue oldValue option context -> change
// 回调给外界
}
Class lg_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end