Objective-C 數組遍歷的性能及原理

fp_hvey 7年前發布 | 35K 次閱讀 Objective-C開發 Objective-C

數組的遍歷,這個話題貌似沒什么好探究的,該怎么遍歷就怎么遍歷唄!但是如果要回答這些問題:

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 的基礎上,不論是 NSArray 或 NSMutableArray 都必定要繼續調用某個 initXXX 方法,而實際上調用的是 __NSPlaceholderArray 的 initXXX .在這個 initXXX 方法內部,如果 self == ___immutablePlaceholderArray 就會重新構造并返回 __NSArrayI 對象,如果 self == ___mutablePlaceholderArray 就會重新構造并返回 _NSArrayM 對象.

總結來說,對于 NSArray 和 NSMutableArray , alloc 時拿到的僅僅是個占位對象, 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數組的內部結構

NSArray 和 NSMutableArray 都沒有定義實例變量,只是定義和實現了接口,且對內部數據操作的接口都是在各個子類中實現的.所以真正需要了解的是子類結構,了解了 __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 ,正如 __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 標識.

五.各種遍歷的內部實現

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 的方式耗時最大,我認為是因為它采用了多線程,就這個方法來講,多線程的優勢并不在于遍歷有多快,而是在于它的回調在各個子線程,如果有 遍歷+分別耗時計算 的場景,這個方法應該是最適合的,只是此處只測遍歷速度,它光啟動分發管理線程就耗時不少,所以性能落后了.

希望通過此文能對你有幫助.

 

來自:http://www.jianshu.com/p/66f8410c6bbc

 

 本文由用戶 fp_hvey 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!