在iOS开发中,KVO的使用频率是非常高的,可能是间接使用也可能是直接使用,今天主要通过以下几点进行探索。
KVO初探
1、 首先通过简单的使用KVO进行分析苹果提供的KVO中的API每个参数所代表的含义以及怎么使用能够让API达到最优使用。
下面看下今天第一份代码:
KGPerson.h
代码如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KGPerson : NSObject
/// 用户姓名
@property(nonatomic,copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
KGPerson.m
代码如下:
#import "KGPerson.h"
@implementation KGPerson
@end
ViewController.m
中代码如下:
#import "ViewController.h"
#import "KGPerson.h"
@interface ViewController ()
@property (nonatomic,strong) KGPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[KGPerson alloc] init];
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionPrior context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@==%@==%@",keyPath,object,change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.name = @"KG";
}
@end
以上就是我们经常使用的KVO
的时候的常规写法,那么接下来先看下- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
这个方法中参数的含义,observer
是观察者对象,也就是消息接受者;keyPath
是路径,也就是我们需要观察的属性或者成员变量;options
和context
通过KVO我们可以看到苹果对于参数的一些解释,那么我们通过代码去观察下这些属性,首先看下options,这是一个枚举,如下:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,//值为1,指示更改字典应提供新的属性值(如果适用)。
NSKeyValueObservingOptionOld = 0x02,//值为2,指示更改字典应包含旧属性值(如果适用)。
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,//值为4,如果指定,则应在观察者注册方法返回之前立即向观察者发送通知。也就是用户主要注册了监听,在值还没有改变前就发送一次消息
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08,值为8,是否应该在每次更改前后向观察者发送单独的通知,而不是更改后的单个通知。
};
补充:
1、枚举值如果是这种按照1<<x
位表示,那么就代表可以进行多选2、以上枚举值
options
存在以下几种情况:(1)、
options=1
:用户选定NSKeyValueObservingOptionNew
(2)、
options=2
:用户选定NSKeyValueObservingOptionOld
(3)、
options=3
:用户选定NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
(4)、
options=4
:用户选定NSKeyValueObservingOptionInitial
(5)、
options=5
:用户选定NSKeyValueObservingOptionInitial
和NSKeyValueObservingOptionNew
(6)、
options=6
:用户选定NSKeyValueObservingOptionInitial
和NSKeyValueObservingOptionOld
(7)、
options=7
:用户选定NSKeyValueObservingOptionInitial
、NSKeyValueObservingOptionOld
、NSKeyValueObservingOptionNew
(8)、
options=8
:用户选定NSKeyValueObservingOptionPrior
(9)、
options=9
:用户选定NSKeyValueObservingOptionPrior
和NSKeyValueObservingOptionNew
(10)、
options=10
:用户选定NSKeyValueObservingOptionPrior
和NSKeyValueObservingOptionOld
(11)、
options=11
:用户选定NSKeyValueObservingOptionPrior
、NSKeyValueObservingOptionOld
、NSKeyValueObservingOptionNew
(12)、
options=12
:用户选定NSKeyValueObservingOptionInitial
和NSKeyValueObservingOptionPrior
(13)、
options=13
:用户选定NSKeyValueObservingOptionInitial
、NSKeyValueObservingOptionPrior
、NSKeyValueObservingOptionNew
(14)、
options=14
:用户选定NSKeyValueObservingOptionInitial
、NSKeyValueObservingOptionPrior
、NSKeyValueObservingOptionNew
、NSKeyValueObservingOptionOld
然后我们通过上述补充中的方案编写代码去看下具体效果:
方案1:(需要点击屏幕触发touchesBegan
)
方案2:(需要点击屏幕触发touchesBegan
)
方案3:(需要点击屏幕触发touchesBegan
)
方案4:(不需要点击屏幕)
image.png方案5:(不需要点击屏幕)
image.png方案6:(不需要点击屏幕)
image.png方案7:(不需要点击屏幕)
image.png方案8:(需要点击屏幕触发touchesBegan
)
方案9:(需要点击屏幕触发touchesBegan
)
方案10:(需要点击屏幕触发touchesBegan
)
方案11:(需要点击屏幕触发touchesBegan
)
方案12:(不点击屏幕,打印第一行,点击屏幕触发touchesBegan
打印后两行)
方案13:(不点击屏幕,打印第一行,点击屏幕触发touchesBegan
打印后两行)
方案14:(不点击屏幕,打印第一行,点击屏幕触发touchesBegan
打印后两行)
以上就是所有options
的方案结果,可以根据自己项目中场景去选择相应的方案。
接下来看下context
参数,这个参数在KVO中解释的很通彻,主要就是来区分不同的类有相同的属性监听,也就是相同用的keyPath
情况下,简化判断条件用的,而且这种判断更安全。下面看下我们常规判断和使用context
判断的实例:
常规观察不同对象的相同属性代码书写:(我们经常写的时候
image.pngcontext
传的是nil
,这种写法虽然运行没有错,但是从代码严谨程度来说是错的,因为苹果在文档中明确指出,这块如果不使用,传入NULL
)使用
image.pngcontext
观察不同对象的相同属性代码书写:补充:
nil
:在OC
中表示一个指针的值为空,经常用来创建一个空对象,表示指针不指向任何内存空间。
NULL
:在C
中表示一个指针的值为空,表示指针不指向任何内存空间。
到这里基本上对于KVO
的使用,我想已经信手拈来了,那么我们再进一步了解下KVO
的主动调用和被动调用。最后记得移除监听,一般都是在dealloc
中进行监听的移除,如下:
2、我们在开发中有的时候,比如说成员变量的KVO
监听,我们直接修改值是监听不到的,那么这时候我们通常会手动调用willChangeValueForKey
和didChangeValueForKey
来触发KVO
监听。代码如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[_person willChangeValueForKey:@"_nikeName"];
[_person setValue:@"KG" forKey:@"_nikeName"];
[_person didChangeValueForKey:@"_nikeName"];
}
另外对于属性的监听,我们如果需要手动去触发KVO
的回调,那么那么应该先重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法。具体如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if (key isEqualToString:@"name") {
return NO;
}
return YES;
}
对于属性我们手动调用除了以上方法重写,还需要主动调用willChangeValueForKey
和didChangeValueForKey
,具体写法如下:
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
到此我们对于KVO
的基本使用就完成了,那么接下来那就看下苹果是如何去实现KVO
的,原理是什么?请听下回分析。
KVO原理探索
1、话不多说,先看以下动图,我们看图说话。
yzsrs-65wku.gif从以上动画我们可以看到,当我们对一个对象添加KVO
属性观察的时候,系统会修改我们对象的isa
指针,指向一个运行时动态创建的类NSKVONotifying_KGPerson
,那么会有同学问了,你咋知道,我不告诉你是苹果给我说的,苹果当时是这么说的:
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
经过我的翻译是这么说的:
自动键值观察是使用称为isa-swizzling的技术实现的。
该isa指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据。
当观察者为对象的属性注册时,被观察对象的 isa 指针被修改,指向中间类而不是真正的类。因此,isa 指针的值不一定反映实例的实际类。
您永远不应该依赖isa指针来确定类成员资格。相反,您应该使用该class方法来确定对象实例的类。
image.png
2、实际上这个是在苹果官方文档KVO中有解析。说的很明确,就是通过isa-swizzling
技术实现的,简单点来说就是在运行时,使用runtime
动态创建一个类,然后将对象的isa
指针指向进行修改,让isa
指向动态创建的类,那么这个动态创建的类是继承于哪个类呢?我们一起修改下代码,然后运行打印,具体代码以及结果如图所示:
我们分别在添加属性监听前以及添加监听后打印KGPerson
这个类的以及它的所有子类,我们可以通过打印输出看到,当添加监听后,系统会动态创建一个继承于KGPerson
类的子类NSKVONotifying_KGPerson
。然后我们修改下KGPerson
这个类代码如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KGPerson : NSObject{
@public
NSString *_nikeName;
}
@property (nonatomic,copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
修改完成后,我们在使用LGPerson
类的地方进行如图所示的修改以及属性监听,然后看下效果:
从上面打印结果可以看出,对于成员变量的监听没有效果,对于属性的监听是能够监听到。然后我对以上代码进行修改,再次运行看下效果:
image.png对于成员变量的属性监听走了,那么对此我们可以得出以下结论:
KVO
的原理包含以下两点:
动态生成子类:
NSKVONotifying_XXX
观察的是
setter
方法
3、下面我们看下动态创建的这个继承于KGPerson
的子类NSKVONotifying_KGPerson
中系统做了哪些操作?下面先看以下代码,然后我们进行分析:
通过以上代码运行结果,我们可以分析出动态创建的子类做了以下几个操作:
重写观察的属性的
setter
方法重写
class
方法,这个方法返回的还是KGPerson
类。重写
dealloc
方法,在执行销毁方法后,会将isa
指针指会到KGPerson
,而且动态创建的类会进行缓存。实现了
_isKVOA
方法
4、对于上面第三条结论,我们进行验证下,修改下代码然后运行,结果如下:
image.png我们在dealloc
方法中进行监听的移除,然后移除完成后走断点,打印输出可以看到po object_getClassName(self.person)
对象的isa
又指会到KGPerson
了,然后我们在上一个界面打印下KGPerson
的所有子类,代码以及结果如下:
5、从上图再次证明了,上面第三条结论,当动态创建子类后,系统会默认缓存子类。到此我们了解了系统KVO
运行的一个基本原理,流程如下:
当我们了解了
KVO
的原理后,那么我们是否可以自定义实现KVO
呢?下面一起研究自定义KVO
。
自定义模拟KVO
1、首先我们先对系统的KVO
方法进行分析,通过查看KVO
的api
我们可以看到,系统是对NSObject
类进行了扩展,也就是通过分类来实现的。那么我们也同样通过分类来进行,比较如果想要对全局所有的类都能进行属性监听,对NSObject
进行分类扩展是最好的,因为所有的类都是继承于NSObject
的,那么下面我们先创建一个NSObject
的分类,命名为KGKVO
。具体代码如下,代码中有详细的注释:
NSObject+KGKVO.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KGKVO)
- (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
NS_ASSUME_NONNULL_END
NSObject+KGKVO.m
#import "NSObject+KGKVO.h"
#import <objc/message.h>
// 动态创建子类时的类名前缀
static NSString *const kKGKVOPrefix = @"KGKVONotifying_";
// 获取消息观察者需要的关键字
static NSString *const kKGKVOAssiociateKey = @"kKGKVO_AssiociateKey";
@implementation NSObject (KGKVO)
- (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 1、验证setter方法是否存在
[self judgeSetterMethodFormKeyPath:keyPath];
// 2、动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3、isa指向新创建的子类,isa_swizzling
object_setClass(self, newClass);
// 4、保存观察者
objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
// 获取到当前子类的父类
Class superClass = [self class];
object_setClass(self, superClass);
}
#pragma mark -- 验证是否存在setter方法
- (void)judgeSetterMethodFormKeyPath:(NSString *)keyPath{
// 获取父类
Class supperClass = object_getClass(self);
// 生成setter方法
SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
// 根据SEL获取方法
Method setterMethod = class_getInstanceMethod(supperClass, setterSEL);
// 判断是否存在方法
if (!setterMethod) {
// 如果方法不存在,抛出异常
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"对不起老铁,当前%@没有setter方法",keyPath] userInfo:nil];
}
}
#pragma mark -- 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 获取当前类的类名
NSString *oldClassName = NSStringFromClass([self class]);
// 生成当前类的子类类名
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kKGKVOPrefix,oldClassName];
// 根据生成的子类类名获取类
Class newClass = NSClassFromString(newClassName);
// 判断是否已经存在子类
if (!newClass) {
// 如果不存在,先申请类,第一个参数是需要传入父类,第二个参数是需要传入类名,第三个参数是需要申请的内存空间大小
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
// 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
// 根据SEL获取Method
Method classMethod = class_getInstanceMethod([self class], @selector(class));
// 获取方法参数以及方法返回类型
const char *classType = method_getTypeEncoding(classMethod);
// 给类添加class方法
class_addMethod(newClass, classSEL, (IMP)kg_class, classType);
}
// 创建setter方法SEL
SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
// 根据SEL获取Method
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
// 获取方法参数以及返回值类型
const char *type = method_getTypeEncoding(setterMethod);
// 添加方法
class_addMethod(newClass, setterSEL, (IMP)kg_setter, type);
// 返回子类
return newClass;
}
#pragma mark --重写dealloc
//static
#pragma mark --创建setter方法
static void kg_setter(id self,SEL _cmd,id newValue){
// 需要进行消息转发,调用msgSendSuper()函数,所以需要创建结构体对象
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass([self class])
};
// 进行强转
void (*kg_objc_msgSendSuper)(void *,SEL ,id) = (void *)objc_msgSendSuper;
// 进行消息转发
kg_objc_msgSendSuper(&superStruct,_cmd,newValue);
// 然后拿到消息观察者
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
// 然后将消息发送给观察者
SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
// 获取getter方法
NSString *keyPath = getterFormSetter(NSStringFromSelector(_cmd));
// 消息转发
((void (*)(id, SEL, NSString *, id, NSDictionary *, void *))(void *)objc_msgSend)(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
}
#pragma mark -- 获取类的父类
Class kg_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
#pragma mark -- 从getter方法获取setter方法 keyPath->setKeyPath
static NSString *setterSelector(NSString *getter){
// 判断keyPath是否合法
if (getter.length <= 0) {
return nil;
}
// 取第一个字符并且转换成大写
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
// 获取除第一个字符外的其它字符
NSString *leaveString = [getter substringFromIndex:1];
// 返回setter方法
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark -- 从setter方法获取getter方法
static NSString *getterFormSetter(NSString *setter){
// 判断setter方法时候合法
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
// 获取keyPath
NSRange range = NSMakeRange(3, setter.length-4);
// 获取getter方法名
NSString *getter = [setter substringWithRange:range];
// 获取第一个字符,并转换成小写
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
// 返回getter方法
return [setter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end
2、然后在调用的地方如下图所示使用:
image.png3、到这里我们仿照系统的KVO
实现原理自定义实现了KVO
,但是也只是仿照系统的实现对属性的观察,没有达到和系统KVO
一样的完善,所以我们继续去完善这个自定义的KVO
。目前存在的问题:
如果说多个对象进行属性值观察,那么我们通过
objc_setAssociatedObject
来保存的observer
就会出现错乱。如果一个对象同时需要观察多个属性,那么意味着我们要多次调用
objc_setAssociatedObject
这个函数,那么就会出现重复使用同一个关键值和关联策略去关联不同的值给对象,那么很容易造成
内存泄露。
4、那么接下来我们针对以上的问题进行完善自定义KVO
,请看下回分析。
自定义模拟KVO+完善
1、首先我们针对多个属性进行观察,首先能够想到的就是通过数组去保存一些信息,然后从数组中取值然后判断做一系列的操作,那么那些信息我们怎么归类呢?在iOS中信息归类,我们无非就是结构体、对象、字典等等,但是在此处我们使用对象,定义如下对象:
@interface KGInfo : NSObject
/// 观察者对象
@property (nonatomic,strong) NSObject *observer;
/// 属性
@property (nonatomic,copy) NSString *keyPath;
/// 观察键值条件
@property (nonatomic,assign) NSKeyValueObservingOptions options;
/// 辨别标识符
@property (nonatomic,assign) void * context;
/// 初始化方法
/// @param observer 观察者对象
/// @param keyPath 属性
/// @param options 观察键值条件
/// @param context 辨别标识符
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
@implementation KGInfo
/// 初始化方法
/// @param observer 观察者对象
/// @param keyPath 属性
/// @param options 观察键值条件
/// @param context 辨别标识符
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
self.context = context;
}
return self;
}
@end
3、然后在之前的基础上做相应的修改:
- (void)kg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 1、验证setter方法是否存在
[self judgeSetterMethodFormKeyPath:keyPath];
// 2、动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3、isa指向新创建的子类,isa_swizzling
object_setClass(self, newClass);
// 4、先进行判断是否已经存在对象属性观察表了
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey));
// 5、判断arr是否存在,如果不存在进行创建,类似懒加载
if (!arr) {
arr = [NSMutableArray array];
}
// 6、创建信息对象
KGInfo *info = [[KGInfo alloc] initWithObserver:observer forKeyPath:keyPath options:options context:context];
// 7、将对象信息添加到数组
[arr addObject:info];
// 7、保存观察属性信息
objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKGKVOAssiociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)kg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
if (arr.count <= 0) {
return;
}
for (KGInfo *info in arr) {
if ([info.keyPath isEqualToString:keyPath]) {
[arr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observer.copy <= 0) {
// 获取到当前子类的父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 获取当前类的类名
NSString *oldClassName = NSStringFromClass([self class]);
// 生成当前类的子类类名
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kKGKVOPrefix,oldClassName];
// 根据生成的子类类名获取类
Class newClass = NSClassFromString(newClassName);;
if (!newClass) {
// 如果不存在,先申请类,第一个参数是需要传入父类,第二个参数是需要传入类名,第三个参数是需要申请的内存空间大小
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
// 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
// 根据SEL获取Method
Method classMethod = class_getInstanceMethod([self class], @selector(class));
// 获取方法参数以及方法返回类型
const char *classType = method_getTypeEncoding(classMethod);
// 给类添加class方法
class_addMethod(newClass, classSEL, (IMP)kg_class, classType);
}
// 创建setter方法SEL
SEL setterSEL = NSSelectorFromString(setterSelector(keyPath));
// 根据SEL获取Method
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
// 获取方法参数以及返回值类型
const char *setterTypes = method_getTypeEncoding(setterMethod);
// 添加方法
class_addMethod(newClass, setterSEL, (IMP)kg_setter, setterTypes);
// 返回子类
return newClass;
}
#pragma mark --创建setter方法
static void kg_setter(id self,SEL _cmd,id newValue){
// 获取getter方法
NSString *keyPath = getterFormSetter(NSStringFromSelector(_cmd));
// 获取旧值
id oldValue = [self valueForKey:keyPath];
// 需要进行消息转发,调用msgSendSuper()函数,所以需要创建结构体对象
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass([self class])
};
// 进行消息转发
((void (*)(struct objc_super *,SEL,id))(void *)objc_msgSendSuper)(&superStruct,_cmd,newValue);
// 然后拿到观察信息数组
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKGKVOAssiociateKey));
// 判断数组是否有值
if (arr.count > 0) {
// 循环拿出数组中的KGInfo对象
for (KGInfo *info in arr) {
// 判断是否是需要的info
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionary];
// 对新旧值进行处理
if (info.options & NSKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & NSKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
}else{
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
// 然后将消息发送给观察者
SEL observerSEL = @selector(kg_observeValueForKeyPath:ofObject:change:context:);
// 消息转发
((void (*)(id, SEL, NSString *, id, id, void *))(void *)objc_msgSend)(info.observer,observerSEL,keyPath,self,change,NULL);
});
}
}
}
}
4、到此的话一个基础的KVO属性观察完成了,但是还是存在一些瑕疵,我们需要添加属性观察,然后跑到- (void)kg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
去判断监听返回的是哪个属性的监听等等一系列操作,那么到此处我们是否还能优化呢?当然是可以的,我们自然而然就想到了Block
,函数式编程能够让我们的代码更加简洁,所以请看下回分析。
自定义模拟KVO+函数式编程思想
1、针对之前的代码比较繁琐,我们再次做一下优化,使用函数式编程的思想,去打破系统KVO
繁琐的步骤,经过优化后代码如下:
2、那么使用的时候就更加简单了,如下:
image.png3、从书写上我们就可以看到,在哪添加的观察,就在那实现回调,逻辑来说更加清晰,而且去掉了繁琐的observer
和keyPath
的判断,让代码更加简洁。
4、既然我们对自定义KVO
都简化到这个程度了,那么我们是否还能再简化,直接实现自动销毁呢?不需要手动去销毁呢?因为经常会忘记手动remove
监听,答案是肯定可以的,那么请看下一节分析。
补充:
- 在这里有一个细节性的东西,就是对
observer
的修饰符,为什么不用strong
而是使用weak
的原因在于,防止循环引用,因为如果不用weak
去打断闭环,那么就是处于VC持有->person(持有)->arr(持有)->info(持有)->VC
的情况。
自定义模拟KVO+自动销毁
1、我们发现每次需要添加属性观察都需要去手动移除,而且很多时候因为粗心大意,很容易忘记移除,那么程序运行就会立马报错奔溃,所以为了防止这种情况,我们需要去考量能否让它进行自动销毁,当对象释放的时候,自动移除属性观察呢?此时我们想到了对系统的方法dealloc
进行method_swizzled
,但是我们应该在什么时机下去做这个操作呢?目前来说我们经常用的是在load
方法中,还有当前添加监听的时候,也就是addObserver
的时候,但是选择哪个呢?首先我们去判断如果在addObserver
的时候做方法Hook的话,怎么去判断是否已经Hook过了?所以我们决定在load
方法中进行hook
,但是因为NSObject
是所有类的基类,这样每个类的load
方法走的时候都会进行Hook,会造成混乱,到时候我们自己也不知道是否Hook成功,所以我们在整个app启动后,在load
方法中只进行一次Hook,而且之后不会再进行Hook,所以我们想到了使用单利时的做法,通过dispatch_once
,然这个操作只执行一次。
2、当我们理清思路后,然后回头去修改代码,就变的很简单了,NSObject+KGKVO.h
的优化如下:
直接去掉了监听移除的一系列处理,直接在Hook后的myDealloc
方法中进行移除,简单而且严谨。
3、当我们使用的时候只需要去添加属性监听,不需要去做额外的处理,也不需要去检查是否进行监听的移除了,只需要一句简简单单的代码调用,就完成了整个复杂的KVO
监听,使用如下:
一行代码,搞定通过KVO
监听属性值变化,而且也没有其他的配置选项,自动返回旧值、新值等等。
总结
到此对于KVO
的探索基本完成了,但是里面还有很多细节性的东西需要自己去优化,比如:
监听不同类型的属性,目前的代码中
kg_setter
方法返回值以及参数不一致会导致奔溃问题对于成员变量的监听实现等等
如果对此感兴趣的同学可以一起探讨,或者去分析下FBKVOController
网友评论