前言:集合类在每门编程语言中都有着非常重要的地位,每一门语言对于集合类的实现和提供的API也大同小异。相比于Java、C#等编程语言Objective-C相对薄弱一些,不过笔者用久了了就慢慢习惯了,思考也少了。之前看
RACSequence
源码时惊叹于其中的设计之余也遇到好多模糊的知识点,NSFastEnumeration
就是其中之一,在此做下学习笔记,至于RAC的源码阅读和分析日后再分章节奉上。
首先看一下forin遍历的日常使用代码:
NSUInteger idx = 0;
NSArray *array = @[@"A",@"B",@"C"];
for(id item in array)
{
NSLog(@"Item %li = %@", idx++, item);
}
//运行结果如下
Item 0 = A
Item 1 = B
Item 2 = C
代码再简单不过了,相比于普通的foreach
循环forin
可以直接得到列表中存储的对象,语法类似满足日常的使用习惯。foreach、forin以及-(void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
交替使用几乎完全满足了我们日常编码需求。可是当我们再往深里思考一下,这些语法糖背后的逻辑是什么?是不是都是基于相同的底层代码实现的呢?当自定义的对象也需要支持这样的语法糖该怎么办?接下来就一起看看forin
背后的原理。
打开NSArray
的头文件,首先看到的是下面一行的代码:
@interface NSArray<__covariant ObjectType> : NSObject
很显然NSArray
类实现NSFastEnumeration
协议,正是NSFastEnumeration
使得NSArray
支持forin
这样的语法糖。打开NSFastEnumeration
头文件(如下)可以看到其中只包含一个方法和一个结构体NSFastEnumerationState
:
typedef struct {
unsigned long state;
id __unsafe_unretained __nullable * __nullable itemsPtr;
unsigned long * __nullable mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;
@protocol NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;
@end
代码看起来有点懵,因为头文件中没有任何有关方法使用的注释和结构体的介绍。没办法只能去搜API文档(点此前往),下面我贴出来API介绍的片段:
依然看不出太多的信息,在继续往下看之前我们不妨根据已有信息做如下的思考:1.forin
只是语法糖,背后遍历使用的肯定是原始的循环结构do-while/while
? 2.数组的下标索引+
原始的do-while/while
循环结构如何实现数组的遍历?3.再考虑空间利用率该如何优化?
继续搜索文档找到了NSFastEnumeration
是Sample文档:
果断下载下来并用Xocde打开,里面有如下三个文件:
EnumerationSample
是
main
调试方法可以忽略。重点分析
EnumerableClass.h
和
EnumerableClass.mm
两个文件。打开头文件果然里面有非常详细的注释(这里就不贴出来),接口定义
@interface EnumerableClass : NSObject
实现了
NSFastEnumeration
协议。此外还实现了对象支持下标索引和block遍历的API,代码很简单可以直接查看文档,这里只关注
NSFastEnumeration
的协议方法,直奔主题找到对应的实现方法如下(注释太多,删除了原先英文注释):
#define USE_STACKBUF 1
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained [])stackbuf
count:(NSUInteger)stackbufLength
{
NSUInteger count = 0;
unsigned long countOfItemsAlreadyEnumerated = state->state;
if(countOfItemsAlreadyEnumerated == 0)
{
state->mutationsPtr = &state->extra[0];
}
#if USE_STACKBUF // Method One.
if(countOfItemsAlreadyEnumerated < _list.size())
{
state->itemsPtr = stackbuf;
while((countOfItemsAlreadyEnumerated < _list.size()) && (count < stackbufLength))
{
stackbuf[count] = _list[countOfItemsAlreadyEnumerated];
countOfItemsAlreadyEnumerated++;
count++;
}
}
else
{
count = 0;
}
#else // Method Two.
if (countOfItemsAlreadyEnumerated < _list.size())
{
__unsafe_unretained const id * const_array = _list.data();
state->itemsPtr = (__typeof__(state->itemsPtr))const_array;
count = _list.size();
countOfItemsAlreadyEnumerated = _list.size();
}
else
{
count = 0;
}
#endif
state->state = countOfItemsAlreadyEnumerated;
return count;
}
分析一下上面的代码:
1.countByEnumeratingWithState
方法的实现有两种方式,例子代码中使用宏USE_STACKBUF
进行了区分,两种方式的主要区别是:第一种方式是利用了参数中的缓冲数组,第二种方式没有使用缓冲数组而是直接持有了原数组的一个弱引用。笔者感觉第二种是不安全的,因此大多情况的实现应该是使用第一中方式。
2.仔细阅读第一种方式的实现代码,可以看出:a.countByEnumeratingWithState
肯定是被外部循环调用的,使用结构体指针NSFastEnumerationState
记录当前最新的遍历状态,循环的次数取决于参数stackbufLength
缓冲区的大小。b.NSFastEnumerationState
中state
用来记录最后遍历的位置,itemsPtr
指向缓冲区数组,mutationsPtr
和extra
是用来检测和控制原始数组改变的,具体使用例子中并没有给出更多信息,待以后探索。c.当最终返回'count = 0'是表示遍历结束。
小结:
forin
是通过外部传入一个缓冲数组,在countByEnumeratingWithState
方法体内通过外部的不断循环调用根据state状态重复填充缓冲区数组并返回结果。那么外部是究竟是如何设置缓冲区大小的?如何遍历countByEnumeratingWithState
方法?
带着上面的疑问来查看一下编译器翻译后的forin
代码如下:
int main (int argc, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
EnumerableClass *example = ((id (*)(id, SEL, NSUInteger))(void *)objc_msgSend)((id)((EnumerableClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("EnumerableClass"), sel_registerName("alloc")), sel_registerName("initWithCapacity:"), (NSUInteger)50);
NSUInteger idx = 0;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_lv_h4gknfp17nq8wm59sqwgtgjm0000gn_T_EnumerationSample_88076b_mi_0);
idx = 0;
{
id item;
struct __objcFastEnumerationState enumState = { 0 };
id __rw_items[16];
id l_collection = (id) example;
_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);
item = (id)enumState.itemsPtr[counter++];
{
NSLog((NSString *)&__NSConstantStringImpl__var_folders_lv_h4gknfp17nq8wm59sqwgtgjm0000gn_T_EnumerationSample_88076b_mi_1, idx++, item);
};
__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)));
item = ((id)0);
__break_label_1: ;
}
else
item = ((id)0);
}
}
return 0;
}
分析代码:
1.缓冲区数组初始化固定的大小为16
的数组__rw_items
。
2.通过两层的do-while
嵌套循环实现了数组的遍历,回答了前面的设想的问题。
3.方法objc_enumerationMutation
只有遍历过程中对象被改变了才会被调用,这一点还没有深入研究。
4.相比于while
循环效率上会有一定的损失。
总结:通过
forin
具体实现代码的阅读,相信以后再去实现NSFastEnumeration
协议时肯定是胸有成竹了,这里只是简单的记录学习心得,大家有新的发现和想法欢迎一起讨论。