上篇文章讲到了什么是isa指针以及KVO的底层实现,如果对KVO和isa指针不熟悉的需要先看看这篇文章。本篇文章主要是实现含有Block的KVO方法。先上代码
1、 KVO的简单实现
从上篇文章中我们知道KVO的底层是通过运行时动态创建一个子类进行监听属性的变化的。我们这里先给出一个简单的实现:
1、创建一个Dog类,含有有个name属性。
2、手动创建一个SimpleKVO_Dog类继承自Dog类,重写setName方法。
3、给NSObject添加个category,增加添加观察者的方法和观察者回调方法,实现代码如下:
NSObject的category中的代码:
#import "SimpleKVO_Dog.h"
NSString *const ObserverKey = @"ObserverKey";
@implementation NSObject (SimpleKVO)
- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 保存观察者
objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 修改isa指针指向的类(指向了Dog子类),这里将指向我们手动创建的Dog的子类
object_setClass(self, [SimpleKVO_Dog class]);
}
// 这里做是为了容错处理
- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{}
@end
子类重写的setName方法实现:
- (void)setName:(NSString *)name{
// 保存旧值
NSString *oldName = self.name;
// 调用父类方法
[super setName:name];
// 获取观察者
id obsetver = objc_getAssociatedObject(self, ObserverKey);
NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
// 调用回调方法,传递旧值和新值
[obsetver ll_observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}
调用代码:
- (void)test{
Dog *dog = [Dog new];
//isa --->Dog类
[dog ll_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
//isa---> SimpleKVO_Dog类
dog.name = @"aaa";
dog.name = @"bbb";
}
- (void)ll_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
打印结果:
上述方法中我们是通过手动创建Dog的子类SimpleKVO_Dog类,并重写了父类的setName
方法,通过修改Dog类实例isa指针的指向,来调用子类的setName方法。子类的setName方法中又调用父类的setName方法以及通知了观察者属性改变。
2、带有block的实现
下面我们来通过runtime
动态生成子类,并实现带有block回调的方法,我们仍以Dog类为例。下面我们通过代码进一步去讲解:
首先先给出两个工具方法(getter方法名和setter方法名的相互转化):
//根据getter方法名返回setter方法名 name -> Name -> setName:
- (NSString *)setterForGetter:(NSString *)key
{
// 1. 首字母转换成大写
unichar c = [key characterAtIndex:0];
NSString *str = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c-32]];
// 2. 最前增加set, 最后增加:
NSString *setter = [NSString stringWithFormat:@"set%@:", str];
return setter;
}
//根据setter方法名返回getter方法名 setName: -> Name -> name
- (NSString *)getterForSetter:(NSString *)key
{
// 1. 去掉set
NSRange range = [key rangeOfString:@"set"];
NSString *subStr1 = [key substringFromIndex:range.location + range.length];
// 2. 首字母转换成大写
unichar c = [subStr1 characterAtIndex:0];
NSString *subStr2 = [subStr1 stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c+32]];
// 3. 去掉最后的:
NSRange range2 = [subStr2 rangeOfString:@":"];
NSString *getter = [subStr2 substringToIndex:range2.location];
return getter;
}
下面我们来看具体逻辑实现:
添加观察者:ll_addObserver:key:callback:
步骤:
- 检查被观察对象对应的类有没有相应的 setter 方法,没有则return;
- 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;(这里还需要判断需要新建的子类是否已经创建过了,如果创建过了,则直接使用该子类);
- 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
- 将观察者、观察的key以及对应的block回调生成相应的字典保存到数组里;
-(void)ll_addObserver:(id)observer key:(NSString *)key callback:(LLKVOBlock)callback{
//1. 通过观察的key获得相应的setter方法
SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) return; //不存在setter方法直接return
//2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类
Class clazz = object_getClass(self);
NSString *className = NSStringFromClass(clazz);
if (![className hasPrefix:KVOPrefix]) {//当前类不是KVO类
clazz = [self ll_KVOClassWithOriginalClassName:className];
object_setClass(self, clazz);
}
//-------到这里self已经是KVO类了---------
// 3. 检查KVO类是否已重写父类的setter方法,如果没有则为KVO类添加setter方法的实现
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
};
// 4. 添加该观察者到观察者列表中
// 4.1 创建观察者相关信息字典(观察者对象、观察的key、block)
NSDictionary *infoDic = @{@"observer":observer,@"key":key,@"callback":callback};
// 4.2 获取关联对象(装着所有观察者的数组)
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, ObserverArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:infoDic];
}
创建子类的步骤:
- 判断要创建的子类是否已经创建,存在直接返回这个类,不存在则去创建;
-
重写子类的class方法,使其返回父类的Class;
(这步只是模仿系统KVO的实现,对业务逻辑没影响,可不实现)
// 动态创建子类的方法
-(Class)ll_KVOClassWithOriginalClassName:(NSString *)className
{
NSString *kvoClassName = [KVOPrefix stringByAppendingString:className];
Class kvoClass = NSClassFromString(kvoClassName);//如果类不存在这个方法返回的值为nil
// 如果kvo class存在则返回
if (kvoClass) {
return kvoClass;
}
// 如果kvo class不存在, 则创建这个类
Class originClass = object_getClass(self);
kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
// 修改kvo class方法的实现
Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClass, @selector(class), (IMP)ll_class, types);
// 注册kvo_class
objc_registerClassPair(kvoClass);
return kvoClass;
}
// 重写的class方法的IMP
static Class ll_class(id self, SEL cmd)
{
//模仿Apple的做法, 欺骗人们这个kvo类还是原类
return class_getSuperclass(object_getClass(self));
}
下面我们来看一下子类的setter方法的实现,这和简单实现的思路是一样的,同样是:
- 获取原来的值;
- 调用父类的setter方法;
- 通知观察者属性改变了(这里换成了block);
static void kvo_setter(id self, SEL _cmd, id newValue)
{
// 1. 获取旧值
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [self getterForSetter:setterName];
id oldValue = [self valueForKey:getterName];
// 2. 调用父类方法
struct objc_super superClazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
objc_msgSendSuper(&superClazz, _cmd, newValue);
// 3、获取观察者列表,遍历找出对应的观察者,执行响应的block
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
for (NSDictionary *info in observers) {
if ([info[@"key"] isEqualToString:getterName]) {
// gcd异步调用callback
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, newValue);
});
}
}
}
注:这里调用父类的方法通过id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
实现,第一个参数是一个指向objc_super结构体的指针, objc_super的定义如下
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
objc_super结构体包含两个成员,receiver表示某个类的实例,这里为self,super_class表示当前类的父类,这里为self的父类。(注:这里的self其实已经是KVO创建的子类类型了)我们这里通过class_getSuperclass(object_getClass(self))
方法获得;
到这里添加观察者的方法暂时差不多了,为什么说暂时呢因为还有些问题,在下面会提出。那么现在我们还需要添加移除观察者的方法:
-(void)ll_removeObserver:(id)observer key:(NSString *)key
{
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
if (!observers) return;
for (NSDictionary *info in observers) {
if([info[@"key"] isEqualToString:key]) {
[observers removeObject:info];
break;
}
}
// 如果观察者列表count为0,则修改kvo类的isa指针,指向原来的类
if (observers.count == 0) {
Class clazz = object_getClass(self);
NSString *className = NSStringFromClass(clazz);
Class oriClass =NSClassFromString([className substringFromIndex:KVOPrefix.length]);
object_setClass(self, oriClass);
}
}
值得注意的是,当所有观察者都移除后,修改isa指针使其指向原来的类。系统的KVO实现就是这么做的,我们可以简单的通过代码测试一下:
-(void)test1{
self.dog = [Dog new];
NSLog(@"%@",object_getClass(self.dog));
[self.dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@",object_getClass(self.dog));
[self.dog removeObserver:self forKeyPath:@"age"];
NSLog(@"%@",object_getClass(self.dog));
}
输出结果:Dog----NSKVONotifying_Dog----Dog
3、 存在的问题:
因为重写setter方法我们的实现static void kvo_setter(id self, SEL _cmd, id newValue){}
是 这样的,newValue是一个id类型,这就要求我们观察的属性必须是OC类的实例。通过尝试发现系统的KVO会将基本类型最终转换成NSNumber类型,再将新/旧值通过字典传递。但是OC对象我们可以通过id来统一表示,基本类型我们却无能为力。所以这里给出两种思路:
-
思路一:可以在添加观察者方法中的第3步
给kvo类重写setter方法
,我们通过判断参数类型,来添加不同setter的方法实现。类型的判断这里用到了@encode关键字,不明白的可以看这篇文章
// 3. 检查KVO类是否已重写父类的setter方法,如果没有则为KVO类添加setter方法的实现
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
// 获取参数类型
char *type = method_copyArgumentType(setterMethod, 2);
if (strcmp(type, "@") == 0) {//对象类型
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}else if (strcmp(type, @encode(long)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)long_setter, types);
}else if (strcmp(type, @encode(int)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)int_setter, types);
}else if (strcmp(type, @encode(float)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
}else if (strcmp(type, @encode(double)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
}else if (strcmp(type, @encode(BOOL)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)bool_setter, types);
}
};
但是这种思路的问题就是需要判断的类型太多,除对象类型外都需要实现不同的setter方法的IMP,而且代码内容大致相同,造成代码的重复。这里给出int类型的setter的方法IMP:
//int 类型
static void int_setter(id self, SEL _cmd, int newValue)
{
// 1. 检查getter方法是否存在
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [self getterForSetter:setterName];
if (!getterName) {
return;
}
// 2. 获取旧值
id oldValue = [self valueForKey:getterName];
// 3. 调用父类方法
struct objc_super superClazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
objc_msgSendSuper(&superClazz, _cmd, newValue);
// 4、获取观察者列表,遍历找出对应的观察者,执行响应的block
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
for (NSDictionary *info in observers) {
if ([info[@"key"] isEqualToString:getterName]) {
// gcd异步调用callback
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, [NSNumber numberWithInt:newValue]);
});
}
}
}
-
思路二:我们是否可以模仿系统KVC的实现通过
[dog setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
这样的形式,在给子类添加setter方法前,通过转换成NSNumer类型后,在实现setter方法呢。然并卵,我也没能实现。需请大神支援~~~
到这里本篇文章基本结束,文中所涉及代码都在这里
最后:喜欢我文章的可以多多点赞和关注,您的鼓励是我写作的动力。O(∩_∩)O~