NSFastEnumeration
前言
最近在实现一个自定义的容器类,在实现过程中,突然想到,如果别人在遍历这个容器类的时候,如果可以实现 for...in 的话,就可以让我们的容器类遍历起来更加的优雅(真的么???总觉得这个是我的强迫症犯了)。那么怎么样才能让我们的容器支持 for...in 呢?做过 iOS 同学大多数都应该知道或者听说过, for...in 只要实现一个协议即可,这个协议就是 NSFastEnumeration 。定义如下:
/*
A protocol that objects adopt to support fast enumeration.
@Discussion The abstract class NSEnumerator provides a convenience implementation that uses nextObject to return items one at a time.
*/
@protocol NSFastEnumeration
/*
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.
@Discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.
@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
@param buffer A C array of objects over which the sender is to iterate.
@param len The maximum number of objects to return in stackbuf.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
@end
乍一看,easy!就一个方法,只要我们正确的实现了这个方法,我们的容器类就可以支持 for...in 遍历了。但是仔细一看各个参数:
NSFastEnumerationState:
/*
This defines the structure used as contextual information in the NSFastEnumeration protocol.
*/
typedef struct {
unsigned long state; /* Arbitrary state information used by the iterator. Typically this is set to 0 at the beginning of the iteration. */
id __unsafe_unretained _Nullable * _Nullable itemsPtr; /* A C array of objects. */
unsigned long * _Nullable mutationsPtr; /* Arbitrary state information used to detect whether the collection has been mutated. */
unsigned long extra[5]; /* A C array that you can use to hold returned values. */
} NSFastEnumerationState;
WTF? 这都是些什么东西,在习惯了 ARC 之后,经常在 OC 面向对象中编程的我一脸懵逼,state 是一个结构体,buffer 是一个指针数组,这些都怎么用?本文接下来会详细的讲解 NSFastEnumeration 这个协议,并且会解释遇到的坑,最终提供一个可以执行的,简单的 demo 容器类。
NSFastEnumeration 协议解析
回到正题,我们来仔细的看看官方给出关于 NSFastEnumeration 的注释。下面给出我粗糙的翻译(英文不好,请见谅):
/*
A protocol that objects adopt to support fast enumeration.
一个对象需要实现以支持快速枚举的协议。
@Discussion The abstract class NSEnumerator provides a convenience implementation that uses nextObject to return items one at a time.
抽象的 NSEnumerator 类,提供了使用 nextObject 在同一时间返回对象的便利实现。
*/
@protocol NSFastEnumeration
/*
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.
译:通过 C 对象数组的引用来返回给调用者需要枚举的内容(调用者枚举的是一个 C 数组),并且通过方法的返回值来返回 C 对象数组中对象的个数。
注:返回两个东西,一个是 C 对象数组,用来给调用者遍历,另一个数 C 对象数组的个数(C 数组,不能使用 sizeof 来取个数,因为有可能是一个指针,所以需要我们返回)。
@Discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.
译:state 结构体被假定为栈的本地内存(随栈帧销毁,无需管理内存),所以您可以根据您的迭代状态,重铸(自己玩)入参的 state 结构体。
注:state 是栈内存,可以根据自己容器内部的代码逻辑,修改 state。
@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
译:用于枚举的上下文信息,在此之上,保证容器不会被修改。
注:state 是一个枚举的上下文,并且在枚举的过程中,需要保证容器不会被修改。相信大家在开始写码不久的时候,也犯过类似的错误,一边遍历数组,一边修改数组,然后出了 crash。如果你没有,那么... 好吧,你赢了。
@param buffer A C array of objects over which the sender is to iterate.
译:一个调用者用于迭代的 C 对象数组。
@param len The maximum number of objects to return in buffer.
译:buffer 中对象的 最大 个数。
注:用到了 maximum number ,说明 buffer 可能是不满的,比如 buffer 可以容纳 10 个对象,但是实际上可能只有 3 个对象。
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
@end
/*
This defines the structure used as contextual information in the NSFastEnumeration protocol.
译:NSFastEnumeration 协议中的上下文信息结构体定义。
注:废话。
*/
typedef struct {
unsigned long state; /* Arbitrary state information used by the iterator. Typically this is set to 0 at the beginning of the iteration. */
译:迭代器使用的任意状态信息。通常在迭代开始时,置为 0。
注:任意的状态信息,其实就是我们自己定义的,通常在迭代开始时置为 0.
id __unsafe_unretained _Nullable * _Nullable itemsPtr; /* A C array of objects. */
译:一个 C 对象数组。
unsigned long * _Nullable mutationsPtr; /* Arbitrary state information used to detect whether the collection has been mutated. */
译:用于迭代器监测发生修改的任意状态信息。
注:我们自己定义,随意的一个值,只是用来判断在迭代过程中,容器是否被修改。
unsigned long extra[5]; /* A C array that you can use to hold returned values. */
译:一个您可以用来保存返回值的 C 数组。
注:可以用,当然也可以不用。
} NSFastEnumerationState;
经过粗糙的翻译,现在大概明白了这个协议以及协议中唯一的方法的大致用法:在我们使用 for...in 去遍历一个容器的时候,系统会调用这个方法,来返回需要迭代的 C 对象数组。但是大致明白不代表明白了,怎么返回,通过 buffer 参数?state 参数是怎么用的?本人的体验:知道了这个方法是干啥的,但是怎么用的,完全不明白。
寻找一个 NSFastEnumeration 的 demo
在经过上述代码描述,的确是不知道这个东西到底是怎么用的,怎么办呢?Google 呗,这种问题肯定有人遇到过。然后找到 Mike Ash 的一篇文章(相信大家应该也看到过这篇):
Implementing Fast Enumeration
读完了这篇文章之后,好像明白了很多,也大概明白了 for...in 的实现。我们自己来操作一遍。
- 以 NSArray 为例,写一个 for...in 的 demo
int main(int argc, const char * _Nullable argv[])
{
/// 为了 rewrite 之后方便寻找,起了一个特殊的名字
NSArray *chris___array = @[@0, @1, @2];
for (id obj in chris___array) {
NSLog(@"%@", obj);
}
return 0;
}
- clang -rewrite-objc main.m(如果有不知道这个怎么用的,自行百度就好,这里随便贴一个链接:iOS clang -rewrite-objc),由于代码太长,这里只粘贴关键部分:
由 clang rewrite-objc 生成的关键 cpp 代码:
int main(int argc, const char * _Nullable argv[])
{
NSArray *chris___array = ((NSArray *(*)(Class, SEL, ObjectType _Nonnull const * _Nonnull, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(3U, ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 0), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2)).arr, 3U);
{
id obj;
struct __objcFastEnumerationState enumState = { 0 };
id __rw_items[16];
id l_collection = (id) chris___array;
_WIN_NSUInteger limit =
((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
if (limit) {
unsigned long startMutations = *enumState.mutationsPtr;
do {
unsigned long counter = 0;
do {
if (startMutations != *enumState.mutationsPtr)
objc_enumerationMutation(l_collection);
obj = (id)enumState.itemsPtr[counter++]; {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_38_50v39g0n3ml22_nccmtc1m5r0000gp_T_main_d36a2c_mi_0, obj);
};
__continue_label_1: ;
} while (counter < limit);
} while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
obj = ((id)0);
__break_label_1: ;
}
else
obj = ((id)0);
}
return 0;
}
看着好恶心,一堆转换。这里我们来简单翻译一下(objc_msgsend 这种,感兴趣的自己查哈):
int main(int argc, const char * _Nullable argv[])
{
NSArray *array = @[@0, @1, @2];
{
id obj; /// 我们在 for in 中声明的 obj 本地变量
struct NSFastEnumerationState enumState = { 0 }; /// 初始化一个所有 fields 都为 0 的 NSFastEnumerationState 结构体
id __rw_items[16]; /// 一个 C 对象数组
id l_collection = (id) chris___array; /// 对原数组(chris___array)的一个重新声明
NSUInteger limit = [l_collection countByEnumeratingWithState:&enumState objects:__rw_items count:16]; /// 向数组发送 -[id countByEnumeratingWithState:objects:count:] 消息,并将结果保存在本地变量 limit 中
if (limit) { /// 由于 Limit 是 unsigned 无符号,所以这里可以直接用 if 判断是否为 0,不用考虑负数的存在(平时大家还是减少这种代码,有点令人疑惑,比如写成 if (limit > 0) 会好很多)
unsigned long startMutations = *enumState.mutationsPtr; /// 将 enumState 的 mutationsPtr 指针保存下来,如果后面这个指针被修改,证明在迭代过程中有修改容器的操作(比如边迭代边向容器中插入元素)
do { /// 第一层循环, 循环向数组发送 -[id countByEnumeratingWithState:objects:count:] 直到遍历结束(返回个数 0),可能一次调用 -[id countByEnumeratingWithState:objects:count:] 返回不了全部的内容,所以需要两次迭代
unsigned long counter = 0; /// 声明本地变量 counter,并置为 0
do { /// 第二层循环, 遍历一次 limit
if (startMutations != *enumState.mutationsPtr) { /// 先判断是否有在迭代过程中修改容器的情况,如果有抛出异常
objc_enumerationMutation(l_collection); /// 抛出异常
}
obj = (id)enumState.itemsPtr[counter++]; /// 从 enumState 的 itemsPtr 变量中,取出来第 counter 个元素
{
NSLog(@"%@", obj); /// 使用取到的元素(一次遍历)
}
} while (counter < limit);
} while ( (limit = [l_collection countByEnumeratingWithState:&enumState objects:__rw_items count:16]) );
} else {
/// 如果遍历结束(返回个数 0),将本地变量 obj 置为 nil
obj = nil;
}
}
return 0;
}
经过我们粗糙的翻译,这段代码简化了 n 多,看起来也更加直接。for...in 展开后的代码,现在看起来并没有那么神秘了。不断的向容器发送 -[id countByEnumeratingWithState:objects:count:] 消息,直到遍历结束(返回 0)。每次取出来的内容,通过 enumState->itemsPtr 引用获取。所以用到的参数/本地变量共有三个:
- -[id countByEnumeratingWithState:objects:count:] 的返回值,作为外层循环的条件;
- 通过 C 指针传入的 enumState 结构体,其中真正遍历的时候用到的字段共 2 个:
- mutationsPtr:每次迭代通过 == 判断是否有在迭代过程中修改容器的情况,如果有,抛出异常;
- itemsPtr:每次迭代,从 itemsPtr 依次读取元素,直到读取完(count == limit)。
看完这段代码之后,NSFastEnumeration 这个协议看起来不再神秘,我们只要在 -[id countByEnumeratingWithState:objects:count:] 将 state 参数的 itemsPtr 变量设置正确,并且在方法结束的时候,返回正确的 itemsPtr 引用数组的个数。注意:这里我们可以一次返回多个数据,也可以返回一个数据,返回数据个数为 0 的时候,遍历结束。通常来说,我们应该一次返回一个元素来给调用者遍历,因为调用者有可能在某些情况打破( break )循环,如果一次处理掉所有的指针,是否有效率上的浪费?这里就需要根据自己的场景来酌情处理。
实现一个支持 NSFastEnumeration 的容器类
先来看一下我们 demo 容器类 XXCustomSet 的声明:
#import
NS_ASSUME_NONNULL_BEGIN
/// 自定义容器类,接受了 NSFastEnumeration 协议
@interface XXCustomSet<__covariant ObjectType> : NSObject
/// 返回容器中的所有元素
@property (nonatomic, copy, readonly, nullable) NSArray *allObjects;
#pragma mark - Public
/// 向容器中添加一个对象
- (void)addObject:(ObjectType)object;
/// 移除容器中的一个对象
- (void)removeObject:(ObjectType)object;
@end
NS_ASSUME_NONNULL_END
XXCustomSet 的实现:
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 内部实际存储的可变数组容器
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
@end
@implementation XXCustomSet
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
return 0;
}
@end
NS_ASSUME_NONNULL_END
代码非常简单,接下来我们来开始实现 NSFastEnumeration 协议。
- 支持遍历
为了支持让外部可以用 for...in 来遍历我们的容器,我们需要在 -[id countByEnumeratingWithState:objects:count:] 方法中设置正确的 state->itemsPtr。代码如下(注:只修改了 -[id countByEnumeratingWithState:objects:count:] 方法):
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 内部实际存储的数组
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
@end
@implementation XXCustomSet
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 思路:初始状态, state->state 为 0 ,每次让 state->state 增加 1 ,然后我们设置 state->itemsPtr 为 buffer 指针,并且设置 buffer 的第一个元素为当前遍历的元素
/// 简单来说,以 state->state 为下标,取出我们内部数组中的元素,放进 buffer 的第一个元素中,并且设置 state->itemsPtr 指针为 buffer,这样结合我们 rewrite 之后的代码来看,结果就变成了每次读取数组中当前 state 下标的元素,一直读取到结束
if (state->state >= self.internalAllObjects.count) {
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
return 1;
}
@end
NS_ASSUME_NONNULL_END
看起来我们这个代码应该可以正常运行了,那么我们来执行一遍,测试代码如下:
#import
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
int main(int argc, const char * _Nullable argv[])
{
XXCustomSet *set = [XXCustomSet new];
for (NSUInteger index = 0; index < 30; index++) {
[set addObject:@(index)];
}
for (id number in set) { /// 执行后,这一行挂了,报错:Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
NSLog(@"%@", number);
}
return 0;
}
NS_ASSUME_NONNULL_END
WTF? 发生什么事了?
再仔细看了一下 rewrite 后的 cpp 代码,应该是这一句:
unsigned long startMutations = *enumState.mutationsPtr; /// 将 enumState 的 mutationsPtr 指针保存下来,如果后面这个指针被修改,证明在迭代过程中有修改容
由于我们没有设置 mutationsPtr 指针,那么这一句话的后果就是在 0 处取值,所以给了我们一个 bad access, address=0x0。我们来做一些简单的修改,让代码可以 正常 执行(未修改部分使用 ... 省略):
...
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 遍历终点
if (state->state >= self.internalAllObjects.count) {
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
state->mutationsPtr = (unsigned long *)(__bridge void*)self; /// 新增一行代码
return 1;
}
...
执行,一切正常。但是,我们的 mutationsPtr 设置的真的正确吗?
测试一下,添加一行测试代码:
int main(int argc, const char * _Nullable argv[])
{
XXCustomSet *set = [XXCustomSet new];
for (NSUInteger index = 0; index < 30; index++) {
[set addObject:@(index)];
}
for (id number in set) {
if ([number isEqualToNumber:@15]) {
[set removeObject:number]; /// 在 number 为 15 的时候,删除一个元素
}
NSLog(@"%@", number);
}
return 0;
}
执行代码之后,没有崩溃,因为我们做了保护,所以数组不会越界,但是我们在遍历完 15 之后,数组长度变了,导致了后续的 state->state 下标取出来的数据,都大了 1,所以 log 完 15 之后,直接 log 了 17,把 16 吃掉了!!!所以, Mike Ash 文章中 state->mutationsPtr = (unsigned long *) self,并不能保证,在迭代中修改容器的操作会在编译器 crash 出来(就像 NSMutableArray 那样)。
好吧,那么我们该如何设置一个合适的值呢?这里我的一个简单(low bee)的想法,我们维护一个变量,用来判断是否有在迭代中修改容器的行为,改动后代码如下:
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 内部实际存储的数组
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
/// 新增:添加一个用于判断是否在迭代中修改容器的指针,在这,存一个编辑版本号
@property (nonatomic, unsafe_unretained) unsigned long *editingVersion;
@end
@implementation XXCustomSet
#pragma mark - Deinit
/// /// 新增:dealloc 的时候,手动释放一下我们申请的内存
- (void)dealloc
{
free(_editingVersion);
}
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
/// 新增,申请一份内存,用来存版本号
_editingVersion = malloc(sizeof(unsigned long));
*_editingVersion = 0;
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
/// 新增:每次修改的时候,让版本号自增1
(*self.editingVersion) += 1;
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
/// 新增:每次修改的时候,让版本号自增1
(*self.editingVersion) += 1;
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 遍历终点
if (state->state >= self.internalAllObjects.count) {
/// 遍历结束,将编辑版本置回 0
*(self.editingVersion) = 0;
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
/// 新增:将 mutationsPtr 设置为版本号
state->mutationsPtr = self.editingVersion;
return 1;
}
@end
NS_ASSUME_NONNULL_END
再次执行我们的测试代码,在迭代中修改容器,结果
2021-01-08 19:57:21.198900+0800 algorithm[46120:1777121] 0
2021-01-08 19:57:21.199299+0800 algorithm[46120:1777121] 1
2021-01-08 19:57:21.199336+0800 algorithm[46120:1777121] 2
2021-01-08 19:57:21.199360+0800 algorithm[46120:1777121] 3
2021-01-08 19:57:21.199379+0800 algorithm[46120:1777121] 4
2021-01-08 19:57:21.199397+0800 algorithm[46120:1777121] 5
2021-01-08 19:57:21.199418+0800 algorithm[46120:1777121] 6
2021-01-08 19:57:21.199447+0800 algorithm[46120:1777121] 7
2021-01-08 19:57:21.199473+0800 algorithm[46120:1777121] 8
2021-01-08 19:57:21.199516+0800 algorithm[46120:1777121] 9
2021-01-08 19:57:21.199559+0800 algorithm[46120:1777121] 10
2021-01-08 19:57:21.199603+0800 algorithm[46120:1777121] 11
2021-01-08 19:57:21.199633+0800 algorithm[46120:1777121] 12
2021-01-08 19:57:21.199667+0800 algorithm[46120:1777121] 13
2021-01-08 19:57:21.199700+0800 algorithm[46120:1777121] 14
2021-01-08 19:57:21.199742+0800 algorithm[46120:1777121] 15
2021-01-08 19:57:21.203778+0800 algorithm[46120:1777121] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection was mutated while being enumerated.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff3039db57 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff692305bf objc_exception_throw + 48
2 CoreFoundation 0x00007fff3041630b __NSFastEnumerationMutationHandler + 159
3 algorithm 0x0000000100002f99 main + 457
4 libdyld.dylib 0x00007fff6a3d8cc9 start + 1
5 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection was mutated while being enumerated.'
terminating with uncaught exception of type NSException
在遍历完 15 之后,一个 NSGenericException 异常抛了出来。
现在我们的 XXCustomSet 容器可以在 for...in 语句中,一次返回一个元素。当然,我们也可以一次返回多个元素,比如 buffer 能容纳的最大长度 (仅列出改动代码,其余 ... 省略 ):
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 当前使用 buffer 的长度
NSUInteger usedBufferLen = 0;
/// 遍历终点
if (state->state >= self.internalAllObjects.count) {
*(self.editingVersion) = 0;
return 0;
}
/// 这个 for 有点奇怪,因为它的条件语句有点长,index < len 是判断 index 不能超过 buffer 的长度, state->state < self.internalAllObjects.count 是为了正确的读取到容器的最后一个元素,不至于越界
for (NSUInteger index = 0; (index < len) && (state->state < self.internalAllObjects.count); index++) {
buffer[index] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
state->mutationsPtr = self.editingVersion;
/// 记录当前这一次,使用到的 buffer 的长度,我们的例子中,第一次使用了 16 个 buffer 长度,第二次使用了 14 个,少两个没有用到,当然我们不能在这里假设 buffer 的长度一定是 16 ,还是要用 len 参数
usedBufferLen+= 1;
}
/// 返回当前这一次数据的长度
return usedBufferLen;
}
运行代码, 一切都 ok 了。其实我们的测试代码写的有点刻意。我们内部已经持有了一个数组了,我们完全可以把消息直接发给数组:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
return [self.internalAllObjects countByEnumeratingWithState:state objects:buffer count:len];
}
一些思考
关于 mutationsPtr
这里其实可能不需要一个类似于版本号的代码,比如我们用链表来实现一个栈,我们内部可以简单的将这个指针指向我们链表的栈顶(如果我们的栈是真正的只在栈顶做操作,并且相同的元素会有不同地址的节点来保存,保证每个节点的内存地址不同),每次修改栈的时候,因为栈顶都会变化,所以就不需要版本号了。版本号的代码,还可以优化一下,在遍历的时候创建一个版本号,在遍历结束的时候释放。为什么我们每次都将 state->itemsPtr 设置为 buffer,用别的指针不行么?
其实是可以的,代码上都可以执行。比如我们内部就是用这样的指针来维护,那么我们完全可以不用 buffer,也不需要管 len,一次性就可以返回所有的元素;我们也可以自己申请一段内存,然后注意释放,代码如下:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 初始状态,我们创建一个能够容纳我们数组的地址,并将指针存到 state->extra 中
if (state->state == 0) {
id __unsafe_unretained * itemsPtr = (id __unsafe_unretained *)malloc(sizeof(id) * self.internalAllObjects.count);
/// 将数组中的每一个元素,放进我们创建的 itemsPtr 中
for (NSUInteger index = 0; index < self.internalAllObjects.count; index++) {
itemsPtr[index] = self.internalAllObjects[index];
}
/// 将 state->itemsPtr 指向我们创建的 itemsPtr
state->itemsPtr = itemsPtr;
/// 存一下我们创建的 itemsPtr ,在结束的时候,释放内存,防止泄露
/// 注:看代码,我们好像也可以不用 extra 字段,在结束的时候直接释放 state->itemsPtr ,但是如果系统修改了这个字段(虽然现在好像没有),就挂了,所以我们将创建的 itemsPtr 存入约定的、给我们自己随意玩的 extra 字段中
state->extra[0] = (unsigned long)itemsPtr;
/// 设置 mutationsPtr 和之前一样
state->mutationsPtr = self.editingVersion;
/// 修改 state->state 状态,再次进入方法的时候,走到遍历结束的分支
state->state = 1; /// 随意的值,1000 都可以
return self.internalAllObjects.count;
}
/// 一次遍历就结束了,所以,如果 state->state 不是 0 ,我们直接清理内存,然后返回 0
*(self.editingVersion) = 0;
id __unsafe_unretained * storedItemsPtr = (id __unsafe_unretained *)(void*)state->extra[0];
free(storedItemsPtr);
return 0;
}
- NSEnumerator 还记文章开头,关于 NSFastEnumeration 中的注释么?
The abstract class NSEnumerator provides a convenience implementation that uses nextObject to return items one at a time. 抽象的 NSEnumerator 类,提供了使用 nextObject 在同一时间返回对象的便利实现。
我们也可以实现我们容器自己的 enumerator ,来一次返回一个对象。这个大家自己尝试吧。
一个可以运行的 demo
我比较懒,就给出一个可执行的 demo 吧,也就是我们一次返回一个元素的版本:
首先是类的声明:
#import
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet<__covariant ObjectType> : NSObject
/// 返回容器中的所有元素
@property (nonatomic, copy, readonly, nullable) NSArray *allObjects;
#pragma mark - Public
/// 向容器中添加一个对象
- (void)addObject:(ObjectType)object;
/// 移除容器中的一个对象
- (void)removeObject:(ObjectType)object;
@end
NS_ASSUME_NONNULL_END
接着是类的实现:
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 内部实际存储的数组
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
/// 添加一个用于判断是否在迭代中修改容器的指针,在这,存一个编辑版本号
@property (nonatomic, unsafe_unretained) unsigned long *editingVersion;
@end
@implementation XXCustomSet
#pragma mark - Deinit
- (void)dealloc
{
free(_editingVersion);
}
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
_editingVersion = malloc(sizeof(unsigned long));
*_editingVersion = 0;
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
(*self.editingVersion) += 1;
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
(*self.editingVersion) += 1;
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 遍历终点
if (state->state >= self.internalAllObjects.count) {
*(self.editingVersion) = 0;
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
state->mutationsPtr = self.editingVersion;
return 1;
}
@end
NS_ASSUME_NONNULL_END
写在最后
如有错误,或者好的想法,大家可以在评论区发言,我尽可能积极配合。
愿大家平安,健康。 Bye~