
授權轉載,作者:wazrx
寫在前面
每次使用KVO和通知我就覺得是一件麻煩的事情,即便談不上麻煩,也可說是不方便吧,對於KVO,你需要注冊,然後實現監聽方法,最後還要移除,通知當然也需要移除操作,這使得相關邏輯的代碼過於分散,控制器搞得亂亂的,而且總有時候會忘記移除什麼的,總之感覺不太好,所以我想如果能有方法添加一個KVO或者通知後能夠省略後面移除或者實現監聽方法步驟的話會多好,所以我就嘗試寫了一個分類,這個分類的目的在於盡可能簡化KVO和通知的步驟,對於KVO,你只需要一句代碼就可完成監聽,無需自己手動移除,通知也差不多,接口如下:
/** * 通過Block方式注冊一個KVO,通過該方式注冊的KVO無需手動移除,其會在被監聽對象銷毀的時候自動移除 * * @param keyPath 監聽路徑 * @param block KVO回調block,obj為監聽對象,oldVal為舊值,newVal為新值 */ - (void)xw_addObserverBlockForKeyPath:(NSString*)keyPath block:(void (^)(id obj, id oldVal, id newVal))block; /** * 通過block方式注冊通知,通過該方式注冊的通知無需手動移除,同樣會自動移除 * * @param name 通知名 * @param block 通知的回調Block,notification為回調的通知對象 */ - (void)xw_addNotificationForName:(NSString *)name block:(void (^)(NSNotification *notification))block;
使用也很簡單咯,github地址如下:XWEasyKVONotification,你只需要導入NSObject+XWAdd這個分類,然後調用上面兩個接口即可完成KVO和通知,事例代碼如下
//監聽_objA的name屬性
[_objA xw_addObserverBlockForKeyPath:@"name" block:^(id obj, id oldVal, id newVal) {
NSLog(@"kvo,修改name為%@", newVal);
}];
[self xw_addNotificationForName:@"XWTestNotificaton" block:^(NSNotification *notification) {
NSLog(@"收到通知:%@", notification.userInfo);
}];是不是非常簡單,再也不用關心忘記移除導致的崩潰了,而且代碼也集中,看著也更舒服了
原理
1、由於KVO和通知都差不多,原理部分通過KVO的接口的的實現原理進行說明,考慮到代碼的統一我首先考慮到使用block,同時為了block能回調,我們需要一個內部的對象target的來實現KVO的代碼,在監聽到值改變的時候通過這個對象來回調block,同時一個target應該對應一個keyPath,並且可應該對應多個Block,因為我們可能對一個keyPath進行多處監聽,這個類的具體代碼大致如下:
@interface _XWBlockTarget : NSObject
/**添加一個KVOblock*/
- (void)xw_addBlock:(void(^)(__weak id obj, id oldValue, id newValue))block;
@end
@implementation _XWBlockTarget{
//保存所有的KVOblock
NSMutableSet *_blockSet;
}
- (instancetype)init
{
self = [super init];
if (self) {
_blockSet = [NSMutableSet new];
}
return self;
}
- (void)xw_addBlock:(void(^)(__weak id obj, id oldValue, id newValue))block{
[_blockSet addObject:[block copy]];
}
//KVO的真正實現
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (!_blockSet.count) return;
BOOL prior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
//只接受值改變時的消息
if (prior) return;
NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
if (changeKind != NSKeyValueChangeSetting) return;
id oldVal = [change objectForKey:NSKeyValueChangeOldKey];
if (oldVal == [NSNull null]) oldVal = nil;
id newVal = [change objectForKey:NSKeyValueChangeNewKey];
if (newVal == [NSNull null]) newVal = nil;
//當KVO觸發,值改變的時候執行該target下的所有block
[_blockSet enumerateObjectsUsingBlock:^(void (^block)(__weak id obj, id oldVal, id newVal), BOOL * _Nonnull stop) {
block(object, oldVal, newVal);
}];
}
@end2、實際進行KVO的監聽的對象有了,我們就可以開始書寫邏輯了,我們給每一個對象綁定一個targets的字典,每次調用該API注冊KVO的就去判斷有沒有對應的keyPath下的target(target和keyPath一一對應),沒有就創建,同時注冊這個keyPath的KVO,有就把block加入這個target以便回調,具體代碼如下:
- (void)xw_addObserverBlockForKeyPath:(NSString*)keyPath block:(void (^)(id obj, id oldVal, id newVal))block {
if (!keyPath || !block) return;
//取出存有所有KVOTarget的字典
NSMutableDictionary *allTargets = objc_getAssociatedObject(self, XWKVOBlockKey);
if (!allTargets) {
//沒有則創建
allTargets = [NSMutableDictionary new];
//綁定在該對象中
objc_setAssociatedObject(self, XWKVOBlockKey, allTargets, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//獲取對應keyPath中的所有target
_XWBlockTarget *targetForKeyPath = allTargets[keyPath];
if (!targetForKeyPath) {
//沒有則創建
targetForKeyPath = [_XWBlockTarget new];
//保存
allTargets[keyPath] = targetForKeyPath;
//如果第一次,則注冊對keyPath的KVO監聽
[self addObserver:targetForKeyPath forKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}
[targetForKeyPath xw_addBlock:block];
//對第一次注冊KVO的類進行dealloc方法調劑
[self _xw_swizzleDealloc];
}3、上一段代碼的最後一個方法是對dealloc方法進行調劑,因為我們想要能夠在合適的時候自動注銷KVO,何為合適的地方呢,當然是被監聽對象銷毀的時候才是最合適的地方,所以dealloc方法裡面是最合適的地方,我們期望能交換被監聽對象的dealloc方法然後自己在該方法中實現注銷KVO的邏輯,最先能想到的方式是通常我們使用的runtime中的swizzle黑魔法直接進行方法交換,但遺憾的是swizzle黑魔法只能在本類中交換本類的方法,而無法在一個類中對另一個類的方法進行調劑,所以需要另想調劑方法,我們采取直接對變監聽對象所在的類修改或者添加dealloc方法來達到調劑目的,我結合代碼進行說明:
/**
* 調劑dealloc方法,由於無法直接使用運行時的swizzle方法對dealloc方法進行調劑,所以稍微麻煩一些
*/
- (void)_xw_swizzleDealloc{
//我們給每個類綁定上一個值來判斷dealloc方法是否被調劑過,因為一個類只需要調劑一次,如果調劑過了就無需再次調劑了
BOOL swizzled = [objc_getAssociatedObject(self.class, deallocHasSwizzledKey) boolValue];
//如果調劑過則直接返回
if (swizzled) return;
//開始調劑
Class swizzleClass = self.class;
//獲取原有的dealloc方法
SEL deallocSelector = sel_registerName("dealloc");
//初始化一個函數指針用於保存原有的dealloc方法
__block void (*originalDealloc)(__unsafe_unretained id, SEL) = NULL;
//實現我們自己的dealloc方法,通過block的方式
id newDealloc = ^(__unsafe_unretained id objSelf){
//在這裡我們移除所有的KVO
[objSelf xw_removeAllObserverBlocks];
//根據原有的dealloc方法是否存在進行判斷
if (originalDealloc == NULL) {//如果不存在,說明本類沒有實現dealloc方法,則需要向父類發送dealloc消息(objc_msgSendSuper)
//構造objc_msgSendSuper所需要的參數,.receiver為方法的實際調用者,即為類本身,.super_class指向其父類
struct objc_super superInfo = {
.receiver = objSelf,
.super_class = class_getSuperclass(swizzleClass)
};
//構建objc_msgSendSuper函數
void (*msgSend)(struct objc_super *, SEL) = (__typeof__(msgSend))objc_msgSendSuper;
//向super發送dealloc消息
msgSend(&superInfo, deallocSelector);
}else{//如果存在,表明該類實現了dealloc方法,則直接調用即可
//調用原有的dealloc方法
originalDealloc(objSelf, deallocSelector);
}
};
//根據block構建新的dealloc實現IMP
IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);
//嘗試添加新的dealloc方法,如果該類已經復寫的dealloc方法則不能添加成功,反之則能夠添加成功
if (!class_addMethod(swizzleClass, deallocSelector, newDeallocIMP, "v@:")) {
//如果沒有添加成功則保存原有的dealloc方法,用於新的dealloc方法中,執行原有的系統的dealloc邏輯
Method deallocMethod = class_getInstanceMethod(swizzleClass, deallocSelector);
originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_getImplementation(deallocMethod);
originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_setImplementation(deallocMethod, newDeallocIMP);
}
//標記該類已經調劑過了
objc_setAssociatedObject(self.class, deallocHasSwizzledKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/**移除所有的KVO*/
- (void)xw_removeAllObserverBlocks {
NSMutableDictionary *allTargets = objc_getAssociatedObject(self, XWKVOBlockKey);
if (!allTargets) return;
[allTargets enumerateKeysAndObjectsUsingBlock:^(id key, _XWBlockTarget *target, BOOL *stop) {
[self removeObserver:target forKeyPath:key];
}];
[allTargets removeAllObjects];
}通過如上方式,我們就完成了對dealloc方法的調劑,新的dealloc方法執行的時候回注銷注冊的KVO,這樣就免去了手動注銷的麻煩事情咯!
寫在最後
通知的大致實現方式和KVO一樣,詳情請自行查看代碼咯,我就不多做說明了,現在終於能優雅愉快的使用KVO和通知了,復習一下github地址:XWEasyKVONotification,如果覺得對您有幫助,歡迎star!