在 Objective-C 2.0 中提供了快速枚举的语法,它是我们遍历集合元素的首选方法。它具有以下优点:

  1. 比直接使用 NSEnumerator 更高效;
  2. 语法非常简洁;
  3. 如果集合在遍历的过程中被修改,它会抛出异常;
  4. 可以同时执行多个枚举。

一、解析 NSFastEnumeration 协议

在 Objective-C 中,我们要想实现快速枚举就必须要实现 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. @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. @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. @return The number of objects returned in stackbuf. Returns 0 when the iteration is finished.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer
count:(NSUInteger)len

结构体 NSFastEnumerationState 的定义如下:

typedef struct {
unsigned long state;
id __unsafe_unretained _Nullable * _Nullable itemsPtr;
unsigned long * _Nullable mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;

这个方法的目的是什么?

概括地说,这个方法就是用于返回一系列的 C 数组,以供调用者进行遍历。

为什么是一系列的 C 数组呢?因为,在一个 for/in 循环中,这个方法其实会被调用多次,每一次调用都会返回一个 C 数组。至于为什么是 C 数组,那当然是为了提高效率了。

既然要返回 C 数组,也就意味着我们需要返回一个数组的指针和数组的长度。而数组的长度就是通过这个方法的返回值来提供的,而数组的指针则是通过结构体 NSFastEnumerationStateitemsPtr 字段进行返回的。所以,这两个值就一起定义了这个方法返回的 C 数组。

通常来说,NSFastEnumeration 允许我们直接返回一个指向内部存储的指针,但是并非所有的数据结构都能够满足内存连续的要求。因此,NSFastEnumeration 还为我们提供了另外一种实现方案,我们可以将元素拷贝到调用者提供的一个 C 数组上,即 buffer ,它的长度由参数 len 指定。

“如果集合在遍历的过程中被修改的话,NSFastEnumeration 就会抛出异常”,这个功能是通过 mutationsPtr 字段来实现的,它指向一个这样的值,这个值在集合被修改时会发生改变。因此,我们就可以利用它来判断集合在遍历的过程中是否被修改。

NSFastEnumerationState 中的 stateextra 是调用者提供给被调用者自由使用的两个字段,调用者根本不关心这两个字段的值。因此,我们可以利用它们来存储任何对自身有用的值。

二、揭密快速枚举的内部实现

自定义 main.m 文件,代码如下:

#import <Foundation/Foundation.h>

int main(int argc, char * argv[]) {
NSArray *array = @[ @1, @2, @3 ];
for (NSNumber * number in array) {
if ([number isEqualToNumber:@1]) {
continue;
}
NSLog(@"%@", number);
break;
}
}

接着,我们使用下面的 clang 命令将 main.m 文件重写成 C++ 代码:

clang -rewrite-objc main.m

生成 main.cpp 文件,其中 main 函数的代码如下:

int main(int argc, char * argv[]) {
// 创建数组 @[ @1, @2, @3 ]
NSArray *array = ((NSArray *(*)(Class, SEL, const ObjectType *, 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:"), 1), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 3)).arr, 3U); {
NSNumber * number; // 初始化结构体 NSFastEnumerationState
struct __objcFastEnumerationState enumState = { 0 }; // 初始化数组 stackbuf
id __rw_items[16]; id l_collection = (id) array; // 第一次调用 - countByEnumeratingWithState:objects:count: 方法,形参和实参的对应关系如下:
// state -> &enumState
// stackbuf -> __rw_items
// len -> 16
_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) {
// 获取 mutationsPtr 的初始值
unsigned long startMutations = *enumState.mutationsPtr; // 外层的 do/while 循环,用于调用 - countByEnumeratingWithState:objects:count: 方法,获取 C 数组
do {
unsigned long counter = 0; // 内层的 do/while 循环,用于遍历获取到的 C 数组
do {
// 判断 mutationsPtr 的值是否有发生变化,如果有则使用 objc_enumerationMutation 函数抛出异常
if (startMutations != *enumState.mutationsPtr) objc_enumerationMutation(l_collection); // 使用指针的算术运算获取相应的集合元素,这是快速枚举之所以高效的关键所在
number = (NSNumber *)enumState.itemsPtr[counter++]; {
if (((BOOL (*)(id, SEL, NSNumber *))(void *)objc_msgSend)((id)number, sel_registerName("isEqualToNumber:"), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1))) {
// continue 语句的实现,使用 goto 语句无条件转移到内层 do 语句的末尾,跳过中间的所有代码
goto __continue_label_1;
} NSLog((NSString *)&__NSConstantStringImpl__var_folders_cr_xxw2w3rd5_n493ggz9_l4bcw0000gn_T_main_fc7b79_mi_0, number); // break 语句的实现,使用 goto 语句无条件转移到最外层 if 语句的末尾,跳出嵌套的两层循环
goto __break_label_1;
}; // goto 语句标号,用来实现 continue 语句
__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))); number = ((NSNumber *)0); // goto 语句标号,用来实现 break 语句
__break_label_1: ;
} else {
number = ((NSNumber *)0);
}
}
}

如上代码所示,快速枚举其实就是用两层 do/while 循环来实现的,外层循环负责调用 - countByEnumeratingWithState:objects:count: 方法,获取 C 数组,而内层循环则负责遍历获取到的 C 数组。同时,我想你应该也注意到了它是如何利用 mutationsPtr 来检测集合在遍历过程中的突变的,以及使用 objc_enumerationMutation 函数来抛出异常。

正如前面提到的,在快速枚举的实现中,确实没有用到结构体 NSFastEnumerationState 中的 state 和 extra 字段,它们只是提供给 - countByEnumeratingWithState:objects:count: 方法的实现者自由使用的字段。

值得一提的是,在 main.m 中加入了 continue 和 break 语句。因此,我们有机会看到了在 for/in 语句中是如何利用 goto 来实现 continue 和 break 语句的。

三、实现 NSFastEnumeration 协议

NSFastEnumeration 在设计上允许我们使用两种不同的方式来实现它。如果集合中的元素在内存上是连续的,那么我们可以直接返回这段内存的首地址;如果不连续,比如链表,就只能使用调用者提供的 C 数组 buffer 了,将我们的元素拷贝到这个 C 数组上。

接下来,我们将通过一个自定义的集合类 Array ,来演示这两种不同的实现 NSFastEnumeration 协议的方式。注:完整的项目代码这里

@interface Array : NSObject <NSFastEnumeration>

- (instancetype)initWithCapacity:(NSUInteger)numItems;

@end

@implementation Array {
std::vector<NSNumber *> _list;
} - (instancetype)initWithCapacity:(NSUInteger)numItems {
self = [super init];
if (self) {
for (NSUInteger i = 0; i < numItems; i++) {
_list.push_back(@(random()));
}
}
return self;
} #define USE_STACKBUF 1 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])stackbuf count:(NSUInteger)len {
// 这个方法的返回值,即我们需要返回的 C 数组的长度
NSUInteger count = 0; // 我们前面已经提到了,这个方法是会被多次调用的
// 因此,我们需要使用 state->state 来保存当前遍历到了 _list 的什么位置
unsigned long countOfItemsAlreadyEnumerated = state->state; // 当 countOfItemsAlreadyEnumerated 为 0 时,表示第一次调用这个方法
// 我们可以在这里做一些初始化的设置
if (countOfItemsAlreadyEnumerated == 0) {
// 我们前面已经提到了,state->mutationsPtr 是用来追踪集合在遍历过程中的突变的
// 它不能为 NULL ,并且也不应该指向 self
//
// 这里,因为我们的 Array 类是不可变的,所以我们不需要追踪它的突变
// 因此,我们的做法是将它指向 state->extra 的其中一个值
// 因为我们知道 NSFastEnumeration 协议本身并没有用到 state->extra
//
// 但是,如果你的集合是可变的,那么你可以考虑将 state->mutationsPtr 指向一个内部变量
// 而这个内部变量的值会在你的集合突变时发生变化
state->mutationsPtr = &state->extra[0];
} #if USE_STACKBUF // 判断我们是否已经遍历完 _list
if (countOfItemsAlreadyEnumerated < _list.size()) {
// 我们知道 state->itemsPtr 就是这个方法返回的 C 数组指针,它不能为 NULL
// 在这里,我们将 state->itemsPtr 指向调用者提供的 C 数组 stackbuf
state->itemsPtr = stackbuf; // 将 _list 中的元素填充到 stackbuf 中,直到以下两个条件中的任意一个满足时为止
// 1. 已经遍历完 _list 中的所有元素
// 2. 已经填充满 stackbuf
while (countOfItemsAlreadyEnumerated < _list.size() && count < len) {
// 取出 _list 中的元素填充到 stackbuf 中
stackbuf[count] = _list[countOfItemsAlreadyEnumerated]; // 更新我们的遍历位置
countOfItemsAlreadyEnumerated++; // 更新我们返回的 C 数组的长度,使之与 state->itemsPtr 中的元素个数相匹配
count++;
}
} #else // 判断我们是否已经遍历完 _list
if (countOfItemsAlreadyEnumerated < _list.size()) {
// 直接将 state->itemsPtr 指向内部的 C 数组指针,因为它的内存地址是连续的
__unsafe_unretained const id * const_array = _list.data();
state->itemsPtr = (__typeof__(state->itemsPtr))const_array; // 因为我们一次性返回了 _list 中的所有元素
// 所以,countOfItemsAlreadyEnumerated 和 count 的值均为 _list 中的元素个数
countOfItemsAlreadyEnumerated = _list.size();
count = _list.size();
} #endif // 将本次调用得到的 countOfItemsAlreadyEnumerated 保存到 state->state 中
// 因为 NSFastEnumeration 协议本身并没有用到 state->state
// 所以,我们可以将这个值保留到下一次调用
state->state = countOfItemsAlreadyEnumerated; // 返回 C 数组的长度
return count;
} @end

值得一提的是,在第二种方式的实现中,我们用到了 ARC 下不同所有权对象之间的相互转换,代码如下:

__unsafe_unretained const id * const_array = _list.data();
state->itemsPtr = (__typeof__(state->itemsPtr))const_array;

其实,这里面涉及到两次类型转换,第一次是从 __strong NSNumber * 类型转换到 __unsafe_unretained const id * 类型,第二次是从 __unsafe_unretained const id * 类型转换到 id __unsafe_unretained * 类型,更多信息可以查看 AutomaticReferenceCounting 中的 4.3.3 小节。

四、内容来源

雷纯锋的技术博客 - Objective-C Fast Enumeration 的实现原理

Fast Enumeration的更多相关文章

  1. [报错]Fast enumeration variables cannot be modified in ARC by default; declare the variable __strong to allow this

    今天写了下面的快速枚举for循环代码,从按钮数组subButtons中取出button,然后修改button的样式,在添加到view中 for (UIButton *button in subButt ...

  2. Objective-C Fast Enumeration

    Fast enumeration is an Objective-C's feature that helps in enumerating through a collection. So in o ...

  3. Objective-C 高性能的循环

    Cocoa编程的一个通常的任务是要去循环遍历一个对象的集合  (例如,一个 NSArray, NSSet 或者是 NSDictionary). 这个看似简单的问题有广泛数量的解决方案,它们中的许多不乏 ...

  4. Objective-C 高性能的循环遍历 forin - NSEnumerator - 枚举 优化

    Cocoa编程的一个通常的任务是要去循环遍历一个对象的集合  (例如,一个 NSArray, NSSet 或者是 NSDictionary). 这个看似简单的问题有广泛数量的解决方案,它们中的许多不乏 ...

  5. 集合类(Objective-C & Swift)

    内容提要: 本文前两部分讲了Cocoa的集合类和Swift的集合类,其中Cocoa提供的集合类包括NSArray.NSMutableArray.NSDictionary.NSMutableDictio ...

  6. 词典对象 NSDictionary与NSMutableDictionary

    做过Java语言或者 C语言开发的朋友应该很清楚关键字map 吧,它可以将数据以键值对儿的形式储存起来,取值的时候通过KEY就可以直接拿到对应的值,非常方便,是一种非常常用的数据结构.在Objecti ...

  7. IOS常用加密GTMBase64

    GTMDefines.h // // GTMDefines.h // // Copyright 2008 Google Inc. // // Licensed under the Apache Lic ...

  8. Objective-C学习篇07—NSArray与NSMutableArray

    大纲 NSArray NSMutableArray 快速枚举 NSArray NSArray是一个静态数组,也就是一个不可变数组,一旦创建以后,就不能进行添加,删除或者修改其中的元素.NSArray继 ...

  9. iOS苹果官方Demo合集

    Mirror of Apple’s iOS samples This repository mirrors Apple’s iOS samples. Name Topic Framework Desc ...

随机推荐

  1. 大厂面试官问你META-INF/spring.factories要怎么实现自动扫描、自动装配?

    大厂面试官问你META-INF/spring.factories要怎么实现自动扫描.自动装配?   很多程序员想面试进互联网大厂,但是也有很多人不知道进入大厂需要具备哪些条件,以及面试官会问哪些问题, ...

  2. AOP面试知识整理,^_^-包括spring Aop

    讲到java企业级开发框架,就不可避免的讲到 IOC,AOP,MCV 今天面试时被问到AOP,讲的很乱,这里整理笔记,包括AOP,spring-AOP的部分知识,错误的地方请小伙伴指出来. 谈谈你对A ...

  3. 前端实现html转pdf方法总结

    最近要搞前端html转pdf的功能.折腾了两天,略有所收,踩了一些坑,所以做些记录,为后来的兄弟做些提示,也算是回馈社区.经过一番调(sou)研(suo)发现html导出pdf一般有这几种方式,各有各 ...

  4. scroll-view组件bindscroll实例应用:自定义滚动条

    我们知道scroll-view组件作为滑动控件非常好用,而有时候我们想放置一个跟随滚动位置来跟进的滚动条,但又不想用滚动条api该怎么办呢?(当然是自己写一个呗还能怎么办[自黑冷漠脸])嗯,没错.自己 ...

  5. volatile关键字的理解

    volatile是java语言提供的一种稍弱的同步机制,它的作用是能够保证被volatile修饰的变量,每个线程在获取它的值时都能获取到最新的值. 要理解这个原理首先要知道java内存模型:每个线程都 ...

  6. python学习(二)之turtle库绘图

    今天是三月七号,也就是女生节,或者女神节.不知道你是不是有自己喜欢的女孩子,在这里你可以用turtle库绘制一朵玫瑰花,送给你喜欢的姑娘.(拉到最后有惊喜哦)但在画这朵玫瑰花之前,先来一个基础的图形, ...

  7. 单页面和多页面的网页差别比较(SPA)

      单页面应用(singlePAge Web Application) 多页面应用MultiPage Applicaton,MPA) 组成 一个外壳页面和多个页面片段组成 多个完整的页面组成 资源公用 ...

  8. vue环境搭建过程中,npm权限不足问题

    今天在用git bash进行全局安装vue-cli的时候,报错: 必须以管理员权限进行安装才行.所以用cmd命令工具,点击右键命令提示符cmd--------以管理员身份运行--------cd进入到 ...

  9. python3.6 单文件爬虫 断点续存 普通版 文件续存方式

    # 导入必备的包 # 本文爬取的是顶点小说中的完美世界为列.文中的aa.text,bb.text为自己创建的text文件 import requests from bs4 import Beautif ...

  10. ML-Agents(二)创建一个学习环境

    ML-Agents(二)创建一个学习环境 一.前言 上一节我们讲了如何配置ML-Agents环境,这一节我们创建一个示例,主要利用Reinforcement Learning(强化学习). 如上图,本 ...