iOS之深入解析数组遍历的底层原理和性能分析

一、OC数组的类体系

  • 当我们创建一个 NSArray 对象时,实际上得到的是 NSArray 的子类 __NSArrayI 对象。同样的,创建 NSMutableArray 对象,得到的同样是其子类 __NSArray 对象。

  • 有趣的是,当我们创建只有一个对象的 NSArray 时,得到的是__NSSingleObjectArrayI 类对象。

  • __NSArrayI 和 __NSArrayM,__NSSingleObjectArrayI 为框架隐藏的类。

  • OC数组的类体系如下:
    在这里插入图片描述

  • 通过 NSArray 和 NSMutableArray 接口,返回的却是子类对象,这是怎么回事呢?

    • NSArray 重写了+ (id)allocWithZone:(struct _NSZone *)zone方法,在方法内部,如果调用类为 NSArray 则直接返回全局变量 ___immutablePlaceholderArray,如果调用类为 NSMUtableArray 则直接返回全局变量 ___mutablePlaceholderArray。
      也就是调用 [NSArray alloc] 或者 [NSMUtableArray alloc] 得到的仅仅是两个占位指针,类型为__NSPlaceholderArray。
    • 在调用了 alloc 的基础上,不论是 NSArray 或 NSMutableArray 都必定要继续调用某个initXXX方法,而实际上调用的是 __NSPlaceholderArray 的 initXXX。在这个initXXX方法内部,如果self == ___immutablePlaceholderArray 就会重新构造并返回__NSArrayI 对象,如果self == ___mutablePlaceholderArray 就会重新构造并返回_NSArrayM对象。
    • 总结来说,对于 NSArray 和 NSMutableArray,alloc 时拿到的仅仅是个占位对象,init 后才得到真实的子类对象。
  • 特别说明:__NSPlaceholderArray 是一个私有类,___immutablePlaceholderArray、___mutablePlaceholderArray 是两个此类的全局变量。
    __NSPlaceholderArray 从类命名上看,它只是用来占位的;而__NSPlaceholderArray 实现了和 NSArray、NSMutableArray 一样的初始化方法,如initWithObjects:count:,initWithCapacity:等。

二、OC数组遍历方式

① for 循环
	for (NSUInteger i = 0;  i < array.count; i++) {
    
    
        object = array[i];
    }

array[I] 会被编译器转化为对- (ObjectType)objectAtIndexedSubscript:(NSUInteger)index 的调用,此方法内部调用的就是- (ObjectType)objectAtIndex:(NSUInteger)index方法。

② for in
	for (id obj in array) {
    
    
        
  	}
③ enumerateObjectsUsingBlock
	[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
       
  	}];
④ enumerateObjectsWithOptions:usingBlock:
  • 通过block回调,在子线程中遍历,对象的回调次序是乱序的,而且调用线程会等待该遍历过程完成:
	[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
        
  	}];
  • 通过block回调,在主线程中逆序遍历:
	[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
        
  	}];
⑤ objectEnumerator/reverseObjectEnumerator
  • 通过Enumerator顺序遍历:
	NSEnumerator *enumerator = array.objectEnumerator;
	while((object = enumerator.nextObject)) {
    
    
    
	}
  • 通过ReverseEnumerator逆序遍历:
	NSEnumerator *enumerator = array.reverseObjectEnumerator;
	while((object = enumerator.nextObject)) {
    
    
    
	}
⑥ enumerateObjectsAtIndexes:options:usingBlock:
  • 通过block回调,在子线程中对指定IndexSet遍历,对象的回调次序是乱序的,而且调用线程会等待该遍历过程完成:
	[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
	        
	 }];
  • 通过block回调,在主线程中对指定IndexSet逆序遍历:
	[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
	
	 }];

三、性能比较

  • 以100为步长,构造对象数目在0-100万之间的NSArray,分别用上述的遍历方式进行遍历并计时(单位us),而且在每一次遍历中,仅仅只是得到对象,没有其他任何输入输出,计算之类的干扰操作。每种遍历方式采集得1万组数据,得到如下的性能对比结果:
    在这里插入图片描述
  • 分析说明:
    • 横轴为遍历的对象数目,纵轴为耗时,单位us。
    • 从图中看出,在对象数目很小的时候,各种方式的性能差别微乎其微。随着对象数目的增大, 性能差异才体现出来。
    • 其中for in的耗时一直都是最低的,当对象数高达100万的时候,for in耗时也没有超过5ms。其次是for循环耗时较低。反而,直觉上应该非常快速的多线程遍历方式。
    • enumerateObjectsUsingBlock :和reverseObjectEnumerator的遍历性能非常相近。
	[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    
    
        
  	}];

四、OC数组内部结构

NSArray 和 NSMutableArray都没有定义实例变量,只是定义和实现了接口,且对内部数据操作的接口都是在各个子类中实现的,所以真正需要了解的是子类结构,__NSArrayI 相当于 NSArray,__NSArrayM 相当于 NSMutableArray。

① __NSArrayI
  • _NSArrayI的结构定义为:
	@interface __NSArrayI : NSArray {
    
    
	    NSUInteger _used;
	    id _list[0];
	}
	@end
  • 说明:
    • _used是数组的元素个数,调用[array count]时,返回的就是_used的值。
    • id _list[0]是数组内部实际存储对象的数组,但为何定义为0长度呢?我们可以把id _list[0] 当作 id *_list 来用,即一个存储 id 对象的 buff。由于__NSArrayI的不可变,所以_list一旦分配,释放之前都不会再有移动删除操作了,只有获取对象一种操作,因此__NSArrayI的实现并不复杂。
② __NSSingleObjectArrayI
  • __NSSingleObjectArrayI的结构定义为:
	@interface __NSSingleObjectArrayI : NSArray {
    
    
	    id object;
	}
	@end
  • 因为只有在"创建只包含一个对象的不可变数组"时,才会得到__NSSingleObjectArrayI对象,所以其内部结构更加简单,一个object足矣。
③ __NSArrayM
  • __NSArrayM的结构定义为:
	@interface __NSArrayM : NSMutableArray {
    
    
	    NSUInteger _used;
	    NSUInteger _offset;
	    int _size:28;
	    int _unused:4;
	    uint32_t _mutations;
	    id *_list;
	}
	@end
  • __NSArrayM 稍微复杂一些,但是同样的,它的内部对象数组也是一块连续内存id* _list,正如__NSArrayI的id _list[0]一样:
    • _used:当前对象数目;
    • _offset:实际对象数组的起始偏移;
    • _size:已分配的_list大小(能存储的对象个数,不是字节数);
    • _mutations:修改标记,每次对__NSArrayM的修改操作都会使_mutations加1,“*** Collection <__NSArrayM: 0x1002076b0> was mutated while being enumerated.”这个异常就是通过对_mutations的识别来引发的。
  • id *_list是个循环数组,并且在增删操作时会动态地重新分配以符合当前的存储需求。以一个初始包含5个对象,总大小_size为6的_list为例:
    _offset = 0, _used = 5, _size=6

在这里插入图片描述

  • 在末端追加3个对象后: _offset = 0, _used = 8, _size=8,_list已重新分配:

在这里插入图片描述

  • 删除对象A:_offset = 1, _used = 7, _size=8:

在这里插入图片描述

  • 删除对象E:_offset = 2, _used = 6, _size=8,B、C往后移动了,E的空缺被填补:
    在这里插入图片描述
  • 在末端追加两个对象:_offset = 2,_used = 8,_size=8,_list足够存储新加入的两个对象,因此没有重新分配,而是将两个新对象存储到了_list起始端:

在这里插入图片描述

  • 因此可见:__NSArrayM的_list是个循环数组,它的起始由_offset标识。

五、遍历内部实现

① 快速枚举
  • for in就是基于快速枚举实现的。先认识一个协议:NSFastEnumeration,它的定义在Foundation框架的NSFastEnumeration .h头文件中:
	@protocol NSFastEnumeration
	
	- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
	
	@end
  • NSFastEnumerationState定义,__NSArrayI,__NSArrayM,__NSSingleObjectArrayI都实现了NSFastEnumeration协议:
	typedef struct {
    
    
	    unsigned long state;
	    id __unsafe_unretained _Nullable * _Nullable itemsPtr;
	    unsigned long * _Nullable mutationsPtr;
	    unsigned long extra[5];
	} NSFastEnumerationState;

__NSArrayI的实现

  • 根据汇编反写可以得到:
 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    
    
    if (!buffer && len > 0) {
    
    
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    if (len >= 0x40000000) {
    
    
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    static const unsigned long mu = 0x01000000;
   
    if (state->state == 0) {
    
    
        state->mutationsPtr = μ
        state->state = ~0;
        state->itemsPtr = _list;
        return _used;
    }
    return 0;
}
  • 可见在__NSArrayI对这个方法的实现中,主要做的事就是把__NSArrayI的内部数组_list赋给state->itemsPtr,并返回_used即数组大小。state->mutationsPtr指向一个局部静态变量,state->state看起来是一个标志,如果再次用同一个state调用这个方法就直接返回0。至于传入的buffer、len仅仅只是用来判断了一下参数合理性。
  • 这样就把全部对象获取到了,而且在一个c数组里,之后要获得哪个位置的对象都可以快速寻址到,调用方通过state->itemsPtr来访问这个数组,通过返回值来确定数组里对象数目,这就跟“快速枚举”的原理差不多了。
  • 例如遍历一个NSArray:
    NSFastEnumerationState state = {
    
    0};
    NSArray *array = @[@1,@2,@3];
    id buffer[2];
	// buffer 实际上内部没有用上,但还是得传, 2表示我期望得到2个对象,实际上返回的是全部对象数3
    NSUInteger n = [array countByEnumeratingWithState:&state objects:buffer count:2];
    for (NSUInteger i=0; i<n; ++i) {
    
    
        NSLog(@"%@", (__bridge NSNumber *)state.itemsPtr[i]);
    }
  • 看来之所以叫快速遍历,是因为这种方式直接从c数组里取对象,不用调用别的方法,所以快速。

__NSArrayM的实现

 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    
    
    if (!buffer && len > 0) {
    
    
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    if (len >= 0x40000000) {
    
    
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    if (state->state != ~0) {
    
    
        if (state->state == 0) {
    
    
            state->mutationsPtr = &_mutations;
            //找到_list中元素起始的位置
            state->itemsPtr = _list + _offset;
            if (_offset + _used <= _size) {
    
    
                //必定没有剩余元素
                //标示遍历完成
                state->state = ~0;
                return _used;
            }
            else {
    
    
                //有剩余元素(_list是个循环数组,剩余元素在_list从起始位置开始存储)
                //state->state存放剩余元素数目
                state->state = _offset + _used - _size;
                //返回本次得到的元素数目 (总数 - 剩余)
                return _used - state->state;
            }
        }
        else {
    
    
            //得到剩余元素指针
            state->itemsPtr = _list;
            unsigned long left = state->state;
            //标示遍历完成了
            state->state = ~0;
            return left;
        }
    }
    return 0;
}
  • 从实现看出,对于__NSArrayM,用快速枚举的方式最多只要两次就可以获取全部元素,如果_list还没有构成循环,那么第一次就获得了全部元素,跟__NSArrayI一样。但是如果_list构成了循环,那么就需要两次,第一次获取_offset到_list末端的元素,第二次获取存放在_list起始处的剩余元素。
② for in的实现
  • 遍历以下数组:
    NSArray *arr = @[@1,@2,@3];
    for (id obj in arr) {
    
    
        NSLog(@"obj = %@",obj);
    }
  • 通过clang -rewrite-objc main.m命令看看编译器把for in变成了什么:
//NSArray *arr = @[@1,@2,@3];
NSArray *arr = ((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);
    {
    
    
//for (id obj in arr) obj的定义
    id obj;
//NSFastEnumerationState
    struct __objcFastEnumerationState enumState = {
    
     0 };
//buffer
    id __rw_items[16];
    id l_collection = (id) arr;
//第一次遍历,调用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) {
    
    
//保存初次得到的enumState.mutationsPtr的值
    unsigned long startMutations = *enumState.mutationsPtr;
    do {
    
    
        unsigned long counter = 0;
        do {
    
    
//在获取enumState.itemsPtr中每个元素前,都检查一遍enumState.mutationsPtr所指标志是否改变,改变则抛出异常
//对__NSArrayI,enumState.mutationsPtr指向一个静态局部变量,永远也不会抛异常
//对__NSArrayM,enumState.mutationsPtr指向_mutations变量, 每次增删操作后,_mutations会+1
            if (startMutations != *enumState.mutationsPtr)
                objc_enumerationMutation(l_collection);
//获取每一个obj
            obj = (id)enumState.itemsPtr[counter++]; {
    
    
//NSLog(@"obj = %@",obj);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rg_wm9xjmyn1kz01_pph_34xcqc0000gn_T_main_c95c5d_mi_8,obj);
    };
    __continue_label_2: ;
        } 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_2: ;
    }
//没有元素,空数组
    else
        obj = ((id)0);
    }
  • 可见,for in就是基于快速枚举实现的,编译器将for in转化为两层循环,外层调用快速枚举方法批量获取元素,内层通过c数组取得一批元素中的每一个,并且在每次获取元素前,检查是否对数组对象进行了变更操作,如果是,则抛出异常。
③ enumerateObjectsUsingBlock
  • 该方法在NSArray中实现,所有子类对象调用的都是这个实现:
 - (void)enumerateObjectsUsingBlock:(void ( ^)(id obj, NSUInteger idx, BOOL *stop))block {
    
    
    if (!block) {
    
    
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[NSArray enumerateObjectsUsingBlock:]");
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    [self enumerateObjectsWithOptions:0 usingBlock:block];
}
  • 内部直接以option = 0调用了enumerateObjectsWithOptions: usingBlock:
④ enumerateObjectsWithOptions: usingBlock:
  • __NSArrayI的实现
 - (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id _Nonnull, NSUInteger, BOOL * _Nonnull))block {
    
    
    if (!block) {
    
    
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[__NSArrayI enumerateObjectsWithOptions:usingBlock:]");
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    __block BOOL stoped = NO;
    void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
    
    
        if(!stoped) {
    
    
            @autoreleasepool {
    
    
                block(_list[idx],idx,&stoped);
            }
        }
    };
    
    if (opts == NSEnumerationConcurrent) {
    
    
        dispatch_apply(_used, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), enumBlock);
    }
    else if(opts == NSEnumerationReverse) {
    
    
        for (NSUInteger idx = _used - 1; idx != (NSUInteger)-1 && !stoped; idx--) {
    
    
            enumBlock(idx);
        }
    }
    //opts == 0
    else {
    
    
        if(_used > 0) {
    
    
            for (NSUInteger idx = 0; idx != _used - 1 && !stoped; idx++) {
    
    
                enumBlock(idx);
            }
        }
    }
}
  • __NSArrayM的实现:唯一不同的是enumBlock
 void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
    
    
        if(!stoped) {
    
    
            @autoreleasepool {
    
    
                NSUInteger idx_ok = _offset + idx;
                //idx对应元素在_list起始处(循环部分)
                if (idx_ok >= _size) {
    
    
                    idx_ok -= _size;
                }
                block(_list[idx_ok],idx,&stoped);
            }
        }
    };
⑤ objectEnumerator/reverseObjectEnumerator
  • 通过array.objectEnumerator得到的是一个__NSFastEnumerationEnumerator私有类对象,在这个enumerator对象上每次调用- (id)nextObject时,实际上内部每次都调用的是array的快速枚举方法:
 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len 
  • 只不过每次只获取并返回一个元素,而通过array.reverseObjectEnumerator得到的是一个__NSArrayReverseEnumerator私有类对象,在这个enumerator对象上每次调用- (id)nextObject时,内部直接调用是:objectAtIndex:来返回对象。

总结

  • for in之所以快,是因为它基于快速枚举,对NSArray只要一次快速枚举调用就可以获取到包含全部元素的c数组,对NSMUtableArray最多两次就可以全部获取。
  • for 之所以比 for in稍慢,仅仅是因为它函数调用开销的问题,相对于for in直接从c数组取每个元素的方式,for靠的是每次调用objectAtIndex:。
  • 而NSEnumerationConcurrent+Block的方式耗时最大,是因为它采用了多线程,就这个方法来讲,多线程的优势并不在于遍历有多快,而是在于它的回调在各个子线程,如果有遍历+分别耗时计算的场景,这个方法应该是最适合的,只是此处只测遍历速度,它光启动分发管理线程就耗时不少,所以性能就降低了。

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/114759664