NSFastEnumeration使用原理分析

前言:集合类在每门编程语言中都有着非常重要的地位,每一门语言对于集合类的实现和提供的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介绍的片段:

NSFastEnumeration使用原理分析_第1张图片
countByEnumeratingWithState:objects:count:

NSFastEnumeration使用原理分析_第2张图片
NSFastEnumerationState

依然看不出太多的信息,在继续往下看之前我们不妨根据已有信息做如下的思考:1.forin只是语法糖,背后遍历使用的肯定是原始的循环结构do-while/while? 2.数组的下标索引+原始的do-while/while循环结构如何实现数组的遍历?3.再考虑空间利用率该如何优化?

继续搜索文档找到了NSFastEnumeration是Sample文档:

NSFastEnumeration使用原理分析_第3张图片
Sample Code

果断下载下来并用Xocde打开,里面有如下三个文件:
NSFastEnumeration使用原理分析_第4张图片
Project Files

EnumerationSamplemain调试方法可以忽略。重点分析 EnumerableClass.hEnumerableClass.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.NSFastEnumerationStatestate用来记录最后遍历的位置,itemsPtr指向缓冲区数组,mutationsPtrextra是用来检测和控制原始数组改变的,具体使用例子中并没有给出更多信息,待以后探索。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协议时肯定是胸有成竹了,这里只是简单的记录学习心得,大家有新的发现和想法欢迎一起讨论。

你可能感兴趣的:(NSFastEnumeration使用原理分析)