回顧
在上篇部落格已經自定義了
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_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(jp_removeObserver:forKeyPath:)isClassMethod:NO];
});
}
因為
removeObserver:forKeyPath:
底層調用的不是
removeObserver:forKeyPath:context:
是以兩個方法都要
Hook
。
那麼我們又如何去怎麼判斷
observer
對應的
keyPath
是否存在。由于
observationInfo
存儲的是私有類,那麼可以直接通過
kvc
擷取值:
- (BOOL)keyPathIsExist:(NSString *)sarchKeyPath observer:(id)observer {
BOOL findKey = NO;
id info = self.observationInfo;
if (info) {
NSArray *observances = [info valueForKeyPath:@"_observances"];
for (id observance in observances) {
id tempObserver = [observance valueForKey:@"_observer"];
if (tempObserver == observer) {
NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
if ([keyPath isEqualToString:sarchKeyPath]) {
findKey = YES;
break;
}
}
}
}
return findKey;
}
Hook
方法的具體實作如下:
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 觀察者已經添加了對應key的觀察,再次添加不做處理。
return;
}
[self jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self jp_removeObserver:observer forKeyPath:keyPath context:context];
}
}
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self jp_removeObserver:observer forKeyPath:keyPath];
}
}
這樣就解決了重複添加和移除的問題。
3.4 自動移除觀察者
重複添加和移除的問題已經解決了,那麼還有一個問題是
dealloc
的時候自動移除。這塊思路與自定義
kvo
相同,可以通過
Hook
觀察者的的
dealloc
實作。
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 觀察者已經添加了對應key的觀察,再次添加不做處理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//類不存在的時候進行 hook 觀察者 dealloc
//hook dealloc
[[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
[self jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
- (void)jp_dealloc {
[self jp_removeSelfAllObserverd];
[self jp_dealloc];
}
如果
kvo
子類已經存在的時候,那麼說明已經
hook
過了。
在
dealloc
中
self.observationInfo
是擷取不到資訊的,
observationInfo
是存儲在被觀察者中的。是以還需要我們自己存儲下資訊。
static NSString *const kJPSafeKVOObserverdAssiociateKey = @"JPSafeKVOObserverdAssiociateKey";
@interface JPSafeKVOObservedInfo : NSObject
@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, strong) id context;
@end
@implementation JPSafeKVOObservedInfo
- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
_context = (__bridge id)(context);
}
return self;
}
@end
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 觀察者已經添加了對應key的觀察,再次添加不做處理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//類不存在的時候進行 hook 觀察者 dealloc
//hook dealloc
[[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
//儲存被觀察者資訊
JPSafeKVOObservedInfo *kvoObservedInfo = [[JPSafeKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath context:context];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPSafeKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPSafeKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//調用原始方法
[self Jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
那麼我們現在就可以在
jp_dealloc
中主動的去調用移除
- (void)jp_dealloc {
[self jp_removeSelfAllObserverd];
[self jp_dealloc];
}
- (void)jp_removeSelfAllObserverd {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kjPSafeKVOObserverdAssiociateKey));
for (jPSafeKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
//調用系統方法,已經hook了,走hook邏輯。
if (info.context) {
[info.observerd removeObserver:self forKeyPath:info.keyPath context:(__bridge void * _Nullable)(info.context)];
} else {
[info.observerd removeObserver:self forKeyPath:info.keyPath];
}
}
}
}
這樣的話就會在
dealloc
的時候,就會自己主動清空,已經釋放掉的
observer
觀察者了。
4. 總結
- 觀察多個屬性的時候,以及新值和舊值,都要觀察以及傳遞了
的情況下就無效,需要儲存觀察者相關的資訊,就可以建立一個新類context
儲存JPKVOInfo
- 建立了觀察者就需要去移除觀察者
- 可以通過
觀察者的的Hook
方法,實作自動移除。dealloc
更多内容持續更新
🌹 喜歡就點個贊吧👍🌹
🌹 覺得有收獲的,可以來一波,收藏+關注,評論 + 轉發,以免你下次找不到我😁🌹
🌹歡迎大家留言交流,批評指正,互相學習😁,提升自我🌹