美文网首页iOS小记iOSiOS开发
Objective-C 数组遍历的性能及原理

Objective-C 数组遍历的性能及原理

作者: RenJK | 来源:发表于2017-02-10 14:50 被阅读2674次

    数组的遍历,这个话题貌似没什么好探究的,该怎么遍历就怎么遍历呗!但是如果要回答这些问题:
    OC数组有哪几种遍历方式?
    哪种方式效率最高?为什么?
    各种遍历方式的内部实现是怎么样的?
    NS(Mutable)Array的内部结构是怎么样的?
    我觉得还是需要探究一下.

    一.OC数组的类体系

    当我们创建一个NSArray对象时,实际上得到的是NSArray的子类__NSArrayI对象.同样的,我们创建NSMutableArray对象,得到的同样是其子类__NSArrayM对象.有趣的是,当我们创建只有一个对象的NSArray时,得到的是__NSSingleObjectArrayI类对象.
    __NSArrayI__NSArrayM,__NSSingleObjectArrayI为框架隐藏的类.

    OC数组的类体系如下:


    通过NSArray和NSMutableArray接口,返回的却是子类对象,怎么做到的?
    先介绍另一个私有类:__NSPlaceholderArray,和两个此类的全局变量___immutablePlaceholderArray,___mutablePlaceholderArray__NSPlaceholderArray从类命名上看,它只是用来占位的,具体怎么占位法稍后讨论,有个重要特点是,__NSPlaceholderArray实现了和NSArray,NSMutableArray一摸一样的初始化方法,如initWithObjects:count:,initWithCapacity:等.

    介绍完__NSPlaceholderArray后,这个机制可以总结为以下两个大步骤:
    (1).NSArray重写了+ (id)allocWithZone:(struct _NSZone *)zone方法,在方法内部,如果调用类为NSArray则直接返回全局变量___immutablePlaceholderArray,如果调用类为NSMUtableArray则直接返回全局变量___mutablePlaceholderArray
    也就是调用[NSArray alloc]或者[NSMUtableArray alloc]得到的仅仅是两个占位指针,类型为__NSPlaceholderArray.
    (2).在调用了alloc的基础上,不论是NSArrayNSMutableArray都必定要继续调用某个initXXX方法,而实际上调用的是__NSPlaceholderArrayinitXXX.在这个initXXX方法内部,如果self == ___immutablePlaceholderArray就会重新构造并返回__NSArrayI 对象,如果self == ___mutablePlaceholderArray就会重新构造并返回_NSArrayM对象.

    总结来说,对于NSArrayNSMutableArrayalloc时拿到的仅仅是个占位对象,init后才得到真实的子类对象.

    接下来清点一下几种遍历方式:

    二.OC数组遍历的几种方式

    1.for 循环

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

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

    for (id obj in array) {
            xxx
      }
    

    文章稍后会讨论到for in的内部实现
    3.enumerateObjectsUsingBlock
    通过block回调顺序遍历:

    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
           xxx
      }];
    

    4.enumerateObjectsWithOptions:usingBlock:
    通过block回调,在子线程中遍历,对象的回调次序是乱序的,而且调用线程会等待该遍历过程完成:

    [array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            xxx
      }];
    

    通过block回调,在主线程中逆序遍历:

    [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            xxx
      }];
    

    5.objectEnumerator/reverseObjectEnumerator
    通过Enumerator顺序遍历:

    NSEnumerator *enumerator = array.objectEnumerator;
    while((object = enumerator.nextObject)) {
        xxx
    }
    

    通过ReverseEnumerator逆序遍历:

    NSEnumerator *enumerator = array.reverseObjectEnumerator;
    while((object = enumerator.nextObject)) {
        xxx
    }
    

    6.enumerateObjectsAtIndexes:options:usingBlock:
    通过block回调,在子线程中对指定IndexSet遍历,对象的回调次序是乱序的,而且调用线程会等待该遍历过程完成:

    [array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            xxx
     }];
    

    通过block回调,在主线程中对指定IndexSet逆序遍历:

    [array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            xxx
     }];
    

    三.性能比较

    以100为步长,构造对象数目在0-100万之间的NSArray, 分别用上述的遍历方式进行遍历并计时(单位us),而且在每一次遍历中,仅仅只是得到对象,没有其他任何输入输出,计算之类的干扰操作。每种遍历方式采集得1万组数据,得到如下的性能对比结果:


    横轴为遍历的对象数目,纵轴为耗时,单位us.

    从图中看出,在对象数目很小的时候,各种方式的性能差别微乎其微。随着对象数目的增大, 性能差异才体现出来.
    其中for in的耗时一直都是最低的,当对象数高达100万的时候,for in耗时也没有超过5ms.
    其次是for循环耗时较低.
    反而,直觉上应该非常快速的多线程遍历方式:

    [array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            xxx
      }];
    

    却是性能最差的。
    enumerateObjectsUsingBlock :reverseObjectEnumerator的遍历性能非常相近.
    为什么会有这样的结果,文章稍后会从各种遍历的内部实现来分析原因。

    四.OC数组的内部结构

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

    @interface __NSArrayI : NSArray
    {
        NSUInteger _used;
        id _list[0];
    }
    @end
    

    _used是数组的元素个数,调用[array count]时,返回的就是_used的值。
    id _list[0]是数组内部实际存储对象的数组,但为何定义为0长度呢?这里有一篇关于0长度数组的文章:http://blog.csdn.net/zhaqiwen/article/details/7904515
    这里我们可以把id _list[0]当作id *_list来用,即一个存储id对象的buff.
    由于__NSArrayI的不可变,所以_list一旦分配,释放之前都不会再有移动删除操作了,只有获取对象一种操作.因此__NSArrayI的实现并不复杂.
    2. __NSSingleObjectArrayI
    __NSSingleObjectArrayI的结构定义为:

    @interface __NSSingleObjectArrayI : NSArray
    {
        id object;
    }
    @end
    

    因为只有在"创建只包含一个对象的不可变数组"时,才会得到__NSSingleObjectArrayI对象,所以其内部结构更加简单,一个object足矣.
    3. __NSArrayM
    __NSArrayM的结构定义为:

    @interface __NSArrayM : NSMutableArray
    {
        NSUInteger _used;
        NSUInteger _offset;
        int _size:28;
        int _unused:4;
        uint32_t _mutations;
        id *_list;
    }
    @end
    

    __NSArrayM稍微复杂一些,但是同样的,它的内部对象数组也是一块连续内存id* _list,正如__NSArrayIid _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标识.

    五.各种遍历的内部实现

    1.快速枚举
    前面并没有说过快速枚举这个词,怎么这里突然蹦出来了,实际上for in就是基于快速枚举实现的,但是先不讨论for in,先认识一个协议:NSFastEnumeration,它的定义在Foundation框架的NSFastEnumeration .h头文件中:

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

    NSFastEnumerationState定义:

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

    看了这些定义和苹果文档,我也不知道究竟怎么用这个方法,它怎么就叫快速枚举了呢,除非知道它的实现细节,否则用的时候疑惑太多了.因此我们就先不管怎么用,而是来看看它的实现细节.
    __NSArrayI,__NSArrayM,__NSSingleObjectArrayI都实现了NSFastEnumeration协议.
    (1) __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数组里取对象,不用调用别的方法,所以快速.

    __NSSingleObjectArrayI的实现也猜得出了,在此就不贴代码了.我们来看看__NSArrayM是怎么实现这个协议的.
    (2) __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起始处的剩余元素.

    2.for in的实现
    如前面性能比较一节提到的,for in的性能是最好的,可以猜测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数组取得一批元素中的每一个,并且在每次获取元素前,检查是否对数组对象进行了变更操作,如果是,则抛出异常.
    3.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:
    4. enumerateObjectsWithOptions: usingBlock:
    (1)__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);
                }
            }
        }
    }
    

    (1)__NSArrayM的实现
    __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);
                }
            }
        };
    
    

    5.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:来返回对象.
    6.enumerateObjectsAtIndexes:options:usingBlock:
    由于时间关系后面再贴了.

    6.总结

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

    希望通过此文能对你有帮助.

    相关文章

      网友评论

      • liuyanhongwl:看 enumerateObjectsWithOptions: usingBlock: 实现,usingBlock 和 NSEnumerationReverse 也是用的 c 数组 + for,没有方法调用的开销,为啥比 for 遍历方式慢呢
        石显军:在循环体中 处理大量逻辑时 才会体现出enumerateObjectsWithOptions的优势
      • Pericly:能写一篇关于可变数组多线程访问的研究吗,栅栏,信号量,互斥锁
      • 088703a3f63b:再补充一个小点,当NSArray的元素个数是0时 ,它是__NSArray0类型的对象
      • 大号鱼骨头:写得不错,相关性比较好,可以比较着来记忆。
      • XIAODAO:你好,请问__NSArrayI、__NSSingleObjectArrayI、__NSArrayM这几个类的定义在哪里可以找到?源码
        XIAODAO:@RenJK 好的,谢谢
        RenJK:没有源码,需要通过查看汇编来反推实现源码。至于类定义结构,在工具的辅助下可以很清晰的看到类的属性列表,方法列表,实例变量列表,协议列表,继承关系等,或者class-dump工具也可以导出.
      • 未来行者:很底层,谢谢作者

      本文标题:Objective-C 数组遍历的性能及原理

      本文链接:https://www.haomeiwen.com/subject/vdbiittx.html