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 *)observerforKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextwithBlock:(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
+ (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 *)keyPathofObject:(id)objectchange:(NSDictionary<NSKeyValueChangeKey,id> *)changecontext:(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 *)kvoObserverobservered:(nonnull __kindof NSObject *)observeredkeyPath:(nonnull NSString *)keyPathcontext:(nullable void *)contextblock:(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 *)kvoObserverobservered:(nonnull __kindof NSObject *)observeredkeyPath:(nonnull NSString *)keyPathcontext:(nullable void *)contextoptions:(NSKeyValueObservingOptions)optionsobservered_property:(nullable __kindof NSObject *)observered_propertyelementKeyPaths:(nullable NSArray *)elementKeyPathsdetailBlock:(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 *)objectoldIndex:(NSInteger)oldIndexnewIndex:(NSInteger)newIndex;@end
这个类主要用来存贮数组元素变化的详细信息的,object就是对应的数组元素,oldIndex是数组元素变化前的索引,newIndex是数组元素变化后的索引
JKKVOArrayChangeModel
@interface JKKVOArrayChangeModel : NSObject@property (nonatomic, assign) JKKVOArrayChangeType changeType;@property (nonatomic, strong) NSArray <JKKVOArrayElement *>*changedElements;@end
这个类主要是用在监听变化的block时返回值,用来记录数组元素变化的详细信息的。changeType表示这次变化的类型,changedElements表示监听到的这次变化有哪些元素直接发生了改变。
使用教程
import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface NSObject (JKKVOHelper)/**添加keyPath监听@param observer 观察者@param keyPath keyPath@param options options@param block 回调*/- (void)jk_addObserver:(__kindof NSObject *)observerforKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionswithBlock:(void(^)(NSDictionary *change, void *context))block;/**添加keyPath监听,有context@param observer 观察者@param keyPath keyPath@param options options@param context context@param block 回调*/- (void)jk_addObserver:(__kindof NSObject *)observerforKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextwithBlock:(void(^)(NSDictionary *change, void *context))block;/// 添加一组keyPath监听,有context/// @param observer 观察者/// @param keyPaths keyPath数组/// @param options options/// @param context context/// @param detailBlock 回调- (void)jk_addObserver:(__kindof NSObject *)observerforKeyPaths:(NSArray <NSString *>*)keyPathsoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextwithDetailBlock:(void(^)(NSString *keyPath, NSDictionary *change, void *context))detailBlock;/**添加keyPath监听,观察者是自己@param keyPath keyPath@param options options@param block 回调*/- (void)jk_addObserverForKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionswithBlock:(void(^)(NSDictionary *change, void *context))block;/**添加keyPath监听,观察者是自己,有context@param keyPath keyPath@param options options@param context context@param block 回调*/- (void)jk_addObserverForKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextwithBlock:(void(^)(NSDictionary *change, void *context))block;/// 添加一组keyPath监听,观察者是自己,有context/// @param keyPaths keyPath数组/// @param options options/// @param context context/// @param detailBlock 回调- (void)jk_addObserverForKeyPaths:(NSArray <NSString *>*)keyPathsoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextwithDetailBlock:(void(^)(NSString *keyPath, NSDictionary *change, void *context))detailBlock;/// 监听数组的变化,不监听数组元素属性的变化/// @param keyPath keyPath,keyPath对应的属性是数组/// @param options options/// @param context context/// @param block block- (void)jk_addObserverOfArrayForKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextwithBlock:(void (^)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context))block;/// 监听数组的变化,同时监听数组元素属性的变化,观察者是自己/// @param keyPath keyPath,keyPath对应的属性是数组/// @param options options/// @param context context/// @param elementKeyPaths elementKeyPaths 数组的元素对应的keypath组成的数组/// @param block block- (void)jk_addObserverOfArrayForKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextelementKeyPaths:(nullable NSArray <NSString *>*)elementKeyPathswithBlock:(void (^)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context))block;/// 监听数组的变化,同时监听数组元素属性的变化/// @param observer 观察者/// @param keyPath keyPath keyPath,keyPath对应的属性是数组/// @param options options/// @param context context/// @param elementKeyPaths elementKeyPaths 数组的元素对应的keypath组成的数组/// @param block block- (void)jk_addObserverOfArray:(__kindof NSObject *)observerkeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullable void *)contextelementKeyPaths:(nullable NSArray <NSString *>*)elementKeyPathswithBlock:(void (^)(NSString *keyPath, NSDictionary *change, JKKVOArrayChangeModel *changedModel, void *context))block;/**移除keyPath监听@param observer 观察者@param keyPath keyPath*/- (void)jk_removeObserver:(__kindof NSObject *)observerforKeyPath:(NSString *)keyPath;/// 移除keyPath监听,有context/// @param observer 观察者/// @param keyPath keyPath/// @param context context- (void)jk_removeObserver:(__kindof NSObject *)observerforKeyPath:(NSString *)keyPathcontext:(nullable void *)context;/**移除某个observer下的对应的keyPath列表监听@param observer 观察者@param keyPaths keyPath组成的数组*/- (void)jk_removeObserver:(__kindof NSObject *)observerforKeyPaths:(NSArray <NSString *>*)keyPaths;/**移除某个keyPath的obsevers对应的监听如果observers为nil,那么remove掉某个keyPath的所有obsevers对应的监听@param observers 观察者数组@param keyPath keyPath*/- (void)jk_removeObservers:(NSArray <__kindof NSObject *>*)observersforKeyPath:(NSString *)keyPath;/**移除所有通过jk_前缀添加的观察者,默认在被观察的对象dealloc的时候调用*/- (void)jk_removeObservers;/**所有的被监听的keyPath列表@return 被监听的keyPath组成的列表*/- (NSArray <NSString *>*)jk_observeredKeyPaths;/// 监听某一个属性的所有监听者的列表/// @param keyPath keyPath- (NSArray <NSObject *>*)jk_observersOfKeyPath:(NSString *)keyPath;/**某个观察者监听的keyPath组成的列表@param observer 观察者@return keyPath组成的列表*/- (NSArray <NSString *>*)jk_keyPathsObserveredBy:(__kindof NSObject *)observer;@endNS_ASSUME_NONNULL_END
遇到的坑点
内存陷阱问题
hook dealloc问题
框架中hook dealloc 的代码如下
- (void)jkhook_dealloc{if (![self is_jk_dealloced]) {[self setIs_jk_dealloced:YES];if ([self is_jk_observered]) {[self setIs_jk_observered:NO];[self jk_dealloc_removeObservers];[self jkhook_dealloc];} else {[self jkhook_dealloc];}}}
待优化项
由于这个框架底层使用了系统的KVO进行变化的监听,对于一个数组中元素的属性发生变化的情况,如果数组的元素数量过多,那么会添加过多监听,性能变差。后续会考虑自定义实现底层的监听。
单元测试例子
为了充分自测自己的框架,这边我写了一些单元测试样例,供大家参考,示例如下:
SPEC_BEGIN(JKKVOHelperSpec)describe(@"JKKVOHelper", ^{context(@"addObserver", ^{afterEach(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});it(@"addObserver", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];__block BOOL invoked1 = NO;[[[[change objectForKey:@] should] equal:@"zhangsan"];invoked1 = YES;}];worker.name = @"zhangsan";NSArray *array = [JKKVOItemManager items];[] haveCountOf:1];[] beYes];});it(@"A observe B, B observe A", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];[}];[}];NSArray *array = [JKKVOItemManager items];[] haveCountOf:2];});it(@"test observerKeyPaths", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];__block NSInteger invokedCout = 0;[] options:NSKeyValueObservingOptionNew context:nil withDetailBlock:^(NSString * _Nonnull keyPath, NSDictionary * _Nonnull change, void * _Nonnull context) {if([keyPath isEqualToString:@"name"]){[] should] equal:@"bbb"];} else if ([keyPath isEqualToString:@"factory"]) {JKFactory *factory = [change objectForKey:@"new"];[] equal:@"China"];}invokedCout++;}];worker.name = @"bbb";JKFactory *factory = [JKFactory new];factory.name = @"China";worker.factory = factory;[] equal:@(2)];});it(@"test observer and observered are the same object", ^{JKPersonModel *person = [JKPersonModel new];__block BOOL invoked1 = NO;[[[[change objectForKey:@] should] equal:@"zhangsan"];invoked1 = YES;}];person.name = @"zhangsan";NSArray *array = [JKKVOItemManager items];[] haveCountOf:1];[] beYes];});});context(@"singleInstance addObserver", ^{it(@"JKFactory", ^{JKFactory *factory = [JKFactory sharedInstance];JKWorker *worker = [JKWorker new];__block BOOL invoked1 = NO;[[[[change objectForKey:@] should] equal:@"北京"];invoked1 = YES;}];factory.name = @"北京";NSArray *array = [JKKVOItemManager items];[] haveCountOf:1];[] beYes];});afterAll(^{NSArray *array = [JKKVOItemManager items];[] haveCountOf:1];});});context(@"addObserver context", ^{beforeAll(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});afterEach(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});it(@"no context", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];__block BOOL invoked1 = NO;__block BOOL invoked2 = NO;[[[[change objectForKey:@] should] equal:@"zhangsan"];invoked1 = YES;}];[[[[change objectForKey:@] should] equal:@"zhangsan"];invoked2 = YES;}];worker.name = @"zhangsan";[] beYes];[] beNo];NSArray *array = [JKKVOItemManager items];[] haveCountOf:1];});it(@"has context", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];__block BOOL invoked1 = NO;__block BOOL invoked2 = NO;void *aaa = &aaa;[[[[change objectForKey:@] should] equal:@"zhangsan"];invoked1 = YES;}];void *bbb = &bbb;[context:bbb withBlock:^(NSDictionary * _Nonnull change, void * _Nonnull context) {[[[change objectForKey:@] should] equal:@"zhangsan"];invoked2 = YES;}];worker.name = @"zhangsan";[] beYes];[] beYes];NSArray *array = [JKKVOItemManager items];[] haveCountOf:2];});});context(@"object", ^{beforeAll(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});afterEach(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});it(@"jk_observeredKeyPaths", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];[}];[}];NSArray *keyPaths = [person jk_observeredKeyPaths];[] haveCountOf:2];});it(@"jk_observersOfKeyPath:1", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];[}];void *aaa = &aaa;[}];NSArray *observers = [person jk_observersOfKeyPath:@"name"];[] haveCountOf:1];});it(@"jk_observersOfKeyPath:2", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];JKWorker *worker1 = [JKWorker new];[}];[}];NSArray *observers = [person jk_observersOfKeyPath:@"name"];[] haveCountOf:2];});it(@"jk_keyPathsObserveredBy:", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];[}];[}];NSArray *keyPaths = [person jk_keyPathsObserveredBy:worker];[] haveCountOf:2];});});context(@"remove", ^{beforeAll(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});afterEach(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});it(@"jk_removeObserver:forKeyPath:", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];[}];[];NSArray *array = [JKKVOItemManager items];[]) should] equal:theValue(0)];});it(@"jk_removeObserver:forKeyPath:context:", ^{JKWorker *worker = [JKWorker new];JKPersonModel *person = [JKPersonModel new];[}];void *aaa = &aaa;[}];NSArray *array = [JKKVOItemManager items];[]) should] equal:theValue(2)];[];NSArray *array1 = [JKKVOItemManager items];[]) should] equal:theValue(1)];JKKVOItem *item = array1.firstObject;[] equal:@"name"];});it(@"jk_removeObserver:forKeyPaths:", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];[}];[}];NSArray *keyPaths = [person jk_observeredKeyPaths];[] haveCountOf:2];[];NSArray *array = [JKKVOItemManager items];[] haveCountOf:0];});it(@"jk_removeObservers:forKeyPath:", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];JKWorker *worker1 = [JKWorker new];[}];[}];NSArray *observers = [person jk_observersOfKeyPath:@"name"];[] haveCountOf:2];[];NSArray *array = [JKKVOItemManager items];[] haveCountOf:0];});it(@"for循环内快速创建对象", ^{NSMutableSet *set = [NSMutableSet new];for (NSInteger i = 0; i < 20; i++) {JKPersonModel *person = [JKPersonModel new];NSInteger itemCount = [JKKVOItemManager items].count;__block BOOL invoked = NO;[invoked = YES;}];person.name = [NSString stringWithFormat:@"name:%@",@(i)];[] beYes];[] equal:theValue([JKKVOItemManager items].count)];NSString *address = [NSString stringWithFormat:@"%p",person];[];NSLog(@"amodel %@",address);}NSLog(@"count %@",@([set count]));[] haveCountOf:20];[] should] haveCountOf:0];});});context(@"array action", ^{afterEach(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});it(@"init", ^{JKTeacher *teacher = [JKTeacher new];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:0];invoked = YES;}];teacher.students = @[].mutableCopy;[] beYes];});it(@"jk_addObject", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:1];[] equal:theValue(JKKVOArrayChangeTypeAddTail)];[] equal:person1];[] equal:theValue(0)];[] equal:theValue(NSNotFound)];invoked = YES;}];[];[] beYes];});it(@"jk_insertObject:atIndex:", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:3];[] equal:theValue(JKKVOArrayChangeTypeAddAtIndex)];[] haveCountOf:1];[] equal:person3];[] equal:theValue(1)];[] equal:theValue(NSNotFound)];invoked = YES;}];[];[] beYes];});it(@"jk_insertObject:atIndex:1", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:3];[] equal:theValue(JKKVOArrayChangeTypeAddAtIndex)];[] haveCountOf:1];[] equal:person3];[] equal:theValue(2)];[] equal:theValue(NSNotFound)];invoked = YES;}];[];[] beYes];});it(@"jk_removeLastObject", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";[];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:2];[] equal:theValue(JKKVOArrayChangeTypeRemoveTail)];[] haveCountOf:1];JKPersonModel *tmpPerson = (JKPersonModel *)changedModel.changedElements.firstObject.object;NSLog(@"person %@",tmpPerson.name);[] equal:person3];[] equal:theValue(NSNotFound)];[] equal:theValue(2)];invoked = YES;}];[];[] beYes];});it(@"jk_removeObjectAtIndex", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";[];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:2];[] equal:theValue(JKKVOArrayChangeTypeRemoveAtIndex)];[] haveCountOf:1];[] equal:person2];[] equal:theValue(NSNotFound)];[] equal:theValue(1)];invoked = YES;}];[];[] beYes];});it(@"jk_replaceObjectAtIndex:withObject:", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";[];JKPersonModel *person4 = [JKPersonModel new];person4.name = @"4";__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:3];[] equal:theValue(JKKVOArrayChangeTypeReplace)];[] haveCountOf:2];[] equal:person4];[] equal:theValue(NSNotFound)];[] equal:theValue(2)];[] equal:theValue(2)];[] equal:theValue(NSNotFound)];invoked = YES;}];[];[] beYes];});it(@"jk_addObjectsFromArray:", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";JKPersonModel *person4 = [JKPersonModel new];NSArray *array = @[person3,person4];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:4];[] equal:theValue(JKKVOArrayChangeTypeAddTail)];[] haveCountOf:2];[] equal:person3];[] equal:theValue(2)];[] equal:theValue(NSNotFound)];[] equal:person4];[] equal:theValue(3)];[] equal:theValue(NSNotFound)];invoked = YES;}];[];[] beYes];});it(@"jk_exchangeObjectAtIndex:withObjectAtIndex:", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";[];JKPersonModel *person4 = [JKPersonModel new];person4.name = @"4";[];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:4];[] equal:theValue(JKKVOArrayChangeTypeReplace)];[] haveCountOf:2];[] equal:person3];[] equal:theValue(3)];[] equal:theValue(2)];[] equal:person4];[] equal:theValue(2)];[] equal:theValue(3)];invoked = YES;}];[];[] beYes];});it(@"jk_removeAllObjects", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];JKPersonModel *person2 = [JKPersonModel new];person2.name = @"2";[];JKPersonModel *person3 = [JKPersonModel new];person3.name = @"3";[];JKPersonModel *person4 = [JKPersonModel new];person4.name = @"4";[];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:0];[] equal:theValue(JKKVOArrayChangeTypeRemoveTail)];[] haveCountOf:4];[].object should] equal:person1];[].newIndex) should] equal:theValue(NSNotFound)];[].oldIndex) should] equal:theValue(0)];[].object should] equal:person2];[].newIndex) should] equal:theValue(NSNotFound)];[].oldIndex) should] equal:theValue(1)];[].object should] equal:person3];[].newIndex) should] equal:theValue(NSNotFound)];[].oldIndex) should] equal:theValue(2)];[].object should] equal:person4];[].newIndex) should] equal:theValue(NSNotFound)];[].oldIndex) should] equal:theValue(3)];invoked = YES;}];[];[] beYes];});it(@"jk_removeObject:", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];[];__block BOOL invoked = NO;[[[[change objectForKey:@] should] haveCountOf:0];[] equal:theValue(JKKVOArrayChangeTypeRemoveAtIndex)];[] haveCountOf:2];[] equal:person1];[] equal:theValue(NSNotFound)];[] equal:theValue(0)];[] equal:person1];[] equal:theValue(NSNotFound)];[] equal:theValue(1)];invoked = YES;}];[];[] beYes];});it(@"element's property change", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];__block BOOL invoked = NO;[] withBlock:^(NSString * _Nonnull keyPath, NSDictionary *change, JKKVOArrayChangeModel * _Nonnull changedModel, void * _Nonnull context) {[] equal:theValue(JKKVOArrayChangeTypeElement)];[] haveCountOf:1];[] equal:person1];[] equal:theValue(0)];[] equal:theValue(0)];invoked = YES;}];person1.name = @"2";[] beYes];});it(@"jk_removeObservers", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];__block BOOL invoked = NO;[] withBlock:^(NSString * _Nonnull keyPath, NSDictionary *change, JKKVOArrayChangeModel * _Nonnull changedModel, void * _Nonnull context) {invoked = YES;}];person1.name = @"2";[] beYes];[];});it(@"jk_removeObserver:forKeyPath:context:", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";[];__block BOOL invoked = NO;[] withBlock:^(NSString * _Nonnull keyPath, NSDictionary *change, JKKVOArrayChangeModel * _Nonnull changedModel, void * _Nonnull context) {invoked = YES;}];person1.name = @"2";[] beYes];[];});});context(@"observerd is nil", ^{beforeAll(^{NSArray *array = [JKKVOItemManager items];for(JKKVOItem *item in array) {[];}});it(@"observerd is nil", ^{JKPersonModel *person = [JKPersonModel new];JKWorker *worker = [JKWorker new];[}];});it(@"two object, one array jk_addObject", ^{JKTeacher *teacher = [JKTeacher new];NSMutableArray *students = [NSMutableArray new];teacher.students = students;JKTeacher *teacher1 = [JKTeacher new];teacher1.students = students;JKPersonModel *person1 = [JKPersonModel new];person1.name = @"1";__block BOOL invoked = NO;__block BOOL invoked1 = NO;[[[[change objectForKey:@] should] haveCountOf:1];[] equal:theValue(JKKVOArrayChangeTypeAddTail)];[] equal:person1];[] equal:theValue(0)];[] equal:theValue(NSNotFound)];invoked = YES;}];[[[[change objectForKey:NSKeyValueChangeNewKey] should] haveCountOf:1];[] should] haveCountOf:0];[] equal:theValue(JKKVOArrayChangeTypeAddTail)];[] equal:person1];[] equal:theValue(0)];[] equal:theValue(NSNotFound)];invoked1 = YES;}];[];[] beYes];[] beYes];});afterAll(^{NSArray *array = [JKKVOItemManager items];[]haveCountOf:0];});});});SPEC_END
pod 集成命令
pod 'JKKVOHelper'