iOS底层探索之KVO—自定义KVO
Posted 卡卡西Sensei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之KVO—自定义KVO相关的知识,希望对你有一定的参考价值。
回顾
在上篇博客已经自定义了KVO
,但是还没有完善,还有些问题需要解决,这么本篇博客就把自定义KVO
进行完善。
1. 观察者信息保存问题
在上一篇的博客中,自定义KVO
的简单逻辑是已经实现了,但是这里还是存在一个大的问题,就是如果我们要观察多个属性的时候,以及新值和旧值,都要观察以及传递了context
的情况下就无效了。
解决的办法就是,我们需要保存观察者相关的信息,那么就创建一个新类JPKVOInfo
保存,代码的实现如下:
typedef NS_OPTIONS(NSUInteger, JPKeyValueObservingOptions) {
JPKeyValueObservingOptionNew = 0x01,
JPKeyValueObservingOptionOld = 0x02,
};
@interface JPKVOInfo : NSObject
@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) JPKeyValueObservingOptions options;
@property (nonatomic, strong) id context;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JPKeyValueObservingOptions)options context:(nullable void *)context;
@end
@implementation JPKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JPKeyValueObservingOptions)options context:(nullable void *)context {
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
self.context = (__bridge id _Nonnull)(context);
}
return self;
}
@end
注意
:
observer
的修饰要使用weak
,弱引用,防止循环引用问题
那么在jp_addObserver
中信息需要保存观察者信息,如下代码:
JPKVOInfo *info = [[JPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
if (!observerArr) {
observerArr = [NSMutableArray arrayWithCapacity:1];
[observerArr addObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
jp_setter方法里面的逻辑修改之后如下代码:
//1.通知观察者,先拿到观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kjPKVOAssiociateKey));
for (JPKVOInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
//对新旧值进行处理
if (info.options & jPKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & jPKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
} else {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
[change setObject:@1 forKey:@"kind"];
//消息发送给观察者
[info.observer jp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
});
}
}
- 在调用父类之前得先获取旧的值。
- 取出关联对象数组数据,循环判断调用。
jp_observeValueForKeyPath
通知观察者。
这个时候观察多个属性以及多次观察就都没问题了。
2. 移除观察者
- jp_removeObserver
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[self jp_removeObserver:observer forKeyPath:keyPath context:NULL];
}
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}
NSMutableArray *tempArray = [observerArray mutableCopy];
for (JPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
} else {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
- 通过
keyPath
以及observer
、context
确定要移除的关联对象数据。 - 当关联对象中没有数据的时候
isa
进行指回。
3. 函数式编程KVO
3.1 注册与回调绑定
我们上面已经完成了KVO
的自定义了,这和系统的KVO
的实现都有一个问题,都需要三步曲。jp_addObserver
和 jp_observeValueForKeyPath
都得分开写,代码分散,逻辑代码和业务代码分太开了,我们能不能用函数式编程思想,将他们放在一起呢?那么试着去实现一下。
先为
KVO
分类添加一个block
,并且在addObserver
的函数里面添加上这个block
,这样注册和回调就可以在一起处理了。
- 修改注册方法为
block
实现:
typedef void(^JPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
......此处省略....
//保存观察者信息-数组
JPKVOBlockInfo *kvoInfo = [[JPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
......此处省略.......
}
将block
实现也保存在JPKVOBlockInfo
中,这样在回调的时候直接执行block
实现就可以了。
- 回调逻辑:
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPBlockKVOAssiociateKey));
for (JPKVOBlockInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
info.handleBlock(info.observer, keyPath, oldValue, newValue);
});
}
}
在回调的时候直接将新值与旧值一起返回。
- 注册调用逻辑:
[self.obj jp_addObserver:self forKeyPath:@"name" block:^(id _Nonnull observer, NSString * _Nonnull keyPath, id _Nonnull oldValue, id _Nonnull newValue) {
NSLog(@"block: oldValue:%@,newValue:%@",oldValue,newValue);
}];
3.2 KVO自动销毁
即使我们已经实现了注册和回调绑定,但是在观察者dealloc
的时候仍然需要remove
。
那么怎么能自动释放不需要主动调用呢?
在removeObserver
的过程中主要做了两件事,移除关联对象数组中的数据以及指回isa
。关联对象不移除的后果是会继续调用回调,那么在调用的时候判断下observer
存不存在,这样来处理是否回调就可以了?核心就在指回isa
了。
对dealloc方法进行Hook
+ (void)jp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
if (!cls) {
NSLog(@"class is nil");
return;
}
if (!swizzledSEL) {
NSLog(@"swizzledSEL is nil");
return;
}
//类/元类
Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
if (!oriMethod) {//原始方法没有实现
// 在oriMethod为nil时,替换后将swizzledSEL复制一个空实现
class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
//添加一个空的实现
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"imp default null implementation");
}));
}
//自己没有则会添加成功,自己有添加失败
BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {//自己没有方法添加一个,添加成功则证明自己没有。
class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { //自己有直接进行交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
+ (void)load {
[self jp_methodSwizzleWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
- (void)jp_dealloc {
// [self.obj jp_removeObserver:self forKeyPath:@""];
[self jp_dealloc];
}
- 在
jp_dealloc
中调用jp_removeObserver
移除观察者。 - 但是我们发现有个问题是:被观察者和
keypath
从哪里来?这里相当于是观察者的dealloc
中调用。 - 所以可以通过在注册的时候对观察者添加关联对象,保存被观察者和
keyPath:
static NSString *const kJPBlockKVOObserverdAssiociateKey = @"JPKVOObserverdAssiociateKey";
@interface JPKVOObservedInfo : NSObject
@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;
@end
@implementation JPKVOObservedInfo
- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
}
return self;
}
@end
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
……
//保存被观察者信息
JPKVOObservedInfo *kvoObservedInfo = [[JPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- 实例化的
kvoObservedInfo
中保存的是self
,也就是被观察者。 - 关联对象关联在observer也就是观察者身上。
那么现在在dealloc
中遍历对其进行移除操作:
- (void)jp_dealloc {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey));
for (JPKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
[info.observerd jp_removeObserver:self forKeyPath:info.keyPath];
}
}
[self jp_dealloc];
}
我们这里的方法的执行,只是针对被观察者没有释放的情况,如果释放了observerd
就不存在的情况下,我们是不需要调用remove
处理的。
Hook的优化
在上面的做法中,有不合理的问题存在:就是在+ load
中Hook dealloc
方法是在NSObject
分类中处理的,那么意味着所有的类的dealloc
方法都被Hook了。
那么我们如何改进优化下
Hook
呢?
解决办法就是:只针对类进行Hook dealloc
方法,所以可以将Hook
延迟到addObserver
中:
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
……
//hook dealloc
[[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
但是对dealloc hook
我们只能够hook
一次,否则又交换回来了。
所以要么做标记,要么在创建kvo
子类的时候进行hoo
k。显然在创建子类的时候更合适。代码如下所示:
//申请类-注册类-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJPBlockKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)_jp_class, classTypes);
//hook dealloc
[[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)_jp_setter, setterTypes);
return newClass;
}
3.3 Hook注册和移除方法
对添加和移除的3个方法进行Hook
处理:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self jp_methodSwizzleWithClass:self oriSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(jp_addObserver:forKeyPath:options:context:) isClassMethod:NO];
[self jp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(jp_removeObserver:forKeyPath:context:)isClassMethod:NO];
[self jp_methodSw以上是关于iOS底层探索之KVO—自定义KVO的主要内容,如果未能解决你的问题,请参考以下文章