ReactiveCocoa 中 集合類RACSequence 和 RACTuple底層實現分析

一縷殤流化隱半邊冰霜發表於2016-12-29
111194012-8f233f770f58594b

前言

在OOP的世界裡使用FRP的思想來程式設計,光有函式這種一等公民,還是無法滿足我們一些需求的。因此還是需要引用變數來完成各式各樣的類的操作行為。

在前幾篇文章中詳細的分析了RACStream中RACSignal的底層實現。RACStream還有另外一個子類,RACSequence,這個類是RAC專門為集合而設計的。這篇文章就專門分析一下RACSequence的底層實現。

目錄

  • 1.RACTuple底層實現分析
  • 2.RACSequence底層實現分析
  • 3.RACSequence操作實現分析
  • 4.RACSequence的一些擴充套件

一. RACTuple底層實現分析

在分析RACSequence之前,先來看看RACTuple的實現。RACTuple是ReactiveCocoa的元組類。

121194012-fd4ed6f45a960af0

1. RACTuple

RACTuple的定義看上去很簡單,底層實質就是一個NSArray,只不過封裝了一些方法。RACTuple繼承了NSCoding, NSCopying, NSFastEnumeration這三個協議。

這裡是NSCoding協議。都是對內部的backingArray進行decodeObjectForKey:和encodeObject: 。

上面這是NSCopying協議。由於內部是基於NSArray的,所以是immutable不可變的。

上面是NSFastEnumeration協議,快速列舉也都是針對NSArray進行的操作。

RACTuple的方法也不多,總共就6個方法,3個類方法,3個例項方法。

先看類方法:

先看這兩個類方法,這兩個類方法的區別在於是否把NSNull轉換成RACTupleNil型別。根據入參array初始化RACTuple內部的NSArray。

131194012-0cbbbf49383bb05f

RACTuplePack( ) 和 RACTuplePack_( )這兩個巨集的實現也是呼叫了tupleWithObjectsFromArray:方法

這裡需要注意的是RACTupleNil

RACTupleNil是一個單例。

重點需要解釋的是另外一種類方法:

這個類方法的引數是可變引數型別。由於用到了可變引數型別,所以就會用到va_list,va_start,va_arg,va_end。

  1. va_list用於宣告一個變數,我們知道函式的可變引數列表其實就是一個字串,所以va_list才被宣告為字元型指標,這個型別用於宣告一個指向引數列表的字元型指標變數,例如:va_list ap;//ap:arguement pointer
  2. va_start(ap,v),它的第一個引數是指向可變引數字串的變數,第二個引數是可變引數函式的第一個引數,通常用於指定可變引數列表中引數的個數。
  3. va_arg(ap,t),它的第一個引數指向可變引數字串的變數,第二個引數是可變引數的型別。
  4. va_end(ap) 用於將存放可變引數字串的變數清空(賦值為NULL)。

剩下的3個例項方法都是對陣列的操作,沒有什麼難度。

一般使用用兩個巨集,RACTupleUnpack( ) 用來解包,RACTuplePack( ) 用來裝包。

關於RACTuple還有2個相關的類,RACTupleUnpackingTrampoline,RACTupleSequence。

2. RACTupleUnpackingTrampoline

首先這個類是一個單例。

RACTupleUnpackingTrampoline這個類也就只有一個作用,就是它對應的例項方法。

這個方法裡面會遍歷入引數組NSArray,然後依次取出陣列裡面每個value 的指標,用這個指標又賦值給了tuple[index]。

為了解釋清楚這個方法的作用,寫出測試程式碼:

輸出如下:

這個函式的作用也就一清二楚了。但是平時我們是很少用到[NSValue valueWithPointer:&string]這種寫法的。究竟是什麼地方會用到這個函式呢?全域性搜尋一下,找到了用到這個的地方。

在RACTuple 中兩個非常有用的巨集:RACTupleUnpack( ) 用來解包,RACTuplePack( ) 用來裝包。RACTuplePack( )的實現在上面分析過了,實際是呼叫tupleWithObjectsFromArray:方法。那麼RACTupleUnpack( ) 的巨集是怎麼實現的呢?這裡就用到了RACTupleUnpackingTrampoline。

141194012-25f7f4179921d514

以上就是RACTupleUnpack( ) 具體的巨集。看上去很複雜。還是寫出測試程式碼分析分析。

把上述的程式碼編譯之後的程式碼貼出來:

轉換成這樣就比較好理解了。RACTupleUnpack_after284: 是一個標號。RACTupleUnpack_state284初始值為0,在下面while裡面有一個for迴圈,在這個迴圈裡面會進行解包操作,也就是會呼叫setObject:forKeyedSubscript:函式。

在迴圈裡面,

這裡就是呼叫了[NSValue valueWithPointer:&string]的寫法。

至此,RACTupleUnpackingTrampoline這個類的作用也已明瞭,它是被作用設計出來用來實現神奇的RACTupleUnpack( ) 這個巨集。

當然RACTupleUnpackingTrampoline這個類的setObject:forKeyedSubscript:函式也可以使用,只不過要注意寫法,注意指標的型別,在NSValue裡面包裹的是valueWithPointer,(nullable const void *)pointer型別的。

3. RACTupleSequence

這個類僅僅只是名字裡面帶有Tuple而已,它其實是繼承自RACSequence。

需要分析這個類的原因是因為RACTuple裡面有一個擴充的屬性rac_sequence。

還是先看看RACTupleSequence的定義。

這個類是繼承自RACSequence,而且只有這一個類方法。

tupleBackingArray是來自於RACTuple裡面的backingArray。

RACTupleSequence這個類的目的就是把Tuple轉換成Sequence。Sequence裡面的陣列就是Tuple內部的backingArray。offset從0開始。

二. RACSequence底層實現分析

151194012-17c905999294e408

RACSequence是RACStream的子類,主要是ReactiveCocoa裡面的集合類。

先來說說關於RACSequence的一些概念。

RACSequence有兩個很重要的屬性就是head和tail。head是一個id,而tail又是一個RACSequence,這個定義有點遞迴的意味。

輸出:

這段測試程式碼就道出了head和tail的定義。更加詳細的描述見下圖:

161194012-882eed872690c203

上述程式碼裡面用到了RACSequence初始化的方法,具體的分析見後面。

objectEnumerator是一個快速列舉器。

之所以需要實現這個,是為了更加方便的RACSequence進行遍歷。

有了這個NSEnumerator,就可以從RACSequence的head一直遍歷到tail。

回到RACSequence的定義裡面的objectEnumerator,這裡就是取出內部的RACSequenceEnumerator。

RACSequence的定義裡面還有一個array,這個陣列就是返回一個NSArray,這個陣列裡面裝滿了RACSequence裡面所有的物件。這裡之所以能用for-in,是因為實現了NSFastEnumeration協議。至於for-in的效率,完全就看重寫NSFastEnumeration協議裡面countByEnumeratingWithState: objects: count: 方法裡面的執行效率了。

在分析RACSequence的for-in執行效率之前,先回顧一下NSFastEnumerationState的定義,這裡的屬性在接下來的實現中會被大量使用。

接下來要分析的這個函式的入參,stackbuf是為for-in提供的物件陣列,len是該陣列的長度。

整個遍歷的過程類似遞迴的過程,從頭到尾依次遍歷一遍。

再來研究研究RACSequence的初始化:

初始化RACSequence,會呼叫RACDynamicSequence。這裡有點類比RACSignal的RACDynamicSignal。

再來看看RACDynamicSequence的定義。

這裡需要說明的是此處的headBlock,tailBlock,dependencyBlock的修飾符都是用了strong,而不是copy。這裡是一個很奇怪的bug導致的。在https://github.com/ReactiveCocoa/ReactiveCocoa/issues/505中詳細記錄了用copy關鍵字會導致記憶體洩露的bug。具體程式碼如下:

最終發現這個問題的人把copy改成strong就神奇的修復了這個bug。最終整個ReactiveCocoa庫裡面就只有這裡把block的關鍵字從copy改成了strong,而不是所有的地方都改成strong。

原作者Justin Spahr-Summers大神對這個問題的最終解釋是:

Maybe there’s just something weird with how we override dealloc, set the blocks from a class method, cast them, or something else.

所以日常我們寫block的時候,沒有特殊情況,依舊需要繼續用copy進行修飾。

hasDependency這個變數是代表是否有dependencyBlock。這個函式裡面就只把headBlock和tailBlock儲存起來了。

另外一個類方法sequenceWithLazyDependency: headBlock: tailBlock:是帶有dependencyBlock的,這個方法裡面會儲存headBlock,tailBlock,dependencyBlock這3個block。

從RACSequence這兩個唯一的初始化方法之間就引出了RACSequence兩大核心問題之一,積極運算 和 惰性求值。

1. 積極運算 和 惰性求值

在RACSequence的定義中還有兩個RACSequence —— eagerSequence 和 lazySequence。這兩個RACSequence就是分別對應著積極運算的RACSequence和惰性求值的RACSequence。

關於這兩個概念最最新形象的比喻還是臧老師部落格裡面的這篇文章聊一聊iOS開發中的惰性計算裡面寫的一段笑話。引入如下:

有一隻小白兔,跑到蔬菜店裡問老闆:“老闆,有100個胡蘿蔔嗎?”。老闆說:“沒有那麼多啊。”,小白兔失望的說道:“哎,連100個胡蘿蔔都沒有。。。”。第二天小白兔又來到蔬菜店問老闆:“今天有100個胡蘿蔔了吧?”,老闆尷尬的說:“今天還是缺點,明天就能好了。”,小白兔又很失望的走了。第三天小白兔剛一推門,老闆就高興的說道:“有了有了,從前天就進貨的100個胡蘿蔔到貨了。”,小白兔說:“太好了,我要買2根!”。。。

如果日常我們遇到了這種問題,就很浪費記憶體空間了。比如在記憶體裡面開了一個100W大小的陣列,結果實際只使用到100個數值。這個時候就需要用到惰性運算了。

在RACSequence裡面這兩種方式都支援,我們來看看底層原始碼是如何實現的。

先來看看平時我們很熟悉的情況——積極運算。

171194012-475743a8c62f161a

在RACSequence中積極運算的代表是RACSequence的一個子類RACArraySequence的子類——RACEagerSequence。它的積極運算表現在其bind函式上。

從上述程式碼中能看到主要是進行了2層迴圈,最外層迴圈遍歷的自己RACSequence中的值,然後拿到這個值傳入閉包bindBlock( )中,返回一個RACSequence,最後用一個NSMutableArray依次把每個RACSequence裡面的值都裝起來。

第二個for-in迴圈是在遍歷RACSequence,之所以可以用for-in的方式遍歷就是因為實現了NSFastEnumeration協議,實現了countByEnumeratingWithState: objects: count: 方法,這個方法在上面詳細分析過了,這裡不再贅述。

這裡就是一個積極運算的例子,在每次迴圈中都會把閉包block( )的值計算出來。值得說明的是,最後返回的RACSequence的型別是self.class型別的,即還是RACEagerSequence型別的。

再來看看RACSequence中的惰性求值是怎麼實現的。

在RACSequence中,bind函式是下面這個樣子:

實際上呼叫了bind: passingThroughValuesFromSequence:方法,第二個入參傳入nil。

在bind: passingThroughValuesFromSequence:方法的實現中,就是用sequenceWithLazyDependency: headBlock: tailBlock:方法生成了一個RACSequence,並返回。在sequenceWithLazyDependency: headBlock: tailBlock:上面分析過原始碼,主要目的是為了儲存3個閉包,headBlock,tailBlock,dependencyBlock。

通過呼叫RACSequence裡面的bind操作,並沒有執行3個閉包裡面的值,只是儲存起來了。這裡就是惰性求值的表現——等到要用的時候才會計算。

通過上述原始碼的分析,可以寫出如下的測試程式碼加深理解。

上述程式碼執行之後,會輸出如下資訊:

只輸出了5遍eagerSequence,lazySequence並沒有輸出。原因是因為bind閉包只在eagerSequence中真正被呼叫執行了,而在lazySequence中bind閉包僅僅只是被copy了。

那如何讓lazySequence執行bind閉包呢?

通過執行上述程式碼,就可以輸出5遍“lazySequence”了。因為bind閉包再次會被呼叫執行。

積極運算 和 惰性求值在這裡就區分出來了。在RACSequence中,除去RACEagerSequence只積極運算,其他的Sequence都是惰性求值的。

接下來再繼續分析RACSequence是如何實現惰性求值的。

181194012-0bb331be85abeed4

在bind操作中建立了這樣一個lazySequence,3個block閉包儲存瞭如何建立一個lazySequence的做法。

headBlock是入參為id,返回值也是一個id。在建立lazySequence的head的時候,並不關心入參,直接返回passthroughSequence的head。

tailBlock是入參為id,返回值為RACSequence。由於RACSequence的定義類似遞迴定義的,所以tailBlock會再次遞迴呼叫bind:passingThroughValuesFromSequence:產生一個RACSequence作為新的sequence的tail。

dependencyBlock的返回值是作為headBlock和tailBlock的入參。不過現在headBlock和tailBlock都不關心這個入參。那麼dependencyBlock就是成為了headBlock和tailBlock閉包執行之前要執行的閉包。

dependencyBlock的目的是為了把原來的sequence裡面的值,都進行一次變換。current是入參passthroughSequence,valuesSeq就是原sequence的引用。每次迴圈一次就取出原sequence的頭,直到取不到為止,就是遍歷完成。

取出valuesSeq的head,傳入bindBlock( )閉包進行變換,返回值是一個current 的sequence。在每次headBlock和tailBlock之前都會呼叫這個dependencyBlock,變換後新的sequence的head就是current的head,新的sequence的tail就是遞迴呼叫傳入的current.tail。

RACDynamicSequence建立的lazyDependency的過程就是儲存了3個block的過程。那這些閉包什麼時候會被呼叫呢?

上面的原始碼就是獲取RACDynamicSequence中head的實現。當要取出sequence的head的時候,就會呼叫headBlock( )。如果儲存了dependencyBlock閉包,在執行headBlock( )之前會先執行dependencyBlock( )進行一次變換。

獲取RACDynamicSequence中tail的時候,和獲取head是一樣的,當需要取出tail的時候才會呼叫tailBlock( )。當有dependencyBlock閉包,會先執行dependencyBlock閉包,再呼叫tailBlock( )。

總結一下:

  1. RACSequence的惰性求值,除去RACEagerSequence的bind函式以外,其他所有的Sequence都是基於惰性求值的。只有到取出來運算之前才會去把相應的閉包執行一遍。
  2. 在RACSequence所有函式中,只有bind函式會傳入dependencyBlock( )閉包,(RACEagerSequence會重寫這個bind函式),所以看到dependencyBlock( )閉包一定可以推斷出是RACSequence做了變換操作了。

2. Pull-driver 和 Push-driver

191194012-a4e49c3050c71cc1

在RACSequence中有一個方法可以讓RACSequence和RACSignal進行關聯上。

RACSequence中的signal方法會呼叫signalWithScheduler:方法。在signalWithScheduler:方法中會建立一個新的訊號。這個新的訊號的RACDisposable訊號由scheduleRecursiveBlock:產生。

這段程式碼雖然長,但是拆分分析一下:

rescheduleCount 是遞迴次數計數。rescheduleImmediately這個BOOL是決定是否立即執行reallyReschedule( )閉包。

recursiveBlock是入參,它實際是下面這段閉包程式碼:

recursiveBlock的入參是reschedule( )。執行完上面的程式碼之後開始執行入參reschedule( )的程式碼,入參reschedule( 閉包的程式碼是如下:

在這段block中會統計rescheduleCount,如果rescheduleImmediately為YES還會繼續開始執行遞迴操作reallyReschedule( )。

最終會在這個迴圈裡面遞迴呼叫reallyReschedule( )閉包。reallyReschedule( )閉包執行的操作就是再次執行scheduleRecursiveBlock:recursiveBlock addingToDisposable:disposable方法。

每次執行一次遞迴就會取出sequence的head值傳送出來,直到sequence.head = = nil傳送完成訊號。

既然RACSequence也可以轉換成RACSignal,那麼就需要總結一下兩者的異同點。

總結一下:

RACSequence 和 RACSignal 異同點對比:

201194012-5d113caff7831592
  1. RACSequence除去RACEagerSequence,其他所有的都是基於惰性計算的,這和RACSignal是一樣的。
  2. RACSequence是在時間上是連續的,一旦把RACSequence變成signal,再訂閱,會立即把所有的值一口氣都傳送出來。RACSignal是在時間上是離散的,當有事件到來的時候,才會傳送出資料流。
  3. RACSequence是Pull-driver,由訂閱者來決定是否傳送值,只要訂閱者訂閱了,就會傳送資料流。RACSignal是Push-driver,它傳送資料流是不由訂閱者決定的,不管有沒有訂閱者,它有離散事件產生了,就會傳送資料流。
  4. RACSequence傳送的全是資料,RACSignal傳送的全是事件。事件不僅僅包括資料,還包括事件的狀態,比如說事件是否出錯,事件是否完成。

三. RACSequence操作實現分析

211194012-40f973dae611c354

RACSequence還有以下幾個操作。

1. foldLeftWithStart: reduce:

這個函式傳入了一個初始值start,然後依次迴圈執行reduce( ),迴圈之後,最終的值作為返回值返回。這個函式就是摺疊函式,從左邊摺疊到右邊。

2. foldRightWithStart: reduce:

這個函式和上一個foldLeftWithStart: reduce:是一樣的,只不過方向是從右往左。

3. objectPassingTest:

objectPassingTest:裡面會呼叫RACStream中的filter:函式,這個函式在前幾篇文章分析過了。如果block(value)為YES,就代表通過了Test,那麼就會返回value的sequence。取出head返回。

4. any:

any:會呼叫objectPassingTest:函式,如果不為nil就代表有value值通過了Test,有通過了value的就返回YES,反之返回NO。

5. all:

all:會從左往右依次對每個值進行block( ) Test,然後每個值依次進行&&操作。

6. concat:

concat:的操作和RACSignal的作用是一樣的。它會把原sequence和入參stream連線到一起,組合成一個高階sequence,最後呼叫flatten“拍扁”。關於flatten的實現見前幾篇RACStream裡面的flatten實現分析。

7. zipWith:

由於sequence的定義是遞迴形式的,所以zipWith:也是遞迴來進行的。zipWith:新的sequence的head是原來2個sequence的head組合成RACTuplePack。新的sequence的tail是原來2個sequence的tail遞迴呼叫zipWith:。

四. RACSequence的一些擴充套件

221194012-a53eafb26e8ba030

關於RACSequence有以下9個子類,其中RACEagerSequence是繼承自RACArraySequence。這些子類看名字就知道sequence裡面裝的是什麼型別的資料。RACUnarySequence裡面裝的是單元sequence。它只有head值,沒有tail值。

231194012-7b0eea8206881961

RACSequenceAdditions 總共有7個Category。這7個Category分別對iOS 裡面的集合類進行了RACSequence的擴充套件,使我們能更加方便的使用RACSequence。

1. NSArray+RACSequenceAdditions

這個Category能把任意一個NSArray陣列轉換成RACSequence。

根據NSArray建立一個RACArraySequence並返回。

2. NSDictionary+RACSequenceAdditions

這個Category能把任意一個NSDictionary字典轉換成RACSequence。

rac_sequence會把字典都轉化為一個裝滿RACTuplePack的RACSequence,在這個RACSequence中,第一個位置是key,第二個位置是value。

rac_keySequence是裝滿所有key的RACSequence。

rac_valueSequence是裝滿所有value的RACSequence。

3. NSEnumerator+RACSequenceAdditions

這個Category能把任意一個NSEnumerator轉換成RACSequence。

返回的RACSequence的head是當前的sequence的head,tail就是當前的sequence。

4. NSIndexSet+RACSequenceAdditions

這個Category能把任意一個NSIndexSet轉換成RACSequence。

返回RACIndexSetSequence,在這個IndexSetSequence中,data裡面裝的NSData,indexes裡面裝的NSUInteger,count裡面裝的是index的總數。

5. NSOrderedSet+RACSequenceAdditions

這個Category能把任意一個NSOrderedSet轉換成RACSequence。

返回的NSOrderedSet中的陣列轉換成sequence。

6. NSSet+RACSequenceAdditions

這個Category能把任意一個NSSet轉換成RACSequence。

根據NSSet的allObjects陣列建立一個RACArraySequence並返回。

7. NSString+RACSequenceAdditions

這個Category能把任意一個NSString轉換成RACSequence。

返回的是一個裝滿string字元的陣列對應的sequence。

最後

關於RACSequence 和 RACTuple底層實現分析都已經分析完成。最後請大家多多指教。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

ReactiveCocoa 中 集合類RACSequence 和 RACTuple底層實現分析 ReactiveCocoa 中 集合類RACSequence 和 RACTuple底層實現分析

相關文章