寫在前面
每次使用KVO和通知我就覺得是一件麻煩的事情,即便談不上麻煩,也可說是不方便吧,對於KVO,你需要註冊,然後實現監聽方法,最後還要移除,通知當然也需要移除操作,這使得相關邏輯的程式碼過於分散,控制器搞得亂亂的,而且總有時候會忘記移除什麼的,總之感覺不太好,所以我想如果能有方法新增一個KVO或者通知後能夠省略後面移除或者實現監聽方法步驟的話會多好,所以我就嘗試寫了一個分類,這個分類的目的在於儘可能簡化KVO和通知的步驟,對於KVO,你只需要一句程式碼就可完成監聽,無需自己手動移除,通知也差不多,介面如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * 通過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和通知,事例程式碼如下
1 2 3 4 5 6 7 |
//監聽_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進行多處監聽,這個類的具體程式碼大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
@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以便回撥,具體程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
- (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方法來達到調劑目的,我結合程式碼進行說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
/** * 調劑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!