實(shí)現(xiàn)快速枚舉 NSFastEnumeration

基礎(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ù)的意思都講解完了哥遮。唯一沒講的就是stateextra這兩個(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
        
    }
    
}

注意到了嗎,這些代碼并沒有用到stateextra字段屯断。正如我前面提到的文虏,這些純粹都是提供給被調(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 = &currentNode->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ù)雜程度澜驮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惋鸥,隨后出現(xiàn)的幾起案子杂穷,更是在濱河造成了極大的恐慌,老刑警劉巖卦绣,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耐量,死亡現(xiàn)場離奇詭異,居然都是意外死亡迎卤,警方通過查閱死者的電腦和手機(jī)拴鸵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人劲藐,你說我怎么就攤上這事八堡。” “怎么了聘芜?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵兄渺,是天一觀的道長。 經(jīng)常有香客問我汰现,道長挂谍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任瞎饲,我火速辦了婚禮口叙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗅战。我一直安慰自己妄田,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布驮捍。 她就那樣靜靜地躺著疟呐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪东且。 梳的紋絲不亂的頭發(fā)上启具,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機(jī)與錄音珊泳,去河邊找鬼鲁冯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛旨椒,可吹牛的內(nèi)容都是我干的晓褪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼综慎,長吁一口氣:“原來是場噩夢啊……” “哼涣仿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起示惊,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤好港,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后米罚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钧汹,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年录择,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拔莱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碗降。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖塘秦,靈堂內(nèi)的尸體忽然破棺而出讼渊,到底是詐尸還是另有隱情,我是刑警寧澤尊剔,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布爪幻,位于F島的核電站,受9級特大地震影響须误,放射性物質(zhì)發(fā)生泄漏挨稿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一京痢、第九天 我趴在偏房一處隱蔽的房頂上張望奶甘。 院中可真熱鬧,春花似錦祭椰、人聲如沸甩十。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鸭轮,卻和暖如春臣淤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窃爷。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工邑蒋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人按厘。 一個(gè)月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓医吊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逮京。 傳聞我的和親對象是個(gè)殘疾皇子卿堂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容