基礎(chǔ)知識
快速枚舉有兩個(gè)優(yōu)點(diǎn)凶杖。一是胁艰,實(shí)現(xiàn)快速枚舉后,你可以直接使用for/in
語法遍歷你的對象官卡。二是蝗茁,如果將快速枚舉實(shí)現(xiàn)得很好,會大大提高遍歷的速度寻咒。
實(shí)現(xiàn)快速枚舉哮翘,很簡單。只需要實(shí)現(xiàn)NSFastEnumeration協(xié)議就可以了毛秘,而且這個(gè)協(xié)議只有一個(gè)方法:
NSMutableArray *array;
NSLock *arrayLock;
[arrayLock lock];
[array addObject:obj];
[arrayLock unlock];
怎么樣饭寺?看上去很簡單,對吧叫挟?等一下艰匙,NSFastEnumerationState是什么啊抹恳?
typedef struct {
unsigned long state;
id *itemsPtr;
unsigned long *mutationsPtr;
unsigned long extra[5];
} NSFastEnumerationState;
呃员凝,這似乎變得有點(diǎn)復(fù)雜了……
字段和參數(shù)的解釋
實(shí)現(xiàn)這個(gè)方法前,讓我們先來搞清楚這些參數(shù)奋献、字段以及返回值的用途健霹。
這個(gè)方法的目的是返回對象數(shù)組中的一部分對象旺上。每次調(diào)用返回一組對象,這樣使對象可以成批的被返回給調(diào)用者糖埋。為了獲得最好的運(yùn)行效率宣吱,這里使用了一個(gè)C數(shù)組,這樣就需要兩個(gè)參數(shù)——數(shù)組首地址指針和數(shù)組長度瞳别。
數(shù)組長度被作為返回值返回征候。這也是方法名字中”count”
所代表的意思。數(shù)組的首地址指針由NSFastEnumerationState結(jié)構(gòu)體中的itemsPtr字段指定祟敛。這兩個(gè)值共同確定了這個(gè)方法返回給調(diào)用者的數(shù)組疤坝。
NSFastEnumeration被設(shè)計(jì)成允許返回一個(gè)指向內(nèi)存的指針。然而垒棋,這并不適于所有數(shù)據(jù)結(jié)構(gòu)卒煞,因此它也被設(shè)計(jì)成允許將內(nèi)部對象拷貝到調(diào)用者提供的一個(gè)數(shù)組容器中。在這里叼架,stackbuf即是指調(diào)用者提供的數(shù)組容器畔裕,len是數(shù)組容器的大小。
NSFastEnumeration還被設(shè)計(jì)成在遍歷集合的時(shí)候乖订,可以檢測到集合的變動扮饶。如果檢測到集合被修改了,就會拋出一個(gè)異常乍构。mutationsPtr被用于指向會隨著集合變動而變化的一個(gè)值甜无。
好了,大部分字段和參數(shù)的意思都講解完了哥遮。唯一沒講的就是state和extra這兩個(gè)字段岂丘。這兩個(gè)字段是預(yù)留給被調(diào)用者的,你可以自由的使用它們來存儲任何你覺得有用的數(shù)據(jù)眠饮。
循環(huán)代碼的結(jié)構(gòu)
現(xiàn)在奥帘,我們知道了所有這些東西是用來干什么的,但是為了真正理解這些東西是如何工作的仪召,最好是要了解一下編譯器生成了哪些代碼寨蹋。比如,你寫了這樣的代碼:
for(id obj in collection)
{
// body
}
那么扔茅,在這背后到底發(fā)生了些什么呢已旧?
編譯器在堆棧上創(chuàng)建了一個(gè)NSFastEnumerationState變量,以及一個(gè)堆棧緩沖區(qū)召娜。然后創(chuàng)建兩個(gè)嵌套的循環(huán)运褪,一個(gè)用于不停的調(diào)用countByEnumeratingWithState:…方法,另一個(gè)則用于遍歷這個(gè)方法返回的數(shù)組。所以最后會生成類似下面這種代碼:
//定義所有需要的局部狀態(tài)變量
NSFastEnumerationState state = { 0 };
id stackbuf[16];
BOOL firstLoop = YES;
long mutationsPtrValue;
//外部循環(huán)
NSUInteger count;
while((count = [collection countByEnumeratingWithState: &state objects: stackbuf count: 16]))
{
// 檢查變量對象是否被修改過吐句,但是只會在第一次循環(huán)后才檢查
// (請注意胁后,我不太確定真實(shí)情況下編譯器會把這個(gè)檢查放在內(nèi)部循環(huán)中還是放在外部循環(huán)中,
// 并且不同版本的編譯器實(shí)現(xiàn)的方式也可能不一樣)
if(!firstLoop && mutationsPtrValue != *state.mutationsPtr)
@throw ..mutation exception...
firstLoop = NO;
mutationsPtrValue = *state.mutationsPtr;
//內(nèi)部循環(huán)嗦枢,遍歷NSFastEnumeration調(diào)用返回的數(shù)組
id obj;
for(NSUInteger index = 0; index < count; index++)
{
obj = state.itemsPtr[index];
// body
}
}
注意到了嗎,這些代碼并沒有用到state和extra字段屯断。正如我前面提到的文虏,這些純粹都是提供給被調(diào)用者用于組織返回?cái)?shù)據(jù)的。
一次返回一個(gè)對象
NSFastEnumeration最主要的一個(gè)目的是想通過成批的遍歷來提高遍歷速度殖演。一次只返回一個(gè)對象實(shí)際上有點(diǎn)背于這個(gè)理念氧秘。然而,這樣也更容易實(shí)現(xiàn)趴久,也可以讓你能夠使用for/in語法來變量你的對象丸相。要避免”預(yù)優(yōu)化(premature optimization)”的思想,如果一次返回一個(gè)對象更容易彼棍,那么就用這個(gè)方法灭忠。
例如,假如你有如下一個(gè)鏈表類:
@implementation LinkedList : NSObject
{
struct Node *listHead;
}
現(xiàn)在我們來為這個(gè)類實(shí)現(xiàn)NSFastEnumeration協(xié)議座硕,用簡單的方式弛作,以一次返回一個(gè)對象的方式來實(shí)現(xiàn)它。
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
// 行動計(jì)劃: 用extra[0]指向要遍歷的下一個(gè)對象华匾。
// 由于extra[0]是一個(gè)long類型映琳,所以這里會涉及到一些類型轉(zhuǎn)換
if(state->state == 0)
{
// state為0代表這是第一次調(diào)用,我們在第一調(diào)用時(shí)設(shè)置好各種初始狀態(tài)
// 由于這里我們不會試著檢測集合的變化蜘拉,所以直接將mutationsPtr指向一個(gè)確定不會變的地方
state->mutationsPtr = (unsigned long *)self;
// 設(shè)置extra[0]初始指向鏈表的頭部
state->extra[0] = (long)listHead;
// 最后更新state萨西,表明遍歷已經(jīng)開始了
state->state = 1;
}
// 從extra[0]取出節(jié)點(diǎn)
struct Node *currentNode = (struct Node *)state->extra[0];
// 如果是NULL,則遍歷完成旭旭,返回0以表示遍歷結(jié)束
if(!currentNode)
return NULL
// 否則, 將itemsPtr指向節(jié)點(diǎn)的值
state->itemsPtr = ¤tNode->value
// 更新extra[0]谎脯,指向下一個(gè)節(jié)點(diǎn)
if(currentNode)
state->extra[0] = (long)currentNode->next;
// 一次只返回一個(gè)項(xiàng)目
return 1;
}
不錯,除了一些指針和整型之間的類型轉(zhuǎn)換穿肄。
成批的返回
讓我們假設(shè)咸产,最后事實(shí)證明上面那個(gè)方法真的太慢了,你想要讓它更快一些屑彻。你可以試試成批的返回對象。由于鏈表中的對象不是連續(xù)存儲的社牲,要這樣做粪薛,你就必須將要返回的對象拷貝到stackbuf中去。雖然stackbuf的大小不確定违寿,但是我們可以認(rèn)為它足夠大到出來這樣的問題熟空。
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
// 行動機(jī)會: 幾乎和前面一樣藤巢,我們還是用extra[0]指向下一個(gè)要訪問的節(jié)點(diǎn),只是會一次遍歷多個(gè)節(jié)點(diǎn)息罗。
if(state->state == 0)
{
state->mutationsPtr = (unsigned long *)self;
state->extra[0] = (long)listHead;
state->state = 1;
}
// 從extra[0]中取出節(jié)點(diǎn)
struct Node *currentNode = (struct Node *)state->extra[0];
// objCount用于記錄我們遍歷了多少個(gè)對象掂咒,以便稍后返回這個(gè)數(shù)目。
NSUInteger objCount = 0;
// 我將會把返回的對象放進(jìn)stackbuf中迈喉,因此itemsPtr指向它
state->itemsPtr = stackbuf;
// 循環(huán)遍歷節(jié)點(diǎn)绍刮,直到stackbuf被我們裝滿了或者沒有任何節(jié)點(diǎn)可再遍歷時(shí)結(jié)束遍歷
while(currentNode && objCount < len)
{
// 填充當(dāng)前stackbuf位置,并將它指向下一個(gè)位置
*stackbuf++ = currentNode->value
// 移動到下一個(gè)節(jié)點(diǎn)
currentNode = currentNode->next;
// 記錄節(jié)點(diǎn)數(shù)目
objCount++;
}
// 更新extra[0]弊添,使其指向下一個(gè)節(jié)點(diǎn)
if(currentNode)
state->extra[0] = (long)currentNode->next;
return objCount;
}
這并沒有多難录淡,并且它會顯著地降低for/in
循環(huán)中間的函數(shù)調(diào)用次數(shù)。
返回整塊內(nèi)存的指針
為了獲得最好的性能油坝,你可以返回一段對象連續(xù)存儲的內(nèi)存指針嫉戚。翻譯的有點(diǎn)拗口哈。舉個(gè)例子澈圈,假如你有一個(gè)如下的簡單的數(shù)組類:
@interface Array : NSObject
{
id *pointer;
NSUInteger count;
}
這個(gè)類實(shí)現(xiàn)NSFastEnumeration協(xié)議相當(dāng)?shù)睾唵伪蛱础V恍枰祷刂赶蛩袑ο蟮膬?nèi)部指針就可以了。
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
{
if(state->state == 0)
{
state->mutationsPtr = (unsigned long *)self;
state->itemsPtr = pointer;
state->state = 1;
return count;
}
else
return 0;
}
很簡單吧瞬女!而且窍帝,它的速度也很快,因?yàn)樽詈蟊闅v循環(huán)基本上就被轉(zhuǎn)換成C中的for循環(huán)形式了诽偷。
NSFastEnumeration也可以被用在一些更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)上坤学。如果你有一系列連續(xù)的指向?qū)ο髩K的指針(a series of contiguous object pointers),你可以依次的返回這些對象塊的指針报慕,這樣可以加快對所有對象的遍歷速度深浮。 這種情況下,你可以充分的利用extra這個(gè)值來跟蹤當(dāng)前你在內(nèi)部數(shù)據(jù)結(jié)構(gòu)中的位置眠冈。
關(guān)于臨時(shí)對象的一個(gè)注意事項(xiàng)
也許你已經(jīng)發(fā)現(xiàn)了飞苇,用extra存儲Objective-C對象很方便:
state->extra[1] = (long)[NSArray arrayWith...];
但是,要注意了!當(dāng)使用下面這種完全合法的遍歷代碼時(shí)布卡,就有問題了:
NSAutoreleasePool *pool = [NSAutoreleasePool new];
for(id obj in collection)
{
// do stuff with obj
[pool release];
pool = [NSAutoreleasePool new];
}
[pool release];
當(dāng)自動釋放池被釋放的時(shí)候雨让,你返回的數(shù)組也會跟著被釋放掉。然后當(dāng)你下次再想訪問它的時(shí)候忿等,就會掛掉栖忠。并且你還不能retain你那個(gè)數(shù)組,因?yàn)椴荒鼙WC調(diào)用者一定會遍歷完然后讓你有機(jī)會釋放掉它贸街。調(diào)用者可能在遍歷過程中很早就跳出了娃闲,這種情況下你的對象就泄漏了。
真的沒有常規(guī)的方法來解決這個(gè)問題匾浪。(我試過實(shí)現(xiàn)了一個(gè)完全瘋狂的解決方案:追蹤堆棧指針的位置以便知道什么時(shí)候可以安全的銷毀這些臨時(shí)對象。但是卷哩,怎么說呢蛋辈?太瘋狂了。)所以如果可能的話将谊,嘗試避免像這樣使用extra來存儲臨時(shí)對象冷溶。如果不得不這樣做,那么要當(dāng)心在for/in循環(huán)中別對這些對象使用自動釋放尊浓。通常情況下逞频,可能你是你實(shí)現(xiàn)的NSFastEnumeration方法的唯一使用者,所以這看起來是個(gè)不值一提的自我約定栋齿,但是這確實(shí)是你必須要注意的一個(gè)問題苗胀。
結(jié)論
實(shí)現(xiàn)NSFastEnumeration協(xié)議讓你可以使用簡單優(yōu)美的語法來遍歷你自定義的集合類對象。而且瓦堵,通常也能夠獲得更快的遍歷速度基协。雖然初見之時(shí)NSFastEnumeration很令人畏懼,但實(shí)際上是相當(dāng)容易實(shí)現(xiàn)它的菇用,當(dāng)然也取決于你的內(nèi)部數(shù)據(jù)結(jié)構(gòu)的復(fù)雜程度澜驮。