iOS基於KVO實現響應式編程之完結篇

  最近一直在探索基於KVO實現響應式編程,之前也寫了兩篇相關的文章。《OC實現KVO監聽block方式響應事件》,《iOS 通過KVO實現響應式編程(一)》最近方案基本完善。這邊完整的和大家梳理一下。

需求梳理

一、監聽非數組對象的屬性變化
二、監聽數組數據的變化
1)監聽數組指針的變化
2)監聽數組元素的變化
a、監聽數組元素數量以及元素順序的變化
b、監聽數組數組元素對應的屬性的變化
能夠將上面所列的變化,以及詳細信息通過回調的形式告知使用者。

意義

  通過實現對數據變化的監聽,我們就可以實現基於數據驅動的UI交互,事件交互。網絡請求交互。邏輯更加的清晰,代碼更好的維護;同時更好的實現局部刷新,提高app的性能。對於大型業務複雜的app更加適合。

技術實現

  技術實現主要從以下幾個方面進行考慮和實現

較低學習成本

  爲了降低學習成本,這邊主要採取類似原生kvo添加監聽的形式,監聽的變化通過回調的形式觸發。示例如下:

/**
 添加keyPath監聽,有context
 
 @param observer 觀察者
 @param keyPath keyPath
 @param options options
 @param context context
 @param block 回調
 */
- (void)jk_addObserver:(__kindof NSObject *)observer
            forKeyPath:(NSString *)keyPath
               options:(NSKeyValueObservingOptions)options
               context:(nullable void *)context
             withBlock:(void(^)(NSDictionary *change, void *context))block;
避免干擾其他代碼

  實現監聽的真正的觀察者並不是從外部傳入的,而是以外部傳入的observer作爲標記的一個JKKVOObserver對象,避免攔截外部傳入的observer相關方法,造成項目中使用原生kvo產生問題。
  對於數組中添加,刪除,替換元素的方法,並且有直接的進行hook,而是在分類裏添加了如下方法:

- (void)kvo_addObject:(id)anObject;
- (void)kvo_insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)kvo_removeLastObject;
- (void)kvo_removeObjectAtIndex:(NSUInteger)index;
- (void)kvo_replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;

- (void)kvo_addObjectsFromArray:(NSArray<id> *)otherArray;
- (void)kvo_exchangeObjectAtIndex:(NSUInteger)idx1 withObjectAtIndex:(NSUInteger)idx2;
- (void)kvo_removeAllObjects;
- (void)kvo_removeObject:(id)anObject;

內部實現監聽相關的操作,避免干擾到原生的方法,造成性能問題 PS:添加監聽後,使用kvo前綴的方法操作數組裏的元素,就能夠捕獲到數組的變化

較低的升級維護成本

   關於監聽相關的方法,以及數組轉化響應式的操作已經完成,後續升級也只是底層方面的性能優化,開發使用的方法不會有太大的變化,因此後續升級維護的成本較低。

主要的類

JKKVOObserver
@interface JKKVOObserver : NSObject

@property (nonatomic, weak, nullable, readonly) __kindof NSObject *originObserver;
@property (nonatomic, copy, nullable, readonly) NSString *originObserver_address;
@property (nonatomic, assign) NSUInteger observerCount;

+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;

+ (instancetype)initWithOriginObserver:(__kindof NSObject *)originObserver;

@end

JKKVOHelper 框架真正的觀察者。originObserver 開發者外部傳入的觀察者,originObserver_address開發者外部傳入的觀察者的內存地址。
內部實現如下:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       Class class = [JKKVOObserver class];
        SEL observeValueForKeyPath = @selector(observeValueForKeyPath:ofObject:change:context:);
        SEL jk_ObserveValueForKeyPath = @selector(jkhook_observeValueForKeyPath:ofObject:change:context:);
        [JKKVOItemManager jk_exchangeInstanceMethod:class originalSel:observeValueForKeyPath swizzledSel:jk_ObserveValueForKeyPath];
    });
}

+ (instancetype)initWithOriginObserver:(__kindof NSObject *)originObserver
{
    JKKVOObserver *kvoObserver = [[self alloc] init];
    if (kvoObserver) {
        kvoObserver.originObserver = originObserver;
        kvoObserver.originObserver_address = [NSString stringWithFormat:@"%p",originObserver];
        kvoObserver.observerCount = 1;
    }
    return kvoObserver;
}

- (void)jkhook_observeValueForKeyPath:(NSString *)keyPath
                             ofObject:(id)object
                               change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                              context:(void *)context
{
    if ([object isKindOfClass:[NSObject class]]) {
        JKKVOItem *item = [JKKVOItemManager isContainItemWith_kvoObserver:self];
        if (!item
            || !item.valid) {
            return;
        }
        
        if ([item isKindOfClass:[JKKVOArrayItem class]]) {
            JKKVOArrayItem *arrayItem = (JKKVOArrayItem *)item;
            NSObject *observeredObject = (NSObject *)object;
            if ([arrayItem.observered isEqual:observeredObject]) { // 數組指針的變化
                arrayItem.observered_property = [observeredObject valueForKeyPath:keyPath];
                if (arrayItem.observered_property) {
                    NSAssert([arrayItem.observered_property isKindOfClass:[NSArray class]], @"make sure [arrayItem.observered_property isKindOfClass:[NSArray class]] be YES");
                }
                void(^detailBlock)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context) = arrayItem.detailBlock;
                if (detailBlock) {
                    detailBlock(keyPath,change,nil,context);
                }
            } else { // 數組元素對應的屬性的變化
               void(^detailBlock)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context) = arrayItem.detailBlock;
                if (detailBlock) {
                    NSArray <JKKVOArrayElement *>*kvoElements = [arrayItem kvoElementsWithElement:observeredObject];
                    JKKVOArrayChangeModel *changedModel = [JKKVOArrayChangeModel new];
                    changedModel.changeType = JKKVOArrayChangeTypeElement;
                    changedModel.changedElements = kvoElements;
                    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
                    if ((arrayItem.options & NSKeyValueObservingOptionOld) == NSKeyValueObservingOptionOld) {
                        dic[NSKeyValueChangeOldKey] = arrayItem.observered_property;
                    }
                    
                    if ((arrayItem.options & NSKeyValueObservingOptionNew) == NSKeyValueObservingOptionNew) {
                        dic[NSKeyValueChangeNewKey] = arrayItem.observered_property;
                    }
                    detailBlock(arrayItem.keyPath,dic,changedModel,arrayItem.context);
                }
            }
            
        } else {
            void(^block)(NSString *keyPath, NSDictionary *change, void *context) = item.block;
            if (block) {
                block(keyPath,change,context);
            }
        }
        
    }
}

可以看到對observeValueForKeyPath:ofObject:change:context:這個方法進行了攔截,不會干擾到外部的類,通過回調的形式,將變化傳遞到業務層。

JKKVOItem
@interface JKKVOItem : NSObject

/// 觀察者
@property (nonatomic, strong, nonnull, readonly) JKKVOObserver *kvoObserver;
/// 被觀察者
@property (nonatomic, weak, nullable, readonly) __kindof NSObject *observered;
///被觀察者的內存地址
@property (nonatomic, copy, nullable, readonly) NSString *observered_address;
/// 監聽的keyPath
@property (nonatomic, copy, nonnull, readonly) NSString *keyPath;
/// 上下文
@property (nonatomic, nullable, readonly) void *context;
/// 回調
@property (nonatomic, copy, readonly) void(^block)(NSString *keyPath, NSDictionary *change, void *context);
/// 是否有效
@property (nonatomic, assign, readonly) BOOL valid;

+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;

/// 非數組的監聽
+ (instancetype)initWith_kvoObserver:(nonnull JKKVOObserver *)kvoObserver
                          observered:(nonnull __kindof NSObject *)observered
                             keyPath:(nonnull NSString *)keyPath
                             context:(nullable void *)context
                               block:(nullable void(^)(NSString *keyPath,  NSDictionary *change, void *context))block;

@end

每一個非數組對象添加一個監聽,就會創建一個JKKVOItem對象,被監聽對象釋放時,自動移除這個JKKVOItem對象。這些創建的JKKVOItem對象存儲在一個全局數組中。

JKKVOArrayItem

如果一個對象監聽的屬性是一個數組,那麼就會創建JKKVOArrayItem,JKKVOArrayItem繼承自JKKVOItem。一個JKKVOArrayItem對象完整的記錄了一個一個數組變化的詳細信息

@interface JKKVOArrayItem : JKKVOItem

/// 被監聽的屬性對應的對象
@property (nonatomic, weak, nullable, readonly) __kindof NSArray *observered_property;
///監聽選項
@property (nonatomic, assign, readonly) NSKeyValueObservingOptions options;
/// 數組元素需要監聽的keyPath的數組
@property (nonatomic, strong, nullable, readonly) NSArray *elementKeyPaths;

/// 被監聽的元素map   key:element   value: 添加監聽的次數
@property (nonatomic, strong, nonnull, readonly) NSMapTable *observered_elementMap;
/// 回調
@property (nonatomic, copy, readonly) void(^detailBlock)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context);

+ (instancetype)initWith_kvoObserver:(nonnull JKKVOObserver *)kvoObserver
                          observered:(nonnull __kindof NSObject *)observered
                             keyPath:(nonnull NSString *)keyPath
                             context:(nullable void *)context
                             options:(NSKeyValueObservingOptions)options
                 observered_property:(nullable __kindof NSObject *)observered_property
                     elementKeyPaths:(nullable NSArray *)elementKeyPaths
                         detailBlock:(nullable void(^)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context))detailBlock;

/// 爲數組中的元素添加指定的監聽
/// @param element 數組元素
- (void)addObserverOfElement:(nonnull __kindof NSObject *)element;

/// 爲數組中的元素移除指定的監聽
/// @param element 數組元素
- (void)removeObserverOfElement:(nonnull __kindof NSObject *)element;

- (nullable NSArray <JKKVOArrayElement *>*)kvoElementsWithElement:(nonnull __kindof NSObject *)element;

@end

下面針對JKKVOArrayItem中幾個屬性進行詳細的說明。

@property (nonatomic, weak, nullable, readonly) __kindof NSArray *observered_property;

observered_property 是被監聽對象的屬性只能是數組類型,如果observered_property 不爲空,是非數組類型的話會直接觸發斷言崩潰掉

@property (nonatomic, strong, nullable, readonly) NSArray *elementKeyPaths;

這個是數組內的元素被監聽的keyPath組成的數組,如果爲nil那麼數組內元素的變化不會觸發回調,如果不爲空的話,數組內元素會添加相應的監聽。

/// 回調
@property (nonatomic, copy, readonly) void(^detailBlock)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context);

keyPath是數組作爲某一個對象的屬性的keyPath,change 裏面對應的是數組變化前後的指針。
changedModel是數組元素數量變化的詳細信息

JKKVOArrayChangeType
typedef NS_ENUM(NSInteger,JKKVOArrayChangeType) {
    /// 缺省值 沒有任何改變
    JKKVOArrayChangeTypeNone = 0,
    /// 根據index增加元素
    JKKVOArrayChangeTypeAddAtIndex,
    /// 尾部增加元素
    JKKVOArrayChangeTypeAddTail,
    /// 根據index移除元素
    JKKVOArrayChangeTypeRemoveAtIndex,
    /// 移除尾部元素
    JKKVOArrayChangeTypeRemoveTail,
    /// 替換元素
    JKKVOArrayChangeTypeReplace,
    /// 元素內容改變,指針不變
    JKKVOArrayChangeTypeElement,
};

JKKVOArrayChangeType 是數組內元素變化的類型

JKKVOArrayElement
@interface JKKVOArrayElement : NSObject

@property (nonatomic, strong, nonnull, readonly) NSObject *object;

@property (nonatomic, assign, readonly) NSInteger oldIndex;

@property (nonatomic, assign, readonly) NSInteger newIndex;

+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)elementWithObject:(__kindof NSObject *)object
                         oldIndex:(NSInteger)oldIndex
                         newIndex:(NSInteger)newIndex;

@end

這個類主要用來存貯數組元素變化的詳細信息的,object就是對應的數組元素,oldIndex是數組元素變化前的索引,newIndex是數組元素變化後的索引

`

遇到的坑點

待優化項

單元測試例子

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章