项目中经常会需要监听一个可变数组里的内容的变化,一般监听用到的方式就是KVO
,但我们知道KVO
监听的是对象的地址的改变,而可变数组里的内容的改变是不会直接导致数组对象地址的改变的。
对于上述情况,一般的做法还是通过KVO
,只是操作数组的时候是通过mutableArrayValueForKey:
的方式取得可变数组,然后进行一些改变内容的操作
[[self.arrayTest mutableArrayValueForKey:@"arrar"] addObject:@"111"];
但是通过这种方式为什么就能使KVO
监听到可变数组的内容的改变呢?
这就得先知道KVO
的原理了,而关于KVO
原理的文章还是有很多,这里我只做简单描述。
KVO
的原理是在程序运行的时候动态的给被监听属性所在的类创建了一个子类,实例化对象的isa指针指向这个子类,然后这个子类会重写父类的方法,在重写被监听属性的set
方法的时候会调用一个C语言函数,这个函数里会在给属性赋值前后分别调用下面两个方法
willChangeValueForKey:
didChangeValueForKey:
调用didChangeValueForKey:
方法后就会通知监听者监听的属性值已经改变。我们修改属性的值实际上是调用了KVO
动态创建的子类里的属性set
方法。
详细参考链接
所以从这里可以看出KVO
监听属性的改变可以通过调用属性的set
方法触发KVO
通知。
调用mutableArrayValueForKey:
到底做了什么?
mutableArrayValueForKey:
是NSKeyValueCoding
协议里的一个方法,平时使用KVC
用的最多的是valueForKey:
这个方法,mutableArrayValueForKey:
有点类似valueForKey:
,都是通过key
搜索返回一个对象,只是mutableArrayValueForKey:
返回的是可变数组对象。另外一点就是调用mutableArrayValueForKey:
后KVC
的搜索顺序。
调用mutableArrayValueForKey
后,KVC
先会搜索类中是否有insertObject:in<Key>AtIndex:
, removeObjectFrom<Key>AtIndex:
或者 insert<Key>AdIndexes
, remove<Key>AtIndexes
格式的方法,如果至少找到一个insert
方法和一个remove
方法,那么返回一个可以响应NSMutableArray
所有方法的代理集合,那么给这个代理集合发送NSMutableArray
的方法,以insertObject:in<Key>AtIndex:
, removeObjectFrom<Key>AtIndex:
或者 insert<Key>AdIndexes
, remove<Key>AtIndexes
组合的形式调用。
如果上述的方法没有找到,则搜索set<Key>:
格式的方法,如果找到,那么发送给代理集合的NSMutableArray
最终都会调用set<Key>:
方法, 也就是说,mutableArrayValueForKey:
取出的代理集合修改后,用set<Key>:
重新赋值回去,这样每次都会产生一个新对象。下面验证下:
创建一个类,用可变数组做属性
@interface MutableArrayTest : NSObject
@property (nonatomic, strong) NSMutableArray *testArray;
@end
@implementation MutableArrayTest
- (NSMutableArray *)testArray{
if (!_testArray){
_testArray = [NSMutableArray array];
}
return _testArray;
}
@end
然后找个控制器实例化这个类并且添加KVO
self.mutableArrayTest = [[MutableArrayTest alloc] init];
[self.mutableArrayTest addObserver:self forKeyPath:@"testArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"初始地址:%p",self.mutableArrayTest.testArray);
[self.mutableArrayTest.testArray addObject:@"000"];
NSLog(@"调用addObject后:%p",self.mutableArrayTest.testArray);
[[self.mutableArrayTest mutableArrayValueForKey:@"testArray"] addObject:@"111"];
NSLog(@"第一次 mutableArrayValueForKey:%p",self.mutableArrayTest.testArray);
[[self.mutableArrayTest mutableArrayValueForKey:@"testArray"] addObject:@"222"];
NSLog(@"第二次 mutableArrayValueForKey:%p",self.mutableArrayTest.testArray);
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
}
打印结果:
可以看到可变数组初始地址和没有加mutableArrayValueForKey:
获得的可变数组使用addObject:
方法后的地址是一样的,而使用mutableArrayValueForKey:
后改变数组内容,可变数组的地址也变了。
结论:最终我们可以知道在给类中的可变数组添加KVO
后,在程序运行时,动态的会给类创建一个子类,类的实例化对象的isa会指向这个子类,这个子类重写了父类的属性set
方法,如果触发了实例化对象的属性的set
方法实际上是调用了子类对象的set
方法,然后在实际给属性赋值后会触发KVO
通知,告知观察者属性值改动了;而调用mutableArrayValueForKey:
方法后,由于类中没用实现insertObject:in<Key>AtIndex:
, removeObjectFrom<Key>AtIndex:
或者 insert<Key>AdIndexes
, remove<Key>AtIndexes
格式的方法,KVC
会搜索到set<Key>:
方法,这时候返回的可变数组对它发送NSMutableArray
的方法最终都会调用set<Key>:
方法,调用了set<Key>:
方法就会触发KVO
,所以这样就能监听到可变数组里的内容的改变。
KVOMutableArray实现的基本原理
经过上面的理解,KVOMutableArray
基本的实现方式就很清楚了,KVOMutableArray
这个类继承NSMutableArray
,然后重写了部分方法,调用这些方法实际上是交由KVOMutableArrayObserver
这个类来处理,KVOMutableArray
是KVOMutableArrayObserver
的一个属性,这样就可以使用KVO
来监测了。
由于KVOMutableArrayObserver
类中是实现了insertObject:in<Key>AtIndex:
, removeObjectFrom<Key>AtIndex:
和 insert<Key>AdIndexes
, remove<Key>AtIndexes
格式的方法(为什么是是实现这些方法?下面有具体说明),所以在添加KVO
后,在程序运行时会在动态创建的子类里重写这些方法,KVOMutableArrayObserver
的实例化对象的isa是指向这个子类的, 所以当KVOMutableArray
调用NSMutableArray
的方法后最后实际调用的是KVOMutableArrayObserver
的子类的实例化对象中的方法,子类中重写的方法在真正修改完数组的内容后会调用didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
,最后KVO通知观察者。使用这种方式改变可变数组里的内容就不会每次都调用set(Key):
产生新的对象,数组属性不改变对象地址的改变内容也能使用KVO
监听。
KVOMutableArrayObserver
中为什么实现insertObject:in<Key>AtIndex:
, removeObjectFrom<Key>AtIndex:
和 insert<Key>AdIndexes
, remove<Key>AtIndexes
格式的方法:
我在了解到使用mutableArrayValueForKey:
方法KVC
的搜索顺序后,并且结合KVO
的原理,猜想上述的四个方法类似于set(Key):
方法,在添加KVO
后在动态创建的子类里会重写这四个方法,然后我利用验证set(Key):
被重写的方法验证上面四个方法被重写后大概的实现结构。
创建一个类,类似于KVOMutableArrayObserver
@interface ArrayTest : NSObject
@property (nonatomic, strong) NSMutableArray* arrar;
- (instancetype)init;
- (instancetype)initWithMutableArray:(NSMutableArray*)array;
- (void)removeObjectFromArrarAtIndex:(NSUInteger)index;
- (void)removeArrarAtIndexes:(NSIndexSet *)indexes;
- (void)insertObject:(id)obj inArrarAtIndex:(NSUInteger)index;
- (void)insertArrar:(NSArray *)array atIndexes:(NSIndexSet *)indexes;
@end
类的实现
@implementation ArrayTest
- (instancetype)init
{
return [self initWithMutableArray:[NSMutableArray new]];
}
- (instancetype)initWithMutableArray:(NSMutableArray*)array
{
if((self = [super init]))
{
_arrar = array;
}
return self;
}
- (void)removeObjectFromArrarAtIndex:(NSUInteger)index;
{
[self.arrar removeObjectAtIndex:index];
}
- (void)removeArrarAtIndexes:(NSIndexSet *)indexes
{
[self.arrar removeObjectsAtIndexes:indexes];
}
- (void)insertObject:(id)obj inArrarAtIndex:(NSUInteger)index
{
NSLog(@"insertObject:%@ inArrarAtIndex:%ld",obj,index);
[self.arrar insertObject:obj atIndex:index];
}
- (void)insertArrar:(NSArray *)array atIndexes:(NSIndexSet *)indexes;
{
[self.arrar insertObjects:array atIndexes:indexes];
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey : %@",key);
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey : %@ --begin",key);
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey : %@ --end",key);
}
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key{
[super willChange:changeKind valuesAtIndexes:indexes forKey:key];
NSLog(@"willChangeValueForKey : %@",key);
}
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key{
NSLog(@"didChangeValueForKey : %@ --begin",key);
[super didChange:changeKind valuesAtIndexes:indexes forKey:key];
NSLog(@"didChangeValueForKey : %@ --end",key);
}
然后找个控制器实例化,给数组添加KVO
监听
self.array = [NSMutableArray array];
self.arrayTest = [[ArrayTest alloc] initWithMutableArray:self.array];
[self.arrayTest addObserver:self forKeyPath:@"arrar" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil]
NSLog(@"添加KVO监听之后:-%@", object_getClass(self.arrayTest));
NSLog(@"添加监听之后:- %p", [self.arrayTest methodForSelector:@selector(insertObject:inArrarAtIndex:)]);
// 断点 使用LLDB打印一下地址的IMP
[self.arrayTest insertObject:@"111" inArrarAtIndex:0]
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"观察的属性值改变了:%@",change);
}
运行结果如下
我们可以看到添加KVO
后,本来是ArrayTest
类现在变成了NSKVONotifying_ArrayTest
,动态创建了子类,在断点处通过lldb打印得知insertObject:inArrarAtIndex:
方法实际上调用了NSKVOInsertObjectAtIndexAndNotify
这么一个方法,说明这个方法被重写了,重写之后是先调用了willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
这个方法(不是willChangeValueForKey:
,经过测试得知的,didChangeValueForKey:
同样),然后调用父类的insertObject:(id)obj inArrarAtIndex:(NSUInteger)index
方法,最后调用了didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
方法,完成后就会通知观察者。
也可以通过下面的方式查看大致方法调用
其他三个方法也可这样验证下。
结论:上述的四个方法在添加KVO
后会在动态创建的子类中重写,当调用这四个方法中的其中一个的时候实际上调用的是子类中的方法,完成后会调用- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
通知观察者,至于为什么子类重写这样四个格式的方法主要还是和KVC
的搜索有关,将上面代码的[self.arrayTest insertObject:@"111" inArrarAtIndex:0]
替换成[[self.arrayTest mutableArrayValueForKey:@"arrar"] addObject:@"111"];
也是一样的效果,所以子类必须重写对应的方法才能实现。