你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> ReactiveCocoa v2.5 源碼解析 之 架構總覽

ReactiveCocoa v2.5 源碼解析 之 架構總覽

編輯:IOS開發基礎

1.jpg

ReactiveCocoa 是一個 iOS 中的函數式響應式編程框架,它受 Functional Reactive Programming 的啟發,是 Justin Spahr-Summers 和 Josh Abernathy 在開發 GitHub for Mac 過程中的一個副產品,它提供了一系列用來組合和轉換值流的 API

Mattt Thompson 大神是這樣評價 ReactiveCocoa 的:

Breaking from a tradition of covering Apple APIs exclusively, this edition of NSHipster will look at an open source project that exemplifies this brave new era for Objective-C.

他認為 ReactiveCocoa 打破了蘋果 API 排他性的束縛,勇敢地開創了 Objective-C 的新紀元,具有劃時代的意義。不得不說,這對於一個第三方框架來說,已經是非常高的評價了。

關於 ReactiveCocoa 的版本演進歷程,簡單介紹如下:

  • <= v2.5Objective-C

  • v3.xSwift 1.2

  • v4.xSwift 2.x

:本文所介紹的均為 ReactiveCocoa v2.5 版本中的內容,這是 Objective-C 最新的穩定版本。另外,本文的目錄結構如下:

  • 簡介

  • 信號源

    • RACStream

    • RACSignal

    • RACSubject

    • RACSequence

  • 訂閱者

    • RACSubscriber

    • RACMulticastConnection

  • 調度器

    • RACScheduler

  • 清潔工

    • RACDisposable

  • 總結

  • 參考鏈接

簡介

ReactiveCocoa 是一個非常復雜的框架,在正式開始介紹它的核心組件前,我們先來看看它的類圖,以便從宏觀上了解它的層次結構:

ReactiveCocoa v2.5

從上面的類圖中,我們可以看出,ReactiveCocoa 主要由以下四大核心組件構成:

  • 信號源:RACStream 及其子類;

  • 訂閱者:RACSubscriber 的實現類及其子類;

  • 調度器:RACScheduler 及其子類;

  • 清潔工:RACDisposable 及其子類。

其中,信號源又是最核心的部分,其他組件都是圍繞它運作的。

對於一個應用來說,絕大部分的時間都是在等待某些事件的發生或響應某些狀態的變化,比如用戶的觸摸事件、應用進入後台、網絡請求成功刷新界面等等,而維護這些狀態的變化,常常會使代碼變得非常復雜,難以擴展。而 ReactiveCocoa 給出了一種非常好的解決方案,它使用信號來代表這些異步事件,提供了一種統一的方式來處理所有異步的行為,包括代理方法、block 回調、target-action 機制、通知、KVO 等:

// 代理方法
[[self
    rac_signalForSelector:@selector(webViewDidStartLoad:)
    fromProtocol:@protocol(UIWebViewDelegate)]
    subscribeNext:^(id x) {
        // 實現 webViewDidStartLoad: 代理方法
    }];

// target-action
[[self.avatarButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    subscribeNext:^(UIButton *avatarButton) {
        // avatarButton 被點擊了
    }

// 通知
[[[NSNotificationCenter defaultCenter]
    rac_addObserverForName:kReachabilityChangedNotification object:nil]
    subscribeNext:^(NSNotification *notification) {
        // 收到 kReachabilityChangedNotification 通知
    }];

// KVO
[RACObserve(self, username) subscribeNext:^(NSString *username) {
    // 用戶名發生了變化
}];

然而,這些還只是 ReactiveCocoa 的冰山一角,它真正強大的地方在於我們可以對這些不同的信號進行任意地組合和鏈式操作,從最原始的輸入 input 開始直至得到最終的輸出 output 為止:

[[[RACSignal
    combineLatest:@[ RACObserve(self, username), RACObserve(self, password) ]
    reduce:^(NSString *username, NSString *password) {
      return @(username.length > 0 && password.length > 0);
    }]
    distinctUntilChanged]
    subscribeNext:^(NSNumber *valid) {
        if (valid.boolValue) {
            // 用戶名和密碼合法,登錄按鈕可用
        } else {
            // 用戶名或密碼不合法,登錄按鈕不可用
        }
    }];

因此,對於 ReactiveCocoa 來說,我們可以毫不誇張地說,阻礙它發揮的瓶頸就只剩下你的想象力了。

信號源

ReactiveCocoa 中,信號源代表的是隨著時間而改變的值流,這是對 ReactiveCocoa 最精准的概括,訂閱者可以通過訂閱信號源來獲取這些值:

Streams of values over time.

你可以把它想象成水龍頭中的水,當你打開水龍頭時,水源源不斷地流出來;你也可以把它想象成電,當你插上插頭時,電靜靜地充到你的手機上;你還可以把它想象成運送玻璃珠的管道,當你打開閥門時,珠子一個接一個地到達。這裡的水、電、玻璃珠就是我們所需要的值,而打開水龍頭、插上插頭、打開閥門就是訂閱它們的過程。

RACStream

RACStreamReactiveCocoa 中最核心的類,代表的是任意的值流,它是整個 ReactiveCocoa 得以建立的基石,下面是它的繼承結構圖:

RACStream

事實上,RACStream 是一個抽象類,通常情況下,我們並不會去實例化它,而是直接使用它的兩個子類 RACSignalRACSequence 。那麼,問題來了,為什麼 RACStream 會被設計成一個抽象類?或者說它的抽象過程是以什麼作為依據的呢?

是的,沒錯,看過我上一篇文章 《Functor、Applicative 和 Monad》 的同學,應該已經知道了,RACStream 就是以 Monad 的概念為依據進行設計的,它代表的就是一個 Monad

/// An abstract class representing any stream of values.
///
/// This class represents a monad, upon which many stream-based operations can
/// be built.
///
/// When subclassing RACStream, only the methods in the main @interface body need
/// to be overridden.
@interface RACStream : NSObject

/// Lifts `value` into the stream monad.
///
/// Returns a stream containing only the given value.
+ (instancetype)return:(id)value;

/// Lazily binds a block to the values in the receiver.
///
/// This should only be used if you need to terminate the bind early, or close
/// over some state. -flattenMap: is more appropriate for all other cases.
///
/// block - A block returning a RACStreamBindBlock. This block will be invoked
///         each time the bound stream is re-evaluated. This block must not be
///         nil or return nil.
///
/// Returns a new stream which represents the combined result of all lazy
/// applications of `block`.
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;

@end

有了 Monad 作為基石後,許多基於流的操作就可以被建立起來了,比如 mapfilterzip 等。

RACSignal

RACSignal 代表的是未來將會被傳送的值,它是一種 push-driven 的流。RACSignal 可以向訂閱者發送三種不同類型的事件:

  • nextRACSignal 通過 next 事件向訂閱者傳送新的值,並且這個值可以為 nil

  • errorRACSignal 通過 error 事件向訂閱者表明信號在正常結束前發生了錯誤;

  • completedRACSignal 通過 completed 事件向訂閱者表明信號已經正常結束,不會再有後續的值傳送給訂閱者。

注意ReactiveCocoa 中的值流只包含正常的值,即通過 next 事件傳送的值,並不包括 errorcompleted 事件,它們需要被特殊處理。通常情況下,一個信號的生命周期是由任意個 next 事件和一個 error 事件或一個 completed 事件組成的。

從前面的類圖中,我們可以看出,RACSignal 並非只有一個類,事實上,它的一系列功能是通過類簇來實現的。除去我們將在下節介紹的 RACSubject 及其子類外,RACSignal 還有五個用來實現不同功能的私有子類:

  • RACEmptySignal :空信號,用來實現 RACSignal+empty 方法;

  • RACReturnSignal :一元信號,用來實現 RACSignal+return: 方法;

  • RACDynamicSignal :動態信號,使用一個 block 來實現訂閱行為,我們在使用 RACSignal+createSignal: 方法時創建的就是該類的實例;

  • RACErrorSignal :錯誤信號,用來實現 RACSignal+error: 方法;

  • RACChannelTerminal :通道終端,代表 RACChannel 的一個終端,用來實現雙向綁定。

對於 RACSignal 類簇來說,最核心的方法莫過於 -subscribe: 了,這個方法封裝了訂閱者對信號源的一次訂閱過程,它是訂閱者與信號源產生聯系的唯一入口。因此,對於 RACSignal 的所有子類來說,這個方法的實現邏輯就代表了該子類的具體訂閱行為,是區分不同子類的關鍵所在。同時,這也是為什麼 RACSignal 中的 -subscribe: 方法是一個抽象方法,並且必須要讓子類實現的原因:

- (RACDisposable *)subscribe:(id)subscriber {
  NSCAssert(NO, @"This method must be overridden by subclasses");
  return nil;
}

RACSubject

RACSubject 代表的是可以手動控制的信號,我們可以把它看作是 RACSignal 的可變版本,就好比 NSMutableArrayNSArray 的可變版本一樣。RACSubject 繼承自 RACSignal ,所以它可以作為信號源被訂閱者訂閱,同時,它又實現了 RACSubscriber 協議,所以它也可以作為訂閱者訂閱其他信號源,這個就是 RACSubject 為什麼可以手動控制的原因:

根據官方的 Design Guidelines 中的說法,我們應該盡可能少地使用它。因為它太過靈活,我們可以在任何時候任何地方操作它,所以一旦過度使用,就會使代碼變得非常復雜,難以理解。

根據我的實際使用經驗,在 MVVM 中使用 RACSubject 可以非常方便地實現統一的錯誤處理邏輯。比如,我們可以在 viewModel 的基類中聲明一個 RACSubject 類型的屬性 errors ,然後在 viewController 的基類中編寫統一的錯誤處理邏輯:

[self.viewModel.errors subscribeNext:^(NSError *error) {
    // 錯誤處理邏輯
}

此時,假設在某個界面的 viewModel 中有三個用來請求遠程數據的命令,分別是 requestReadmeMarkdownCommandrequestBlobCommandrequestReadmeHTMLCommand ,那麼這個界面的錯誤處理邏輯就可以這麼寫:

[[RACSignal
    merge:@[
        self.requestReadmeMarkdownCommand.errors,
        self.requestBlobCommand.errors,
        self.requestReadmeHTMLCommand.errors
    ]]
    subscribe:self.errors];

另外,RACSubject 也有三個用來實現不同功能的子類:

  • RACGroupedSignal :分組信號,用來實現 RACSignal 的分組功能;

  • RACBehaviorSubject :重演最後值的信號,當被訂閱時,會向訂閱者發送它最後接收到的值;

  • RACReplaySubject :重演信號,保存發送過的值,當被訂閱時,會向訂閱者重新發送這些值。

RACSubject 的功能非常強大,但是太過靈活,也正是因為如此,我們只有在迫不得已的情況下才會使用它。

RACSequence

RACSequence 代表的是一個不可變的值的序列,與 RACSignal 不同,它是 pull-driven 類型的流。從嚴格意義上講,RACSequence 並不能算作是信號源,因為它並不能像 RACSignal 那樣,可以被訂閱者訂閱,但是它與 RACSignal 之間可以非常方便地進行轉換。

從理論上說,一個 RACSequence 由兩部分組成:

  • head :指的是序列中的第一個對象,如果序列為空,則為 nil

  • tail :指的是序列中除第一個對象外的其它所有對象,同樣的,如果序列為空,則為 nil

事實上,一個序列的 tail 仍然是一個序列,如果我們將序列看作是一條毛毛蟲,那麼 headtail 可表示如下:

listmonster

同樣的,一個序列的 tail 也可以看作是由 headtail 組成,而這個新的 tail 又可以繼續看作是由 headtail 組成,這個過程可以一直進行下去。而這個就是 RACSequence 得以建立的理論基礎,所以一個 RACSequence 子類的最小實現就是 headtail

/// Represents an immutable sequence of values. Unless otherwise specified, the
/// sequences' values are evaluated lazily on demand. Like Cocoa collections,
/// sequences cannot contain nil.
///
/// Most inherited RACStream methods that accept a block will execute the block
/// _at most_ once for each value that is evaluated in the returned sequence.
/// Side effects are subject to the behavior described in
/// +sequenceWithHeadBlock:tailBlock:.
///
/// Implemented as a class cluster. A minimal implementation for a subclass
/// consists simply of -head and -tail.
@interface RACSequence : RACStream /// The first object in the sequence, or nil if the sequence is empty.
///
/// Subclasses must provide an implementation of this method.
@property (nonatomic, strong, readonly) id head;

/// All but the first object in the sequence, or nil if the sequence is empty.
///
/// Subclasses must provide an implementation of this method.
@property (nonatomic, strong, readonly) RACSequence *tail;

@end

總的來說,RACSequence 存在的最大意義就是為了簡化 Objective-C 中的集合操作:

Simplifying Collection Transformations: Higher-order functions like map, filter, fold/reduce are sorely missing from Foundation.

比如下面的代碼:

NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
    if (str.length < 2) {
        continue;
    }

    NSString *newString = [str stringByAppendingString:@"foobar"];
    [results addObject:newString];
}

可以用 RACSequence 來優雅地實現:

RACSequence *results = [[strings.rac_sequence
    filter:^ BOOL (NSString *str) {
        return str.length >= 2;
    }]
    map:^(NSString *str) {
        return [str stringByAppendingString:@"foobar"];
    }];

因此,我們可以非常方便地使用 RACSequence 來實現集合的鏈式操作,直到得到你想要的最終結果為止。除此之外,使用 RACSequence 的另外一個主要好處是,RACSequence 中包含的值在默認情況下是懶計算的,即只有在真正用到的時候才會被計算,並且只會計算一次。也就是說,如果我們只用到了一個 RACSequence 中的部分值的時候,它就在不知不覺中提高了我們應用的性能。

同樣的,RACSequence 的一系列功能也是通過類簇來實現的,它共有九個用來實現不同功能的私有子類:

  • RACUnarySequence :一元序列,用來實現 RACSequence+return: 方法;

  • RACIndexSetSequence :用來遍歷索引集;

  • RACEmptySequence :空序列,用來實現 RACSequence+empty 方法;

  • RACDynamicSequence :動態序列,使用 blocks 來動態地實現一個序列;

  • RACSignalSequence :用來遍歷信號中的值;

  • RACArraySequence :用來遍歷數組中的元素;

  • RACEagerSequence :非懶計算的序列,在初始化時立即計算所有的值;

  • RACStringSequence :用來遍歷字符串中的字符;

  • RACTupleSequence :用來遍歷元組中的元素。

RACSequence 為類簇提供了統一的對外接口,對於使用它的客戶端代碼來說,完全不需要知道私有子類的存在,很好地隱藏了實現細節。另外,值得一提的是,RACSequence 實現了快速枚舉的協議 NSFastEnumeration ,在這個協議中只聲明了一個看上去非常抽筋的方法:

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

有興趣的同學,可以看看 RACSequence 中的相關實現,我們將會在後續的文章中進行介紹。因此,我們也可以直接使用 for in 來遍歷一個 RACSequence

訂閱者

現在,我們已經知道信號源是什麼了,為了獲取信號源中的值,我們需要對信號源進行訂閱。在 ReactiveCocoa 中,訂閱者是一個抽象的概念,所有實現了 RACSubscriber 協議的類都可以作為信號源的訂閱者。

RACSubscriber

RACSubscriber 協議中,聲明了四個必須實現的方法:

/// Represents any object which can directly receive values from a RACSignal.
///
/// You generally shouldn't need to implement this protocol. +[RACSignal
/// createSignal:], RACSignal's subscription methods, or RACSubject should work
/// for most uses.
///
/// Implementors of this protocol may receive messages and values from multiple
/// threads simultaneously, and so should be thread-safe. Subscribers will also
/// be weakly referenced so implementations must allow that.
@protocol RACSubscriber @required

/// Sends the next value to subscribers.
///
/// value - The value to send. This can be `nil`.
- (void)sendNext:(id)value;

/// Sends the error to subscribers.
///
/// error - The error to send. This can be `nil`.
///
/// This terminates the subscription, and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendError:(NSError *)error;

/// Sends completed to subscribers.
///
/// This terminates the subscription, and invalidates the subscriber (such that
/// it cannot subscribe to anything else in the future).
- (void)sendCompleted;

/// Sends the subscriber a disposable that represents one of its subscriptions.
///
/// A subscriber may receive multiple disposables if it gets subscribed to
/// multiple signals; however, any error or completed events must terminate _all_
/// subscriptions.
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;

@end

其中 -sendNext:-sendError:-sendCompleted 分別用來從 RACSignal 接收 nexterrorcompleted 事件,而 -didSubscribeWithDisposable: 則用來接收代表某次訂閱的 disposable 對象。

訂閱者對信號源的一次訂閱過程可以抽象為:通過 RACSignal-subscribe: 方法傳入一個訂閱者,並最終返回一個 RACDisposable 對象的過程:

注意:在 ReactiveCocoa 中並沒有專門的類 RACSubscription 來代表一次訂閱,而間接地使用 RACDisposable 來充當這一角色。因此,一個 RACDisposable 對象就代表著一次訂閱,並且我們可以用它來取消這次訂閱,詳細內容將會在下面的章節中進行介紹。

除了 RACSignal 的子類外,還有兩個實現了 RACSubscriber 協議的類,如下圖所示:

其中,RACSubscriber 類的名字與 RACSubscriber 協議的名字相同,這跟 Objective-C 中的 NSObject 類的名字與 NSObject 協議的名字相同是一樣一樣的,除了名字相同外,然並卵。通常來說,RACSubscriber 類充當的角色就是信號源的真正訂閱者,它老老實實地實現了 RACSubscriber 協議。

既然 RACSubscriber 類就是真正的訂閱者,那麼 RACPassthroughSubscriber 類又是干嘛用的呢?原來,在 ReactiveCocoa 中,一個訂閱者是可以訂閱多個信號源的,也就是說它會擁有多個 RACDisposable 對象,並且它可以隨時取消其中任何一個訂閱。為了實現這個功能,ReactiveCocoa 就引入了 RACPassthroughSubscriber 類,它是 RACSubscriber 類的一個裝飾器,封裝了一個真正的訂閱者 RACSubscriber 對象,它負責轉發所有事件給這個真正的訂閱者,而當此次訂閱被取消時,它就會停止轉發:

- (void)sendNext:(id)value {
  if (self.disposable.disposed) return;
  [self.innerSubscriber sendNext:value];
}

- (void)sendError:(NSError *)error {
  if (self.disposable.disposed) return;
  [self.innerSubscriber sendError:error];
}

- (void)sendCompleted {
  if (self.disposable.disposed) return;
  [self.innerSubscriber sendCompleted];
}

事實上,在 ReactiveCocoa 中,我們傾向於隱藏訂閱者,因為外界根本不需要知道訂閱者的存在,這是內部的實現細節。這樣做的主要目的是進一步簡化信號源的訂閱邏輯,客戶端代碼只需要關心它所需要的值就可以了,根本不需要關心內部的訂閱過程。

RACMulticastConnection

通常來說,我們在訂閱一個信號源的過程中可能會產生副作用或者消耗比較大的資源,比如修改全局變量、發送網絡請求等。這個時候,我們往往需要讓多個訂閱者之間共享一次訂閱,就好比我們讀高中時,多個好朋友一起訂閱一份英語周報,然後只要出一份錢,是一個道理。這就是 ReactiveCocoa 中引入 RACMulticastConnection 類的原因。

RACMulticastConnection 通過一個標志 _hasConnected 來保證只對 sourceSignal 訂閱一次,然後對外暴露一個 RACSubject 類型的 signal 供外部訂閱者訂閱。這樣一來,不管外部訂閱者對 signal 訂閱多少次,我們對 sourceSignal 的訂閱至多只會有一次:

RACMulticastConnection

:了解 RACMulticastConnection 的實現原理,對於我們後面理解 -replayreplayLastreplayLazily 等方法非常有幫助。

調度器

有了信號源和訂閱者,我們還需要由調度器來統一調度訂閱者訂閱信號源的過程中所涉及到的任務,這樣才能保證所有的任務都能夠合理有序地執行。

RACScheduler

RACSchedulerReactiveCocoa 中就是扮演著調度器的角色,本質上,它就是用 GCD 的串行隊列來實現的,並且支持取消操作。是的,在 ReactiveCocoa 中,並沒有使用到 NSOperationQueueNSRunloop 等技術,RACScheduler 也只是對 GCD 的簡單封裝而已。

同樣的,RACScheduler 的一系列功能也是通過類簇來實現的,除了用來測試的子類外,總共還有四個私有子類:

咋看之下,RACScheduler 的兒子貌似還不少,但是真正出力干活的卻真心不多,主要就是 RACTargetQueueScheduler 子類:

  • RACImmediateScheduler :立即執行調度的任務,這是唯一一個支持同步執行的調度器;

  • RACQueueScheduler :一個抽象的隊列調度器,在一個 GCD 串行列隊中異步調度所有任務;

  • RACTargetQueueScheduler :繼承自 RACQueueScheduler ,在一個以一個任意的 GCD 隊列為 target 的串行隊列中異步調度所有任務;

  • RACSubscriptionScheduler :一個只用來調度訂閱的調度器。

值得一提的是,在 RACScheduler 中有一個非常特殊的方法:

- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock;

這個方法的作用非常有意思,它可以將遞歸調用轉換成迭代調用,這樣做的目的是為了解決深層次的遞歸調用可能會帶來的堆棧溢出問題。

清潔工

正如我們前面所說的,在訂閱者訂閱信號源的過程中,可能會產生副作用或者消耗一定的資源,所以當我們在取消訂閱或者完成訂閱時,我們就需要做一些資源回收和垃圾清理的工作。

RACDisposable

RACDisposableReactiveCocoa 中就充當著清潔工的角色,它封裝了取消和清理一次訂閱所必需的工作。它有一個核心的方法 -dispose ,調用這個方法就會執行相應的清理工作,這有點類似於 NSObject-dealloc 方法。RACDisposable 總共有四個子類,它的繼承結構圖如下:

  • RACSerialDisposable :作為 disposable 的容器使用,可以包含一個 disposable 對象,並且允許將這個 disposable 對象通過原子操作交換出來;

  • RACKVOTrampoline :代表一次 KVO 觀察,並且可以用來停止觀察;

  • RACCompoundDisposable :跟 RACSerialDisposable 一樣,RACCompoundDisposable 也是作為 disposable 的容器使用。不同的是,它可以包含多個 disposable 對象,並且支持手動添加和移除 disposable 對象,有點類似於可變數組 NSMutableArray 。而當一個 RACCompoundDisposable 對象被 disposed 時,它會調用其所包含的所有 disposable 對象的 -dispose 方法,有點類似於 autoreleasepool 的作用;

  • RACScopedDisposable :當它被 dealloc 的時候調用本身的 -dispose 方法。

咋看之下,RACDisposable 的邏輯似乎有些復雜,不過換湯不換藥,不管它們怎麼換著花樣玩,最終都只是為了能夠在合適的時機調用 disposable 對象的 -dispose 方法,執行清理工作而已。

總結

至此,我們介紹完了 ReactiveCocoa 的四大核心組件,對它的架構有了宏觀上的認識。它建立於 Monad 的概念之上,然後圍繞其搭建了一系列完整的配套組件,它們共同支撐了 ReactiveCocoa 的強大功能。盡管,ReactiveCocoa 是一個重型的函數式響應式框架,但是它並不會對我們現有的代碼構成侵略性,我們完全可以在一個單獨的類中使用它,哪怕只是簡單的一行代碼,也是沒有問題的。所以,如果你對 ReactiveCocoa 感興趣的話,不妨就從現在開始嘗試吧,Let’s go !

PS:MVVMReactiveCocoa 是我用 MVVM + RAC 編寫的一個開源應用,如果你有興趣的話不妨 clone 下來看看 ReactiveCocoa 的具體實踐吧。

參考鏈接

https://github.com/ReactiveCocoa/ReactiveCocoa/tree/v2.5
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/Documentation/FrameworkOverview.md
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/v2.5/Documentation/DesignGuidelines.md#avoid-using-subjects-when-possible
http://nshipster.com/reactivecocoa/
http://nathanli.cn/2015/08/27/reactivecocoa2-%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90/
http://blog.devtang.com/blog/2014/02/11/reactivecocoa-introduction/
http://m.oschina.net/blog/294178

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved