NSFastEnumeration

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 的实现。我们自己来操作一遍。

  1. 以 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;
}
  1. 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 引用获取。所以用到的参数/本地变量共有三个:

  1. -[id countByEnumeratingWithState:objects:count:] 的返回值,作为外层循环的条件;
  2. 通过 C 指针传入的 enumState 结构体,其中真正遍历的时候用到的字段共 2 个:
    1. mutationsPtr:每次迭代通过 == 判断是否有在迭代过程中修改容器的情况,如果有,抛出异常;
    2. 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 协议。

  1. 支持遍历

为了支持让外部可以用 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];
}

一些思考

  1. 关于 mutationsPtr
    这里其实可能不需要一个类似于版本号的代码,比如我们用链表来实现一个栈,我们内部可以简单的将这个指针指向我们链表的栈顶(如果我们的栈是真正的只在栈顶做操作,并且相同的元素会有不同地址的节点来保存,保证每个节点的内存地址不同),每次修改栈的时候,因为栈顶都会变化,所以就不需要版本号了。版本号的代码,还可以优化一下,在遍历的时候创建一个版本号,在遍历结束的时候释放。

  2. 为什么我们每次都将 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;
}
  1. 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~

你可能感兴趣的:(NSFastEnumeration)