你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 一句代碼,更加優雅的調用KVO和通知

一句代碼,更加優雅的調用KVO和通知

編輯:IOS開發基礎

QQ截圖20160727105756.jpg

授權轉載,作者: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!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved