平时工作中我们接触最多的容器,可能就数字典与数组了。有时我们在使用这些容器时有这样一个困惑,容器中的数据改变了,我们却无法立即知道。近日小编因为这个感到有些小苦恼,思来想去发现还是有途径可以做到监听容器的变化。
接下来我将会用文字将我的想法叙述出来,供大家参考。
要想监听一个容器的变化,首先我们需要对这个容器的属性及方法有着深入的了解。要监听容器的元素改变,那肯定是针对于可变容器。我们先着手于可变数组的监听工作。
接下来的工作将分成三部分:
- 可变数组容器的分析
- 设计一套监听回调机制
- 使用runtime机制替换或者交换对应的函数指针IMP
现在我们开始着手分析可变数组的方法。
可变数组的分析
可变数组的定义
NSMutableArray定义了一套接口用于管理可变数组对象,该类的添加/插入/删除基础操作继承于不可变数组NSArray
可变数组的方法分类
通过查阅苹果的官方文档,我们大致可以将可变数组的方法分为以下几部分:
- 创建和初始化一个可变数组
- 数组元素的添加操作
- 数组元素的删除操作
- 数组元素的替换操作
- 数组内容的筛选操作
- 数组内容的排序操作
下面我将各部分的相关函数贴出,以便大家更好的直观了解。
创建和初始化一个可变数组
+ arrayWithCapacity:
+ arrayWithContentsOfFile:
+ arrayWithContentsOfURL:
- init
- initWithCapacity:
- initWithContentsOfFile:
- initWithContentsOfURL:
数组元素的添加操作
- addObject:
- addObjectsFromArray:
- insertObject:atIndex:
- insertObjects:atIndexes:
ps:批量元素的插入会调用多次单个元素的插入方法,在以后的监听过程中需要做些特殊的处理
- void insertObjects:(NSArray *)additions atIndexes:(NSIndexSet *)indexes { NSUInteger currentIndex = [indexes firstIndex]; NSUInteger i, count = [indexes count]; for (i = 0; i < count; i++) { [self insertObject:[additions objectAtIndex:i] atIndex:currentIndex]; currentIndex = [indexes indexGreaterThanIndex:currentIndex]; } }
数组元素的删除操作
//删除元素的删除
- removeLastObject
- removeObjectAtIndex:
// 批量元素的删除
- removeAllObjects
- removeObject:
- removeObject:inRange:
- removeObjectsAtIndexes:
- removeObjectIdenticalTo:
- removeObjectIdenticalTo:inRange:
- removeObjectsInArray:
- removeObjectsInRange:
ps: 以下方法会多次调用
- removeObjectAtIndex:
方法:,在监听过程中需要处理多次调用问题。- removeObject: - removeObject:inRange: - removeObjectIdenticalTo: - removeObjectIdenticalTo:inRange: - removeObjectsInArray:
数组元素的替换操作
- replaceObjectAtIndex:withObject:
- setObject:atIndexedSubscript:
ps:
- setObject:atIndexedSubscript:
这个方法我们最好不要直接使用而是使用语法糖的形式调用。mutableArray[3] = @"someValue"; // equivalent to [mutableArray replaceObjectAtIndex:3 withObject:@"someValue"];
若替换对象为空会抛出
NSInvalidArgumentException
异常,若索引下标越界会抛出NSRangeException
异常
- replaceObjectsAtIndexes:withObjects:
- replaceObjectsInRange:withObjectsFromArray:range:
- replaceObjectsInRange:withObjectsFromArray:
- setArray:
ps: 以下方法会调用多次
- replaceObjectAtIndex:withObject:
方法- replaceObjectsAtIndexes:withObjects: //PS: objects的个数必须与indexes的个数相同,否则会抛出异常
数组元素的筛选
- filterUsingPredicate:
ps: 会在数组中直接筛选出符合条件的数组元素,不符合的会直接删除
数组元素的排序
- exchangeObjectAtIndex:withObjectAtIndex:
- sortUsingDescriptors:
- sortUsingComparator:
- sortWithOptions:usingComparator:
- sortUsingFunction:context:
- sortUsingSelector:
ps:数组的排序方法会递归调用
- replaceObjectAtIndex:withObject:
在监听数组排序时需要处理多次回调的问题
通过对数组方法的分类及分析,我们对数组的方法有了大概的了解。接下来我们来制定一套协议,用来做回调操作。
监听机制的协议商定
协议行为的划分
从数组元素的变化行为来看,协议主要可以划分为以下几部分:
- 元素增加
- 单个元素的增加
- 多个元素的增加
- 元素删除
- 单个元素的删除
- 多个元素的删除
- 元素的替换
- 单个元素的替换
- 数组改变了
- 删除所有元素
- 批量元素的替换
- 数组的排序操作
- 数组元素的筛选
- 数组元素的交换
- 元素的交换
参数商定
监听数组变化,哪些数据需要被回调回来呢?
通过参考表格协议和同事进行商讨决定
对于数组元素的添加/删除操作需要将以下数据回调回来:
- 监听的数组本身
- 添加删除的元素对象
- 及对应的索引返回。
对于数组的替换操作将以下数据回调回来:
- 监听数组对象本身
- 替换的对象
- 替换的新对象
- 对象的索引
对于数组改变了的相关操作,考虑到期复杂度相对较高,先采用较简单的方式处理,简洁明了的告诉协议的遵守者,说数组变了,回调回监听的数组对象本身。
对于数组元素的交换,将以下数据回调:
- 监听的数组本身
- 交换的下标1
- 将交的下标2
协议确认
@protocol NSmutableArrayOberver <NSObject>
@optional
#pragma mark - 增加
-(void)mutableArray:(NSMutableArray*)array didAddObject:(id)anObject atIndex:(NSInteger)index;
-(void)mutableArray:(NSMutableArray*)array didAddObjects:(NSArray *)objects atIndexes:(NSIndexSet*)indexSet;
#pragma mark - 删除
-(void)mutableArray:(NSMutableArray *)array didDeleteObject:(id)anObject atIndex:(NSInteger)index;
-(void)mutableArray:(NSMutableArray *)array didDeleteObjects:(NSArray*)objects atIndexes:(NSIndexSet*)indexes;
#pragma mark - 替换
-(void)mutableArray:(NSMutableArray*)array replaceObject:(id )object withObject:(id)anObject atIndex:(NSUInteger)index;
#pragma mark - 改变(排序)
-(void)mutableArrayhasChanged:(NSMutableArray*)array;
#pragma mark - 位置交换
-(void)mutableArray:(NSMutableArray*)array exchangeObjectAtIndex:(NSUInteger)index1 withObjectAtIndex:(NSUInteger)index2;
@end
使用runtime机制替换或者交换对应的函数指针IMP
- 创建一个数组的扩展,并添加代理属性(使用关联属性添加)
- 创建相应的函数指针,并替换数组对应的方法IMP
这里以- insertObject:atIndex:为例:
创建C函数指针:typedef void (*insertObject_atIndex_IMP)(id self, SEL _cmd ,id anObject ,NSUInteger index); static insertObject_atIndex_IMP origin_insertObject_atIndex = nil; static void replace_insertObject_atIndex(id self, SEL _cmd ,id anObject ,NSUInteger index){ NSMutableArray * array = self; if ([array.delegate conformsToProtocol:@protocol(NSmutableArrayOberver) ]&& [array.delegate respondsToSelector:@selector(mutableArray:didAddObject:atIndex:)]) { NSInteger number = addCount(); origin_insertObject_atIndex(self,_cmd,anObject,index); if (number==1) { [array.delegate mutableArray:self didAddObject:anObject atIndex:index]; } decreaseCount(); }else{ origin_insertObject_atIndex(self,_cmd,anObject,index); } }
+(void)load{ Method method; Class class = NSClassFromString(@"__NSArrayM"); #pragma mark - 添加 method = class_getInstanceMethod(class, @selector(insertObject:atIndex:)); origin_insertObject_atIndex = (insertObject_atIndex_IMP)method_setImplementation(method, (IMP)replace_insertObject_atIndex); }
PS:这里使用对一个线程变量用于统计函数的调用次数,进入函数时进行加1操作,退出时进行减1操作。如果当前调用次数为1,则在当前方法中进行回调,如果不在则不回调,由上一级调用函数进行回调。该原理类似于引用计数。
至一个基础版本的数组监听工作已经完成,后续将兼容block的工作模式,以便提高代码的高聚合。
参考博客: