在 Objective-C 語言的2.0 版本中提供了快速枚舉的語法,它是我們遍歷集合元素的首選方法,因?yàn)樗哂幸韵聝?yōu)點(diǎn):
比直接使用 NSEnumerator 更高效;
語法非常簡潔;
如果集合在遍歷的過程中被修改,它會拋出異常;
可以同時(shí)執(zhí)行多個(gè)枚舉。
那么問題來了,它是如何做到的呢?我想,你應(yīng)該也跟我一樣,對 Objective-C 中快速枚舉的實(shí)現(xiàn)原理非常感興趣,事不宜遲,讓我們來一探究竟吧。
解析 NSFastEnumeration 協(xié)議
在 Objective-C 中,我們要想實(shí)現(xiàn)快速枚舉就必須要實(shí)現(xiàn) NSFastEnumeration 協(xié)議,在這個(gè)協(xié)議中,只聲明了一個(gè)必須實(shí)現(xiàn)的方法:
/**
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.
@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
@param buffer A C array of objects over which the sender is to iterate.
@param len The maximum number of objects to return in stackbuf.
@discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.
@return The number of objects returned in stackbuf. Returns 0 when the iteration is finished.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained [])stackbuf
count:(NSUInteger)len
其中,結(jié)構(gòu)體 NSFastEnumerationState 的定義如下:
typedef struct {
unsigned long state;
id __unsafe_unretained __nullable * __nullable itemsPtr;
unsigned long * __nullable mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;
說實(shí)話,剛開始看到這個(gè)方法的時(shí)候,其實(shí)我是拒絕的,原因你懂的。好吧,先不吐槽了,言歸正傳,下面,我們將對這個(gè)方法進(jìn)行全方位的剖析:
首先,我們需要了解的最重要的一點(diǎn),那就是這個(gè)方法的目的是什么?概括地說,這個(gè)方法就是用于返回一系列的 C 數(shù)組,以供調(diào)用者進(jìn)行遍歷。為什么是一系列的 C 數(shù)組呢?因?yàn)椋谝粋€(gè) for/in 循環(huán)中,這個(gè)方法其實(shí)會被調(diào)用多次,每一次調(diào)用都會返回一個(gè) C 數(shù)組。至于為什么是 C 數(shù)組,那當(dāng)然是為了提高效率了。
既然要返回 C 數(shù)組,也就意味著我們需要返回一個(gè)數(shù)組的指針和數(shù)組的長度。是的,我想你應(yīng)該已經(jīng)猜到了,數(shù)組的長度就是通過這個(gè)方法的返回值來提供的,而數(shù)組的指針則是通過結(jié)構(gòu)體 NSFastEnumerationState 的 itemsPtr 字段進(jìn)行返回的。所以,這兩個(gè)值就一起定義了這個(gè)方法返回的 C 數(shù)組。
通常來說,NSFastEnumeration 允許我們直接返回一個(gè)指向內(nèi)部存儲的指針,但是并非所有的數(shù)據(jù)結(jié)構(gòu)都能夠滿足內(nèi)存連續(xù)的要求。因此,NSFastEnumeration 還為我們提供了另外一種實(shí)現(xiàn)方案,我們可以將元素拷貝到調(diào)用者提供的一個(gè) C 數(shù)組上,即 stackbuf ,它的長度由參數(shù) len 指定。
在本文的開頭,我們提到了如果集合在遍歷的過程中被修改的話,NSFastEnumeration 就會拋出異常。而這個(gè)功能就是通過 mutationsPtr 字段來實(shí)現(xiàn)的,它指向一個(gè)這樣的值,這個(gè)值在集合被修改時(shí)會發(fā)現(xiàn)改變。因此,我們就可以利用它來判斷集合在遍歷的過程中是否被修改。
現(xiàn)在,我們還剩下 NSFastEnumerationState 中的 state 和 extra 字段沒有進(jìn)行介紹。實(shí)際上,它們是調(diào)用者提供給被調(diào)用者自由使用的兩個(gè)字段,調(diào)用者根本不關(guān)心這兩個(gè)字段的值。因此,我們可以利用它們來存儲任何對自身有用的值。
揭密快速枚舉的內(nèi)部實(shí)現(xiàn)
說了這么多,感覺好像 NSFastEnumeration 是你設(shè)計(jì)的一樣,你到底是怎么知道的呢?額,我說我是瞎猜的,你信么?好了,不開玩笑了。接下來,我們就一起來探究一下快速枚舉的內(nèi)部實(shí)現(xiàn)。假設(shè),我們有一個(gè) main.m 文件,其中的代碼如下:
#import
int main(int argc, char * argv[]) {
NSArray *array = @[ @1, @2, @3 ];
for (NSNumber *number in array) {
if ([number isEqualToNumber:@1]) {
continue;
}
NSLog(@"%@", number);
break;
}
}
接著,我們使用下面的 clang 命令將 main.m 文件重寫成 C++ 代碼:
clang -rewrite-objc main.m
得到 main.cpp 文件,其中 main 函數(shù)的代碼如下:
int main(int argc, char * argv[]) {
// 創(chuàng)建數(shù)組 @[ @1, @2, @3 ]
NSArray *array = ((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);
{
NSNumber * number;
// 初始化結(jié)構(gòu)體 NSFastEnumerationState
struct __objcFastEnumerationState enumState = { 0 };
// 初始化數(shù)組 stackbuf
id __rw_items[16];
id l_collection = (id) array;
// 第一次調(diào)用 - countByEnumeratingWithState:objects:count: 方法,形參和實(shí)參的對應(yīng)關(guān)系如下:
// state -> &enumState
// stackbuf -> __rw_items
// len -> 16
_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) {
// 獲取 mutationsPtr 的初始值
unsigned long startMutations = *enumState.mutationsPtr;
// 外層的 do/while 循環(huán),用于調(diào)用 - countByEnumeratingWithState:objects:count: 方法,獲取 C 數(shù)組
do {
unsigned long counter = 0;
// 內(nèi)層的 do/while 循環(huán),用于遍歷獲取到的 C 數(shù)組
do {
// 判斷 mutationsPtr 的值是否有發(fā)生變化,如果有則使用 objc_enumerationMutation 函數(shù)拋出異常
if (startMutations != *enumState.mutationsPtr) objc_enumerationMutation(l_collection);
// 使用指針的算術(shù)運(yùn)算獲取相應(yīng)的集合元素,這是快速枚舉之所以高效的關(guān)鍵所在
number = (NSNumber *)enumState.itemsPtr[counter++];
{
if (((BOOL (*)(id, SEL, NSNumber *))(void *)objc_msgSend)((id)number, sel_registerName("isEqualToNumber:"), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1))) {
// continue 語句的實(shí)現(xiàn),使用 goto 語句無條件轉(zhuǎn)移到內(nèi)層 do 語句的末尾,跳過中間的所有代碼
goto __continue_label_1;
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_cr_xxw2w3rd5_n493ggz9_l4bcw0000gn_T_main_fc7b79_mi_0, number);
// break 語句的實(shí)現(xiàn),使用 goto 語句無條件轉(zhuǎn)移到最外層 if 語句的末尾,跳出嵌套的兩層循環(huán)
goto __break_label_1;
};
// goto 語句標(biāo)號,用來實(shí)現(xiàn) continue 語句
__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)));
number = ((NSNumber *)0);
// goto 語句標(biāo)號,用來實(shí)現(xiàn) break 語句
__break_label_1: ;
} else {
number = ((NSNumber *)0);
}
}
}
如上代碼所示,快速枚舉其實(shí)就是用兩層 do/while 循環(huán)來實(shí)現(xiàn)的,外層循環(huán)負(fù)責(zé)調(diào)用 - countByEnumeratingWithState:objects:count: 方法,獲取 C 數(shù)組,而內(nèi)層循環(huán)則負(fù)責(zé)遍歷獲取到的 C 數(shù)組。同時(shí),我想你應(yīng)該也注意到了它是如何利用 mutationsPtr 來檢測集合在遍歷過程中的突變的,以及使用 objc_enumerationMutation 函數(shù)來拋出異常。
正如我們前面提到的,在快速枚舉的實(shí)現(xiàn)中,確實(shí)沒有用到結(jié)構(gòu)體 NSFastEnumerationState 中的 state 和 extra 字段,它們只是提供給 - countByEnumeratingWithState:objects:count: 方法的實(shí)現(xiàn)者自由使用的字段。
值得一提的是,我特意在 main.m 中加入了 continue 和 break 語句。因此,我們有機(jī)會看到了在 for/in 語句中是如何利用 goto 來實(shí)現(xiàn) continue 和 break 語句的。
實(shí)現(xiàn) NSFastEnumeration 協(xié)議
看到這里,我相信你對 Objective-C 中快速枚舉的實(shí)現(xiàn)原理已經(jīng)有了一個(gè)比較清晰地認(rèn)識。下面,我們就一起來動手實(shí)現(xiàn)一下 NSFastEnumeration 協(xié)議。
我們前面已經(jīng)提到了,NSFastEnumeration 在設(shè)計(jì)上允許我們使用兩種不同的方式來實(shí)現(xiàn)它。如果集合中的元素在內(nèi)存上是連續(xù)的,那么我們可以直接返回這段內(nèi)存的首地址;如果不連續(xù),比如鏈表,就只能使用調(diào)用者提供的 C 數(shù)組 stackbuf 了,將我們的元素拷貝到這個(gè) C 數(shù)組上。
接下來,我們將通過一個(gè)自定義的集合類 Array ,來演示這兩種不同的實(shí)現(xiàn) NSFastEnumeration 協(xié)議的方式。注:完整的項(xiàng)目代碼可以在這里找到。
@interface Array : NSObject
- (instancetype)initWithCapacity:(NSUInteger)numItems;
@end
@implementation Array {
std::vector
_list;
}
- (instancetype)initWithCapacity:(NSUInteger)numItems {
self = [super init];
if (self) {
for (NSUInteger i = 0; i < numItems; i++) {
_list.push_back(@(random()));
}
}
return self;
}
#define USE_STACKBUF 1
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])stackbuf count:(NSUInteger)len {
// 這個(gè)方法的返回值,即我們需要返回的 C 數(shù)組的長度
NSUInteger count = 0;
// 我們前面已經(jīng)提到了,這個(gè)方法是會被多次調(diào)用的
// 因此,我們需要使用 state->state 來保存當(dāng)前遍歷到了 _list 的什么位置
unsigned long countOfItemsAlreadyEnumerated = state->state;
// 當(dāng) countOfItemsAlreadyEnumerated 為 0 時(shí),表示第一次調(diào)用這個(gè)方法
// 我們可以在這里做一些初始化的設(shè)置
if (countOfItemsAlreadyEnumerated == 0) {
// 我們前面已經(jīng)提到了,state->mutationsPtr 是用來追蹤集合在遍歷過程中的突變的
// 它不能為 NULL ,并且也不應(yīng)該指向 self
//
// 這里,因?yàn)槲覀兊?nbsp;Array 類是不可變的,所以我們不需要追蹤它的突變
// 因此,我們的做法是將它指向 state->extra 的其中一個(gè)值
// 因?yàn)槲覀冎?nbsp;NSFastEnumeration 協(xié)議本身并沒有用到 state->extra
//
// 但是,如果你的集合是可變的,那么你可以考慮將 state->mutationsPtr 指向一個(gè)內(nèi)部變量
// 而這個(gè)內(nèi)部變量的值會在你的集合突變時(shí)發(fā)生變化
state->mutationsPtr = &state->extra[0];
}
#if USE_STACKBUF
// 判斷我們是否已經(jīng)遍歷完 _list
if (countOfItemsAlreadyEnumerated < _list.size()) {
// 我們知道 state->itemsPtr 就是這個(gè)方法返回的 C 數(shù)組指針,它不能為 NULL
// 在這里,我們將 state->itemsPtr 指向調(diào)用者提供的 C 數(shù)組 stackbuf
state->itemsPtr = stackbuf;
// 將 _list 中的元素填充到 stackbuf 中,直到以下兩個(gè)條件中的任意一個(gè)滿足時(shí)為止
// 1. 已經(jīng)遍歷完 _list 中的所有元素
// 2. 已經(jīng)填充滿 stackbuf
while (countOfItemsAlreadyEnumerated < _list.size() && count < len) {
// 取出 _list 中的元素填充到 stackbuf 中
stackbuf[count] = _list[countOfItemsAlreadyEnumerated];
// 更新我們的遍歷位置
countOfItemsAlreadyEnumerated++;
// 更新我們返回的 C 數(shù)組的長度,使之與 state->itemsPtr 中的元素個(gè)數(shù)相匹配
count++;
}
}
#else
// 判斷我們是否已經(jīng)遍歷完 _list
if (countOfItemsAlreadyEnumerated < _list.size()) {
// 直接將 state->itemsPtr 指向內(nèi)部的 C 數(shù)組指針,因?yàn)樗膬?nèi)存地址是連續(xù)的
__unsafe_unretained const id * const_array = _list.data();
state->itemsPtr = (__typeof__(state->itemsPtr))const_array;
// 因?yàn)槲覀円淮涡苑祷亓?nbsp;_list 中的所有元素
// 所以,countOfItemsAlreadyEnumerated 和 count 的值均為 _list 中的元素個(gè)數(shù)
countOfItemsAlreadyEnumerated = _list.size();
count = _list.size();
}
#endif
// 將本次調(diào)用得到的 countOfItemsAlreadyEnumerated 保存到 state->state 中
// 因?yàn)?nbsp;NSFastEnumeration 協(xié)議本身并沒有用到 state->state
// 所以,我們可以將這個(gè)值保留到下一次調(diào)用
state->state = countOfItemsAlreadyEnumerated;
// 返回 C 數(shù)組的長度
return count;
}
@end
我已經(jīng)在上面的代碼中添加了必要的注釋,相信你理解起來應(yīng)該沒有什么難度。不過,值得一提的是,在第二種方式的實(shí)現(xiàn)中,我們用到了 ARC 下不同所有權(quán)對象之間的相互轉(zhuǎn)換,代碼如下:
__unsafe_unretained const id * const_array = _list.data();
state->itemsPtr = (__typeof__(state->itemsPtr))const_array;
其實(shí),這里面涉及到兩次類型轉(zhuǎn)換,第一次是從 __strong NSNumber * 類型轉(zhuǎn)換到 __unsafe_unretained const id * 類型,第二次是從 __unsafe_unretained const id * 類型轉(zhuǎn)換到 id __unsafe_unretained * 類型,更多信息可以查看 AutomaticReferenceCounting 中的 4.3.3 小節(jié)。
另外,我在前面的文章《ReactiveCocoa v2.5 源碼解析之架構(gòu)總覽》中,已經(jīng)有提到過,ReactiveCocoa 中的 RACSequence 類其實(shí)是實(shí)現(xiàn)了 NSFastEnumeration 協(xié)議的。因?yàn)?nbsp;RACSequence 中的元素在內(nèi)存上并不連續(xù),所以它采用的是第一種實(shí)現(xiàn)方式。對此感興趣的同學(xué),可以去看看它的實(shí)現(xiàn)源碼,這里不再贅述。
總結(jié)
本文從NSFastEnumeration 協(xié)議的定義出發(fā),解析了 - countByEnumeratingWithState:objects:count: 方法中的返回值以及各個(gè)參數(shù)的含義;接著,我們使用 clang -rewrite-objc 命令探究了快速枚舉的內(nèi)部實(shí)現(xiàn);最后,通過一個(gè)自定義的集合類 Array 演示了兩種實(shí)現(xiàn) NSFastEnumeration 協(xié)議的方式,希望本文能夠?qū)δ阌兴鶐椭?/span>
文章來源:CocoaChina