KVO 本质 & 自定义实现
KVO 是什么?
Key-Value Observer 即键值观察者。作用为监听某个对象的某个属性的值发生改变,通知其观察者,观察者可以做出相应的处理。
KVO 的使用方法很简单,代码如下:
- (void)viewDidLoad
{
[super viewDidLoad];
XYPerson *p1 = [XYPerson new];
XYPerson *p2 = [XYPerson new];
// 1.设置监听
[p2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
// 触发
p1.age = 10;
p2.age = 20;
// 3.移除监听
[p2 removeObserver:self forKeyPath:@"age"];
}
// 2.收到监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"keyPath = %@",keyPath); // keyPath = age
NSLog(@"object = %@",object); // object = <XYPerson: 0x6000010d68b0>
NSLog(@"change = %@",change); // change = { kind = 1; new = 20;}
NSLog(@"context = %@",context); // context = (null)
}
内部实现原理
15919555920910.jpg- 系统监听到
addObserver:forKeyPath:options:context:
消息的接受者 p2,会根据其类XYPerson
动态创建一个子类NSKVONotifying_XYPerson
,并将 p2->isa 指向其子类 - 子类
NSKVONotifying_XYPerson
内部重写了 KeyPath 的 set 方法, 其实现思路如下
[self willChangeValueForKey:@"age"]; // 发生改变前
[super setAge:age];
[self didChangeValueForKey:@"age"]; // 发生改变之后,内部会实现对 observer 的回调
- 系统会重写子类的
class
方法,隐藏动态创建的子类,在使用者看来p2指向的还是XYPerson
类 - 调用移除监听方法,会将 p2->isa 重新指向自己原来的类
添加KVO之前:
p2 : <XYPerson: 0x600002ce4530>
p2->isa : XYPerson
添加KVO之后:
p2 : <XYPerson: 0x600002ce4530> // 因为运行时,修改了内部的 class 实现,屏蔽真实类
p2->isa : NSKVONotifying_XYPerson
移除KVO之后:
p2 : <XYPerson: 0x600002ce4530>
p2->isa : XYPerson
自定义实现 KVO
系统 KVO 好用,但是其写法为固定的三段式,且代码比较分散(见上面 KVO 示例代码)。
熟悉了 KVO 的原理,就可以自己基于 runtime 来实现一个简单的 KVO。直接上代码
/// 一行代码添加KVO监听
/// @param key 需要被监听的key
/// @param options 一个混合选项,在 readonly 属性中与系统同效果
/// @param handler 监听回调,需注意 block 循环引用
- (void)xy_addObserverForKey:(NSString *)key
options:(NSKeyValueObservingOptions)options
observerHandler:(xy_observerHandler)handler;
这个 KVO 可以实现一行代码添加 KVO 监听,回调直接在 block 中执行,且无需手动执行移除监听。监听关系会在内部通过 runtime 与 被观察对象进行绑定。当被监听对象销毁的时候,自行销毁。
具体实现如下:
- (void)xy_addObserverForKey:(NSString *)key options:(NSKeyValueObservingOptions)options observerHandler:(xy_observerHandler)handler
{
// 1. 校验
SEL setterSEL = [self selWithKey:key];
Method setterMethod = class_getInstanceMethod(self.class, setterSEL);
if (!setterMethod) {
// 没有相应的 setter 方法实现。 如 WKWebView.title
// 此处为 readonly 方法,具体实现可以看 Demo
return;
}
// 2. 创建子类
Class clz = object_getClass(self);
NSString *clzName = NSStringFromClass(clz);
if (![clzName containsString:clzPre]) {
clz = [self creatKVOClassWithOriginalClassName:clzName];
// 设置成子类
object_setClass(self, clz);
}
// 3. 添加方法,仅添加一次
if (![self hasSeleter:setterSEL]) {
const char *types = method_getTypeEncoding(setterMethod);
BOOL s = class_addMethod(clz, setterSEL, (IMP)kvo_setter, types);
NSLog(@"%d",s);
}
// 4. 保存KVO相关内容
KVOInfomation *info = [[KVOInfomation alloc] initWithObserver:self key:key opeitons:options handler:handler];
NSMutableArray *infos = objc_getAssociatedObject(self, xy_infosKey);
if (!infos) {
infos = [NSMutableArray array];
objc_setAssociatedObject(self, xy_infosKey, infos, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[infos addObject:info];
}
第一步校验此 Key 是否为 readonly 的属性,如果是 readonly 属性则没有 setter 方法,就无法通过创建子类监听 setter 然后代用父类 setter 的方式实现。系统的实现应该是内部做了兼容,当加载到被监听的 Key 时候主动调用了监听器方法。
/// 通过key获取父类的 set 方法
/// @param key 监听的key
- (SEL)selWithKey:(NSString *)key{
// 父类是否存在此set方法
NSString *selName = [NSString stringWithFormat:@"%@%@%@",@"set",[self xy_firstCharUpper:key],@":"];
SEL sel = NSSelectorFromString(selName);
return sel;
}
- (NSString *)xy_firstCharUpper:(NSString *)string
{
if (string.length == 0) return string;
NSMutableString *stringM = [NSMutableString string];
[stringM appendString:[NSString stringWithFormat:@"%c", [string characterAtIndex:0]].uppercaseString];
if (string.length >= 2) [stringM appendString:[string substringFromIndex:1]];
return stringM;
}
第二步创建子类,先判断当前对象是否已经是子类,如果是首次就创建子类并将当前对象指向其子类类型。
/// 通过本类创建KVO监听的子类
- (Class)creatKVOClassWithOriginalClassName:(NSString *)originalClassName{
NSString *originalClzName = originalClassName;
NSString *newClassName = [clzPre stringByAppendingString:originalClzName];
if (objc_getClass(newClassName.UTF8String)) {
return NSClassFromString(newClassName);
}
Class newClass = objc_allocateClassPair(self.class, [newClassName UTF8String], 0);
// 重写class 方法,指向父类,隐藏kvo子类
const char *types = method_getTypeEncoding(class_getInstanceMethod(newClass, @selector(class)));
class_addMethod(newClass, @selector(class), (IMP)kvo_class, types);
objc_registerClassPair(newClass);
return newClass;
}
// 重写子类的 class 实现,屏蔽其子类的存在
static Class kvo_class(id self, SEL _cmd)
{
return class_getSuperclass(object_getClass(self));
}
第三步给子类添加被监听Key的 setter 方法实现,其内部调用父类的 setter 方法,并执行监听回调
static void kvo_setter(id self, SEL _cmd, id newValue)
{
// 给父类发送修改值的消息
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [self getterFromSetterName:setterName];
if (!getterName) {
NSLog(@" ----- 原来类中没有 getter 方法");
}
id oldValue = [self valueForKey:getterName];
// 调用父类setter
// 构造 objc_super 的结构体
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
// 对 objc_msgSendSuper 进行类型转换,解决编译器报错的问题
void (* objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ,传入结构体、方法名称,和参数等
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// 回调 handler - 依据options
NSMutableArray *infos = objc_getAssociatedObject(self, xy_infosKey);
for (KVOInfomation *info in infos) {
if ((info.observer == self) && ([info.key isEqualToString:getterName])) {
NSDictionary *dict = @{
@"observeredObj": info.observer,
@"key": info.key,
NSKeyValueChangeNewKey: newValue?:@"",
NSKeyValueChangeOldKey: oldValue?:NSNull.null
};
info.handler(dict);
}
}
}
第四步保存添加的监听信息,当监听到某个 Key 发生回调之后,可以去内部缓存中去调用回调。
关于监听关系类的设计如下,其属性均为 readonly, 规避了一些循环引用问题。
@interface KVOInfomation : NSObject
/** observer: 被观察对象 */
@property (nonatomic, readonly,weak) NSObject * observer;
/** key */
@property (nonatomic, readonly,copy) NSString *key;
/** options */
@property (nonatomic, readonly,assign) NSKeyValueObservingOptions options;
/** key */
@property (nonatomic, readonly,copy) xy_observerHandler handler;
- (instancetype)initWithObserver:(id)observer key:(NSString *)key opeitons:(NSKeyValueObservingOptions)options handler:(xy_observerHandler)handler;
@end
代码就写到这里了,有兴趣的可以看一下全部 Demo
祝大家学习进步~
网友评论