如何实现一个自己的for...in...操作呢

需求:对于一个自定义类如何也可以想和NSArrayNSDictionary一样可以直接遍历?

本篇目录:

  1. 解析系统for...in...的实现原理;
  2. 自己实现一个for...in...的类;
  3. 简单解释一下objc_enumerationMutation是如何抛出异常的。

1. 解析系统for...in...的实现原理

我们来看看苹果在2.0推出来的Fast Enumeration。
引用苹果官方文档的一段总结

The enumeration is more efficient than using NSEnumerator directly
The syntax is concise.
The enumerator raises an exception if you modify the collection while enumerating.
You can perform multiple enumerations concurrently.

翻译过来就是

  • 它比之前的NSEnumerator更高效
  • 语法更简洁
  • 如果这个集合在遍历的过程中修改了,会抛出异常
  • 可以同时执行对个枚举

Fast Enumeration是一个协议

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state 
                                  objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer 
                                    count:(NSUInteger)len;

@end

这个方法的作用是根据具体数据个数返回一定数量的数组供调用者使用的,为什么是一定数量的数组呢,比如说数据源是有5个数据[@"1",@"2",@"3",@"4",@"5"],若调用者想要2个一组,那么需要3组才能完成;前面提到的一组,其实就是C语言的数组,而这个方法就是用来确定返回一个怎样的数组,方法的返回值就是对应数组的长度。

这个协议方法传3个参数分别是:

  1. state,它是个结构体;
```
 typedef struct {
        unsigned long state;
        id __unsafe_unretained _Nullable * _Nullable itemsPtr;
        unsigned long * _Nullable mutationsPtr;
        unsigned long extra[5];
    } NSFastEnumerationState;
```
  * `state`这个参数在for...in...的方法内部是没有使用的,是留给调用者备用的,用来记录一些状态;
  * `itemsPtr`就是C数组的指针,它和方法的返回共同构成了C语言数组;
  * `mutationsPtr`这个字段是用来记录在遍历的过程中,被遍历的对象有没有被改变,从而可以抛出异常;
  * `extra`这个和`state`字段一样,在for...in...的方法内部是没有使用的也是没有使用的,留给调用者使用的。
  1. buffer它是一个缓冲区,其实是一个C数组,因为在内存中不是所有的对象都是内存连续的,针对那些内存不连续,方法提供一个内存区域,调用者把数组都放到这个缓冲区,他的长度由len决定;
  2. len上面已经提到就是定义buffer长度的。

为何我会这样解释这些属性呢,我们来看看for...in...的C++实现,就可以一一验证上面的说法了

先写一个demo

#import <Foundation/Foundation.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        NSMutableArray *arry = [NSMutableArray arrayWithObjects:@"1",@"2",@"3", nil];
        for (NSString *str  in arry) {
            NSLog(@"dddd---%@",str);
        }
        return 0;
        
}

使用命令

clang -rewrite-objc main.m

可以得到


int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSMutableArray *arry = ((NSMutableArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObjects:"), (id)(NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_2, __null);
        {
    NSString * str;
    
    // 这个就是传出去的State
    struct __objcFastEnumerationState enumState = { 0 };
    
    //这个就是buffer,可以看出是初始化了一个16长度的数组
    id __rw_items[16];
    
    //这个表示调用哪个对象的"countByEnumeratingWithState:objects:count:"方法
    id l_collection = (id) arry;
    
    //limit就是countByEnumeratingWithState:objects:count:返回值
    _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) {
    
    // 这里面有两个do...while...循环
    // 外层循环用来获取一共需要的数组的个数,并获取对应数组的长度
    // 内部循环是遍历获取对应数组的元素进行下一步操作
    
            //startMutations就是用来监控遍历的过程中遍历对象有没有改变
           unsigned long startMutations = *enumState.mutationsPtr;
            do {
                      unsigned long counter = 0;
                do {
                
                //如果遍历对象发生了改变就会调用`objc_enumerationMutation`来抛出异常
                    if (startMutations != *enumState.mutationsPtr)
                        objc_enumerationMutation(l_collection);
                        
                    // 取出对应的元素,这是高效快速的关键
                    str = (NSString *)enumState.itemsPtr[counter++]; 
                    {
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_3,str);
                };
                // 结束这次循环,进行下一次
            __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)));
            str = ((NSString *)0);
            __break_label_1: ;
    }else
        str = ((NSString *)0);
    }

        return 0;


    }
}


对于上面的源码进行了一些必要的注释帮助大家理解,整个方法下来,并没有看到stateextra字段,这也验证了之前的说法。

2. 自己实现一个for...in...的类

这里参照苹果的官方demo写了一个简单的例子
对于countByEnumeratingWithState:objects:count:我们有两种方法来实现

  1. 对于在内存中连续的结合来说可以直接返回这段内存的首地址;
  2. 对于不连续的来说,这个时候就要使用buffer了,接下来分别给出两种方式

.h

#import <Foundation/Foundation.h>

@interface MyFastIterator : NSObject<NSFastEnumeration>


@end

.m

#import "MyFastIterator.h"
#include <vector>
#import <objc/message.h>

@interface MyFastIterator ()

@property (nonatomic, strong) NSArray *myArray;

@property (nonatomic, assign) long tagSi;

@end

@implementation MyFastIterator
{
    std::vector<NSNumber *> _list;
}

- (instancetype)init {
    if (self = [super init]) {
        for (NSUInteger i = 0; i < 17; i++) {
            _list.push_back(@(i));
        }
        
        self.myArray = @[@"1",@"2",@"3",@"4",@"5",@"6",
                         @"1",@"2",@"3",@"4",@"5",@"6",
                         @"1",@"2",@"3",@"4",@"5",
                         ];
        
    
        
    }
    return self;
}


countByEnumeratingWithState:objects:count:实现


#define USE_BUFF 1

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id  _Nullable __unsafe_unretained [])buffer count:(NSUInteger)len {
 NSUInteger count = 0;
    
    unsigned long countOfItemsAlreadyEnumerated = state->state;
    
    if (countOfItemsAlreadyEnumerated == 0 ) {// 等于0说明是第一次调用可以初始化一些数据
        NSLog(@"countByEnumeratingWithState");
        // 上面已经说到,mutationsPtr是用来记录遍历的过程中被遍历的对象有没有被修改的
        // 由于我们这里是NSArray是不可变的,所以无需追踪他的改变
        // 从而这里取 的是 &state->extra[0];
        state->mutationsPtr = &state->extra[0];
    }
#if USE_BUFF
    if (countOfItemsAlreadyEnumerated < self.myArray.count) {
        state->itemsPtr = buffer;
        
        while (countOfItemsAlreadyEnumerated < self.myArray.count && count < len){
//            NSLog(@"--%d",count);
            buffer[count] = self.myArray[countOfItemsAlreadyEnumerated];
            countOfItemsAlreadyEnumerated++;
            count++;
        }
    }else {
        count = 0;
    }
#else
    
    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 中的元素个数
        
        //  这里使用的是官方demo的写法
        countOfItemsAlreadyEnumerated = _list.size();
        count = _list.size();
    }else {
        count = 0;
    }
    
#endif
    
    state->state = countOfItemsAlreadyEnumerated;
    return count;

}

外面调用

- (void)testMyFastIterator {
    MyFastIterator *fast = [[ MyFastIterator alloc] init];
    for (NSNumber *num in fast) {
        NSLog(@"testMyFastIterator---%@",num);
    }
}

其实还有一种简单的写法,直接返回要遍历的对象的方法,前提是遍历的对象实现了countByEnumeratingWithState方法

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id  _Nullable __unsafe_unretained [])buffer count:(NSUInteger)len {
    
   return [self.myArray countByEnumeratingWithState:state objects:buffer count:len];
}

3. 简单解释一下objc_enumerationMutation是如何抛出异常的。

objc_enumerationMutation方法是如何抛出异常的呢,打开objc4-646的源码中可以看到具体实现



static void (*enumerationMutationHandler)(id);


void objc_enumerationMutation(id object) {
    if (enumerationMutationHandler == nil) {
        _objc_fatal("mutation detected during 'for(... in ...)'  enumeration of object %p.", (void*)object);
    }
    (*enumerationMutationHandler)(object);
}


void objc_setEnumerationMutationHandler(void (*handler)(id)) {
    enumerationMutationHandler = handler;
}

阅读源码可以得出看出:

  1. objc_setEnumerationMutationHandler方法接收一个函数指针,保存在内部定义的之前声明好的函数static void (*enumerationMutationHandler)(id);
  2. objc_enumerationMutation被调用的时候,如果调用者没有实现objc_setEnumerationMutationHandler的话,此时函数指针enumerationMutationHandler为nil,就会执行_objc_fatal("mutation detected during 'for(... in ...)' enumeration of object %p.", (void*)object);,否则就会通过*enumerationMutationHandler拿到函数并把object传递出去。

我们来看一个demo

//先初始化一个函数
void voidVoidTest(id objt) {
    NSLog(@"%@挂啦",objt);
}

- (void)testMutation {
    void (*funcVoidVoid)() = &voidVoidTest;
    objc_setEnumerationMutationHandler(funcVoidVoid);
    NSString *str = @"test";
    objc_enumerationMutation(str);
   } 
   

输出

test挂啦
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容