NSFastEnumeration使用原理分析

前言:集合類在每門編程語(yǔ)言中都有著非常重要的地位末盔,每一門語(yǔ)言對(duì)于集合類的實(shí)現(xiàn)和提供的API也大同小異拾因。相比于Java浇辜、C#等編程語(yǔ)言O(shè)bjective-C相對(duì)薄弱一些刘急,不過筆者用久了了就慢慢習(xí)慣了函荣,思考也少了显押。之前看RACSequence源碼時(shí)驚嘆于其中的設(shè)計(jì)之余也遇到好多模糊的知識(shí)點(diǎn)扳肛,NSFastEnumeration就是其中之一,在此做下學(xué)習(xí)筆記乘碑,至于RAC的源碼閱讀和分析日后再分章節(jié)奉上挖息。


首先看一下forin遍歷的日常使用代碼:

 NSUInteger idx = 0;
 NSArray *array = @[@"A",@"B",@"C"];
 for(id item in array)
 {
     NSLog(@"Item %li = %@", idx++, item);
 }
//運(yùn)行結(jié)果如下
Item 0 = A
Item 1 = B
Item 2 = C

代碼再簡(jiǎn)單不過了,相比于普通的foreach循環(huán)forin可以直接得到列表中存儲(chǔ)的對(duì)象兽肤,語(yǔ)法類似滿足日常的使用習(xí)慣套腹。foreach、forin以及-(void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;交替使用幾乎完全滿足了我們?nèi)粘>幋a需求资铡〉缳鳎可是當(dāng)我們?cè)偻罾锼伎家幌拢@些語(yǔ)法糖背后的邏輯是什么笤休?是不是都是基于相同的底層代碼實(shí)現(xiàn)的呢尖飞?當(dāng)自定義的對(duì)象也需要支持這樣的語(yǔ)法糖該怎么辦?接下來就一起看看forin背后的原理店雅。


打開NSArray的頭文件政基,首先看到的是下面一行的代碼:

@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

很顯然NSArray類實(shí)現(xiàn)NSFastEnumeration協(xié)議,正是NSFastEnumeration使得NSArray支持forin這樣的語(yǔ)法糖底洗。打開NSFastEnumeration頭文件(如下)可以看到其中只包含一個(gè)方法和一個(gè)結(jié)構(gòu)體NSFastEnumerationState

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

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;

@end

代碼看起來有點(diǎn)懵腋么,因?yàn)轭^文件中沒有任何有關(guān)方法使用的注釋和結(jié)構(gòu)體的介紹。沒辦法只能去搜API文檔(點(diǎn)此前往)亥揖,下面我貼出來API介紹的片段:

countByEnumeratingWithState:objects:count:

NSFastEnumerationState

依然看不出太多的信息珊擂,在繼續(xù)往下看之前我們不妨根據(jù)已有信息做如下的思考:1.forin只是語(yǔ)法糖,背后遍歷使用的肯定是原始的循環(huán)結(jié)構(gòu)do-while/while? 2.數(shù)組的下標(biāo)索引+原始的do-while/while循環(huán)結(jié)構(gòu)如何實(shí)現(xiàn)數(shù)組的遍歷费变?3.再考慮空間利用率該如何優(yōu)化摧扇?

繼續(xù)搜索文檔找到了NSFastEnumeration是Sample文檔:

Sample Code

果斷下載下來并用Xocde打開,里面有如下三個(gè)文件:
Project Files

EnumerationSamplemain調(diào)試方法可以忽略。重點(diǎn)分析EnumerableClass.hEnumerableClass.mm兩個(gè)文件挚歧。打開頭文件果然里面有非常詳細(xì)的注釋(這里就不貼出來)扛稽,接口定義@interface EnumerableClass : NSObject <NSFastEnumeration>實(shí)現(xiàn)了NSFastEnumeration協(xié)議。此外還實(shí)現(xiàn)了對(duì)象支持下標(biāo)索引和block遍歷的API滑负,代碼很簡(jiǎn)單可以直接查看文檔在张,這里只關(guān)注NSFastEnumeration的協(xié)議方法,直奔主題找到對(duì)應(yīng)的實(shí)現(xiàn)方法如下(注釋太多矮慕,刪除了原先英文注釋):

#define USE_STACKBUF 1

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
                                  objects:(id __unsafe_unretained [])stackbuf
                                    count:(NSUInteger)stackbufLength
{
 NSUInteger count = 0;
 unsigned long countOfItemsAlreadyEnumerated = state->state;
    

 if(countOfItemsAlreadyEnumerated == 0)
 {
  state->mutationsPtr = &state->extra[0];
 }
    
#if USE_STACKBUF // Method One.
    
 if(countOfItemsAlreadyEnumerated < _list.size())
 {
  state->itemsPtr = stackbuf;
  while((countOfItemsAlreadyEnumerated < _list.size()) && (count < stackbufLength))
  {
   stackbuf[count] = _list[countOfItemsAlreadyEnumerated];
   countOfItemsAlreadyEnumerated++;
          
   count++;
  }
 }
 else
 {
  count = 0;
 }
    
#else // Method Two.
    if (countOfItemsAlreadyEnumerated < _list.size())
    {
        __unsafe_unretained const id * const_array = _list.data();
        state->itemsPtr = (__typeof__(state->itemsPtr))const_array;
        
        count = _list.size();
       
        countOfItemsAlreadyEnumerated = _list.size();
    }
    else
    {
        count = 0;
    }
#endif
     state->state = countOfItemsAlreadyEnumerated;
     return count;
}

分析一下上面的代碼:
1.countByEnumeratingWithState方法的實(shí)現(xiàn)有兩種方式帮匾,例子代碼中使用宏USE_STACKBUF進(jìn)行了區(qū)分,兩種方式的主要區(qū)別是:第一種方式是利用了參數(shù)中的緩沖數(shù)組痴鳄,第二種方式?jīng)]有使用緩沖數(shù)組而是直接持有了原數(shù)組的一個(gè)弱引用瘟斜。筆者感覺第二種是不安全的,因此大多情況的實(shí)現(xiàn)應(yīng)該是使用第一中方式。
2.仔細(xì)閱讀第一種方式的實(shí)現(xiàn)代碼螺句,可以看出:a.countByEnumeratingWithState肯定是被外部循環(huán)調(diào)用的虽惭,使用結(jié)構(gòu)體指針NSFastEnumerationState記錄當(dāng)前最新的遍歷狀態(tài),循環(huán)的次數(shù)取決于參數(shù)stackbufLength緩沖區(qū)的大小蛇尚。b.NSFastEnumerationStatestate用來記錄最后遍歷的位置芽唇,itemsPtr指向緩沖區(qū)數(shù)組,mutationsPtrextra是用來檢測(cè)和控制原始數(shù)組改變的,具體使用例子中并沒有給出更多信息佣蓉,待以后探索披摄。c.當(dāng)最終返回'count = 0'是表示遍歷結(jié)束。

小結(jié):forin是通過外部傳入一個(gè)緩沖數(shù)組勇凭,在countByEnumeratingWithState方法體內(nèi)通過外部的不斷循環(huán)調(diào)用根據(jù)state狀態(tài)重復(fù)填充緩沖區(qū)數(shù)組并返回結(jié)果疚膊。那么外部是究竟是如何設(shè)置緩沖區(qū)大小的?如何遍歷countByEnumeratingWithState方法虾标?

帶著上面的疑問來查看一下編譯器翻譯后的forin代碼如下:

int main (int argc, const char * argv[])
{
    /* @autoreleasepool */
    { __AtAutoreleasePool __autoreleasepool; 


        EnumerableClass *example = ((id (*)(id, SEL, NSUInteger))(void *)objc_msgSend)((id)((EnumerableClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("EnumerableClass"), sel_registerName("alloc")), sel_registerName("initWithCapacity:"), (NSUInteger)50);
        NSUInteger idx = 0;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_lv_h4gknfp17nq8wm59sqwgtgjm0000gn_T_EnumerationSample_88076b_mi_0);
        idx = 0;
        {
            id item;
            struct __objcFastEnumerationState enumState = { 0 };
            id __rw_items[16];
            id l_collection = (id) example;
            _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) {
                        unsigned long startMutations = *enumState.mutationsPtr;
                        do {
                            unsigned long counter = 0;
                            do {
                                if (startMutations != *enumState.mutationsPtr)
                                    objc_enumerationMutation(l_collection);
                                item = (id)enumState.itemsPtr[counter++];
                                {
                                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_lv_h4gknfp17nq8wm59sqwgtgjm0000gn_T_EnumerationSample_88076b_mi_1, idx++, item);
                                };
                                __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)));
                        item = ((id)0);
                        __break_label_1: ;
            }
            else
                item = ((id)0);
        }

    }
    return 0;
}

分析代碼:
1.緩沖區(qū)數(shù)組初始化固定的大小為16的數(shù)組__rw_items寓盗。
2.通過兩層的do-while嵌套循環(huán)實(shí)現(xiàn)了數(shù)組的遍歷,回答了前面的設(shè)想的問題璧函。
3.方法objc_enumerationMutation只有遍歷過程中對(duì)象被改變了才會(huì)被調(diào)用傀蚌,這一點(diǎn)還沒有深入研究。
4.相比于while循環(huán)效率上會(huì)有一定的損失蘸吓。

總結(jié):通過forin具體實(shí)現(xiàn)代碼的閱讀善炫,相信以后再去實(shí)現(xiàn)NSFastEnumeration協(xié)議時(shí)肯定是胸有成竹了,這里只是簡(jiǎn)單的記錄學(xué)習(xí)心得库继,大家有新的發(fā)現(xiàn)和想法歡迎一起討論箩艺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市宪萄,隨后出現(xiàn)的幾起案子艺谆,更是在濱河造成了極大的恐慌,老刑警劉巖拜英,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件静汤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡居凶,警方通過查閱死者的電腦和手機(jī)虫给,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侠碧,“玉大人狰右,你說我怎么就攤上這事∮叽玻” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)挨队。 經(jīng)常有香客問我谷暮,道長(zhǎng),這世上最難降的妖魔是什么盛垦? 我笑而不...
    開封第一講書人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任湿弦,我火速辦了婚禮,結(jié)果婚禮上腾夯,老公的妹妹穿的比我還像新娘颊埃。我一直安慰自己,他們只是感情好蝶俱,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開白布班利。 她就那樣靜靜地躺著,像睡著了一般榨呆。 火紅的嫁衣襯著肌膚如雪罗标。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評(píng)論 1 309
  • 那天积蜻,我揣著相機(jī)與錄音闯割,去河邊找鬼。 笑死竿拆,一個(gè)胖子當(dāng)著我的面吹牛宙拉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丙笋,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼谢澈,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了不见?” 一聲冷哼從身側(cè)響起澳化,我...
    開封第一講書人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稳吮,沒想到半個(gè)月后缎谷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灶似,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年列林,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酪惭。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡希痴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出春感,到底是詐尸還是另有隱情砌创,我是刑警寧澤虏缸,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站嫩实,受9級(jí)特大地震影響刽辙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜甲献,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一宰缤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晃洒,春花似錦慨灭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至桶略,卻和暖如春语淘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背际歼。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工惶翻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹅心。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓吕粗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親旭愧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颅筋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

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

  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,161評(píng)論 30 470
  • 2016年咱們暑假的閱讀作文班里,有三個(gè)五年級(jí)升六年級(jí)的小朋友输枯,作文寫得很不錯(cuò)议泵,很有靈氣。 為了鼓勵(lì)他們?cè)敢鈱懽鳎?..
    正高興閱讀 1,265評(píng)論 0 0
  • 晚上7點(diǎn),劉先生給他太太打了個(gè)電話:“今天晚上可能要通宵瞳收〉锞”“恩,知道了”正在哄孩子吃飯的劉太太輕描淡寫的應(yīng)了聲并...
    李珍妮666閱讀 949評(píng)論 7 5
  • 1. 你怎樣理解互聯(lián)網(wǎng)廣告的價(jià)值螟深?你認(rèn)為未來互聯(lián)網(wǎng)廣告的發(fā)展方向是怎樣的谐宙? 1.互聯(lián)網(wǎng)廣告充分體現(xiàn)了互聯(lián)網(wǎng)連接...
    子震閱讀 1,441評(píng)論 2 9