乐趣区

2 RAC解析 自定义KVO

知识点概述
1.KVO 实现原理 2.runtime 使用
目的
给 NSObject 添加一个 Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。
大体思路
添加观察者的方法中
– (void)SQ_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
会用 runtime 的方式手动创建一个其子类,并且将该对象变为该子类。该子类会复写观察方法中 keyPath 的 setter 方法,使这个 setter 被调用时利用 runtime 去调用 observer 的回调方法
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;
实现
这里只做 KVO 的基本功能,当被观察者改变属性的时候通知观察者,所以定义如下方法
NSObject+SQKVO.h
/**
添加观察者

@param observer 观察者
@param keyPath 被观察的属性名
*/
– (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/**
当被观察的观察属性改变的时候的回调函数

@param keyPath 所观察被观察者的属性名
@param object 被观察者
@param value 被观察的属性的新值
*/
– (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value;

@end
因为这里要用到 runtime 所以需要添加 runtime 的头文件
#import <objc/message.h>
而且因为用到 objc_msgSend 所以要改变一下工程的环境变量
一. 动态生成子类
在被观察者调用 - SQ_addObserver:forKeyPath: 时首先动态生成一个其子类。
// 1. 生成子类
// 1.1 获取名称
Class selfClass = [self class];
NSString *className = NSStringFromClass(selfClass);
NSString *KVOClassName = [className stringByAppendingString:@”_SQKVO”];
const char *KVOClassNameChar = [KVOClassName UTF8String];
// 1.2 创建子类
Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0);
// 1.3 注册
objc_registerClassPair(KVOClass);
这里可以看到,我们将子类的类名命名为“类名”+“_SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。这里有个注意点,一般为动态创建的类名应尽量复杂一些避免重复。最好加上“_”。
二. 根据 KeyPath 动态添加对应的 setter
1 确定 setter 的名字
举个例子,如果用户给的 keyPath 是 name,应该动态添加一个 -setName: 的方法。而这个 setter 的名字是 “set” + “ 把 keyPath 变为首字母大写 ” + “:” 所以可以得出
NSString *setterString =
[NSString stringWithFormat:@”set%@:”, [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);
2 利用 class_addMethod()给子类动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

cls: 给哪个类添加方法。即新生成的子类,上面生成的 KVOClass。
name:所添加方法的名称。即上一步生成的字符串 setterString。
imp:所添加方法的实现。即这个方法的 C 语言实现,首先在下面先写一个 C 语言的方法。稍后会讲具体实现。

void setValue(id self, SEL _cmd, id newVale) {
}
types:
所添加方法的编码类型。setter 的返回值是 void,参数是一个对象(id)。void 用 ”v” 表示,返回值和参数之间用“@:”隔开,对象用 ”@” 表示。最后我们可以得出结果 ”v@:@”。具体其他的编码类型可以参考苹果文档。
ps: 这里说下为什么返回值和参数之间用“@:”隔开。“:”代表字符串,所有的 OC 方法都有两个隐藏参数在参数列表的最前面,“发起者”和“方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个 types 其实是 imp 返回值和参数的编码。因为 OC 方法中返回值和参数之间必然有“发起者”和“SEL”隔着,所以“@:”自然而然就成了返回值和参数之间的分隔符。当然我们还可以用 @encode 来得到我们想要的编码类型
NSString *encodeString =
[NSString stringWithFormat:@”%s%s%s%s”,
@encode(void),
@encode(id),
@encode(SEL),
@encode(id)];
3 将当前对象的类变为我们所创建的子类的类型,即更改 isa 指针
object_setClass(self, KVOClass);
4 将 keyPath 和观察者关联 (associate) 到我们的对象上
用下面这个函数可以很方便的将一个对象用键值对的方式绑定到一个目标对象上。* 如果想了解跟多可以查找《Effective Objective-C》的第 10 条
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
object
目标对象
key
绑定对象的键,相当于 NSDictionary 的 key 这里的 key 一般采用下面的方式声明:
static const void *SQKVOObserverKey = &SQKVOObserverKey;
static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;
这样做是因为若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。
value
绑定对象,相当于 NSDictionary 的 value
policy
绑定对象的缓存策略 @property (nonatomic, weak):OBJC_ASSOCIATION_ASSIGN@property (nonatomic, strong):OBJC_ASSOCIATION_RETAIN_NONATOMIC@property (nonatomic, copy):OBJC_ASSOCIATION_COPY_NONATOMIC@property (atomic, strong):OBJC_ASSOCIATION_RETAIN@property (atomic, weak):OBJC_ASSOCIATION_COPY
最后关联的代码:
objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
三.setValue()的实现
这个函数的目的主要是:1. 利用 objc_msgSend 触发原先类的 setter2. 利用 objc_msgSend 触发观察者的回调方法
1. 触发原先的 setter 方法
// 保存子类
Class KVOClass = [self class];

// 变回原先的类型,去触发 setter
object_setClass(self, class_getSuperclass(KVOClass));
NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey);
NSString *setterString = [NSString stringWithFormat:@”set%@:”, [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);
objc_msgSend(self, setter, newVale);
2. 调用观察者的回调方法
id observer = objc_getAssociatedObject(self, SQKVOObserverKey);
objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);
3. 改回 KVO 类
object_setClass(self, KVOClass);
四. 实现空的回调方法
– (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {

}
五. 调用自定义的 KVO
恭喜你看到这里,并且恭喜你已经成功了!
– (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

self.name = @”A”;
[self SQ_addObserver:self forKeyPath:@”name”];

self.name = @”B”;
}

– (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {
NSLog(@”%@.%@=%@”, object, keyPath, value);
}
六. 代码
代码下载地址

退出移动版