目录
一、自定义KVO流程
下面通过简单自定义KVO
实现系统类似的功能,加深对KVO
底层实现逻辑的理解。
目标如下:
- 模拟系统的
KVO
功能 - 实现自动移除观察者
- 响应式+函数式方式整合
KVO
使用代码
实现步骤:
- 验证是否存在
setter
方法:观察属性,不观察实例变量、成员变量 - 动态生成子类
2.1 申请类(申请类之后、注册类之前可以添加ivar
)
2.2 注册类 - 修改
isa
指向 - 添加父类setter同名方法
- 可以添加方法:注册类后新增加的方法存在
rwe
中(dirty memory
) - 不能添加属性:
ivar
只存在ro
中(clean memory
),因为类加载完毕后其内存大小已经确定
- 可以添加方法:注册类后新增加的方法存在
- 调用父类
setter
- 观察者响应
KVO
通知
二、自定义KVO实现
NAPerson:用于观察该对象中的属性
@interface NAPerson : NSObject {
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSMutableArray *dataArray;
@end
NAKVOInfo:保存信息
typedef void (^LCJKVOBlock)(NSObject *observer, NSString *keyPath, id oldValue, id newValue);
@interface NAKVOInfo : NSObject
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, assign) void *context;
@property (nonatomic, copy) LCJKVOBlock handleBlock;
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block;
@end
//NAKVOInfo.m
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block {
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.context = context;
self.handleBlock = block;
}
return self;
}
NSObject+LCJKVO:通过分类提供添加观察者的方法
@interface NSObject (LCJKVO)
- (void)lcj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block;
@end
//NSObject+LCJKVO.m
static NSString *const kLCJKVOPrefix = @"LCJKVONotifying_";
static NSString *const kLCJKVOAssiociateKey = @"LCJKVO_AssiociateKey";
@implementation NSObject (LCJKVO)
- (void)lcj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block {
//1. 验证是否存在setter方法
[self judgeSetterMethodFromKeyPath:keyPath];
NAKVOInfo *info = [[NAKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath context:context handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLCJKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
// 保存观察者(分类中主要使用关联对象保存变量信息)(TODO:如果有这个Key就不添加了)
// 这里不会存在引用关系,更不会出现循环引用
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLCJKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
//2. 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
//3. 修改 isa 指向
object_setClass(self, newClass);
//4. 添加父类setter同名方法 - 不能添加属性:ivar存在ro中(clean memory),因为类加载完毕后其内存大小已经确定
// 可以添加方法:方法存在dirty memory
SEL setterSel = NSSelectorFromString(setterFromGetter(keyPath));
Method method = class_getInstanceMethod([self class], setterSel);
const char *type = method_getTypeEncoding(method);
class_addMethod(newClass, setterSel, (IMP)lcj_setter, type);
}
#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterFromGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有%@的setter方法",keyPath] userInfo:nil];
}
}
#pragma mark - 创建KVO中间子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLCJKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
if (newClass) return newClass;
// 2.1 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 注册类
objc_registerClassPair(newClass);
// 添加父类class同名方法
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)lcj_class, classTypes);
// 添加父类dealloc同名方法
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)lcj_dealloc, deallocTypes);
return newClass;
}
// 子类重写的setter imp
static void lcj_setter(id self,SEL _cmd,id newValue) {
CJLog(@"进入了子类重写的setter方法:%@",newValue);
NSString *keyPath = getteFromSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//5. 调用父类 setter
// 封装objc_msgSendSuper方法
void (*lcj_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),//= [self class]
};
lcj_msgSendSuper(&superStruct, _cmd, newValue);
// TODO:实现KVO自动开关
//6. 观察者响应KVO通知
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLCJKVOAssiociateKey));
for (NAKVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {//TODO 暂未考虑context
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterFromGetter(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];
}
// 重写的class方法
Class lcj_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
// 重写的dealloc方法
static void lcj_dealloc(id self,SEL _cmd) {
// isa 指回父类
Class superClass = [self class];//已重写了这个方法
object_setClass(self, superClass);
}
#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getteFromSetter(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
ViewController:使用自定义KVO
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [NAPerson new];
[self.person lcj_addObserver:self forKeyPath:@"name" context:NULL handleBlock:^(NSObject * _Nonnull observer, NSString * _Nonnull keyPath, id _Nonnull oldValue, id _Nonnull newValue) {
NSLog(@"%@-%@",oldValue,newValue);
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"differ";
}
三、扩展一:KVOController使用
#import "TestViewController.h"
#import "NAStudent.h"
#import <FBKVOController.h>
@interface TestViewController ()
@property (nonatomic, strong) NAPerson *person;
@property (nonatomic, strong) FBKVOController *kvoCtrl;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [NAPerson new];
self.person.dataArray = [NSMutableArray arrayWithObject:@"1"];
self.kvoCtrl = [FBKVOController controllerWithObserver:self];
[self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"****%@****",change);
}];
//集合类型的观察
[self.kvoCtrl observe:self.person keyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"****%@****",change);
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"differ";
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:self.person.name];
}
@end
使用KVOController也不需要手动移除观察者,进入其底层代码可看到对kvoCtrl做了弱引用:
__weak FBKVOController *_controller;
KVOController流程图如下:
四、扩展一:GNU
由于Apple Foundation层源码不开源导致不清楚底层的实现逻辑,而GNU被认为是Foundation层最接近的源码实现。GNU下载地址 下载GNUstep Base
进入KVO
原生添加观察者方法:- (void)addObserver:forKeyPath:options:context:
可以看到是在NSObject(NSKeyValueObserverRegistration)
分类中。
在GNU
项目中搜索NSKeyValueObserverRegistration
即可看到KVO
的GNU
源码。
网友评论