授權轉載,作者: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); }]; } @end
2、實際進行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!