前言
在使用ReactiveCocoa 過程中,Josh Abernathy和Justin Spahr-Summers 兩位大神為了能讓RAC的使用者更暢快的在沉浸在FRP的世界裡,更好的進行併發程式設計,於是就對GCD進行了一次封裝,並與RAC的各大元件進行了完美的整合。
自從有了RACScheduler以後,使整個RAC併發程式設計的程式碼裡面更加和諧統一,更加順手,更加“ReactiveCocoa”。
目錄
- 1.RACScheduler是如何封裝GCD的
- 2.RACSequence的一些子類
- 3.RACScheduler是如何“取消”併發任務的
- 4.RACScheduler是如何和RAC其他元件進行完美整合的
一. RACScheduler是如何封裝GCD的
RACScheduler在ReactiveCocoa中到底是幹嘛的呢?處於什麼地位呢?官方給出的定義如下:
1 |
Schedulers are used to control when and where work is performed |
RACScheduler在ReactiveCocoa中是用來控制一個任務,何時何地被執行。它主要是用來解決ReactiveCocoa中併發程式設計的問題的。
RACScheduler的實質是對GCD的封裝,底層就是GCD實現的。
要分析RACScheduler,先來回顧一下GCD。
眾所周知,在GCD中,Dispatch Queue主要分為2類,Serial Dispatch Queue 和 Concurrent Dispatch Queue 。其中Serial Dispatch Queue是等待現在執行中處理結束的佇列,Concurrent Dispatch Queue是不等待現在執行中處理結束的佇列。
生成Dispatch Queue的方法也有2種,第一種方式是通過GCD的API生成Dispatch Queue。
生成Serial Dispatch Queue
1 |
dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.gcd.SerialDispatchQueue", DISPATCH_QUEUE_SERIAL); |
生成Concurrent Dispatch Queue
1 |
dispatch_queue_t concurrentDispatchQueue = dispatch_queue_create("com.gcd.ConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT); |
第二種方法是直接獲取系統提供的Dispatch Queue。系統提供的也分為2類,Main Dispatch Queue 和 Global Dispatch Queue。Main Dispatch Queue 對應著是Serial Dispatch Queue,Global Dispatch Queue 對應著是Concurrent Dispatch Queue。
Global Dispatch Queue主要分為8種。
首先是以下4種,分別是優先順序對應Qos的情況。
1 2 3 4 |
- DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND |
其次是,是否支援 overcommit。加上上面4個優先順序,所以一共8種Global Dispatch Queue。帶有 overcommit 的佇列表示每當有任務提交時,系統都會新開一個執行緒處理,這樣就不會造成某個執行緒過載(overcommit)。
回到RACScheduler中來,RACScheduler既然是對GCD的封裝,那麼上述說的這些型別也都有其一一對應的封裝。
1 2 3 4 5 6 |
typedef enum : long { RACSchedulerPriorityHigh = DISPATCH_QUEUE_PRIORITY_HIGH, RACSchedulerPriorityDefault = DISPATCH_QUEUE_PRIORITY_DEFAULT, RACSchedulerPriorityLow = DISPATCH_QUEUE_PRIORITY_LOW, RACSchedulerPriorityBackground = DISPATCH_QUEUE_PRIORITY_BACKGROUND, } RACSchedulerPriority; |
首先是RACScheduler中的優先順序,這裡只封裝了4種,也是分別對應GCD中的DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,DISPATCH_QUEUE_PRIORITY_LOW,DISPATCH_QUEUE_PRIORITY_BACKGROUND。
RACScheduler有6個類方法,都是用來生成一個queue的。
1 2 3 4 5 6 7 8 |
+ (RACScheduler *)immediateScheduler; + (RACScheduler *)mainThreadScheduler; + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name; + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority; + (RACScheduler *)scheduler; + (RACScheduler *)currentScheduler; |
接下來依次分析一下它們的底層實現。
1. immediateScheduler
1 2 3 4 5 6 7 8 9 |
+ (instancetype)immediateScheduler { static dispatch_once_t onceToken; static RACScheduler *immediateScheduler; dispatch_once(&onceToken, ^{ immediateScheduler = [[RACImmediateScheduler alloc] init]; }); return immediateScheduler; } |
immediateScheduler底層實現就是生成了一個RACImmediateScheduler的單例。
RACImmediateScheduler 是繼承自RACScheduler。
1 2 |
@interface RACImmediateScheduler : RACScheduler @end |
在RACScheduler中,每個種類的RACScheduler都會有一個name屬性,名字也算是他們的標示。RACImmediateScheduler的name是@”com.ReactiveCocoa.RACScheduler.immediateScheduler”
RACImmediateScheduler的作用和它的名字一樣,是立即執行閉包裡面的任務。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); block(); return nil; } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != NULL); [NSThread sleepUntilDate:date]; block(); return nil; } |
在schedule:方法中,直接呼叫執行入參block( )閉包。在after: schedule:方法中,執行緒先睡眠,直到date的時刻,再醒過來執行入參block( )閉包。
1 2 3 4 |
- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCAssert(NO, @"+[RACScheduler immediateScheduler] does not support %@.", NSStringFromSelector(_cmd)); return nil; } |
當然RACImmediateScheduler是不可能支援after: repeatingEvery: withLeeway: schedule:方法的。因為它的定義就是立即執行的,不應該repeat。
1 2 3 4 5 6 7 8 9 |
- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock { for (__block NSUInteger remaining = 1; remaining > 0; remaining--) { recursiveBlock(^{ remaining++; }); } return nil; } |
RACImmediateScheduler的scheduleRecursiveBlock:方法中只要recursiveBlock閉包存在,就會無限遞迴呼叫執行,除非recursiveBlock不存在了。
2. mainThreadScheduler
mainThreadScheduler也是一個型別是RACTargetQueueScheduler的單例。
1 2 3 4 5 6 7 8 9 |
+ (instancetype)mainThreadScheduler { static dispatch_once_t onceToken; static RACScheduler *mainThreadScheduler; dispatch_once(&onceToken, ^{ mainThreadScheduler = [[RACTargetQueueScheduler alloc] initWithName:@"com.ReactiveCocoa.RACScheduler.mainThreadScheduler" targetQueue:dispatch_get_main_queue()]; }); return mainThreadScheduler; } |
mainThreadScheduler的名字是@”com.ReactiveCocoa.RACScheduler.mainThreadScheduler”。
RACTargetQueueScheduler繼承自RACQueueScheduler
1 2 3 |
@interface RACTargetQueueScheduler : RACQueueScheduler - (id)initWithName:(NSString *)name targetQueue:(dispatch_queue_t)targetQueue; @end |
在RACTargetQueueScheduler中,只有一個初始化方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (id)initWithName:(NSString *)name targetQueue:(dispatch_queue_t)targetQueue { NSCParameterAssert(targetQueue != NULL); if (name == nil) { name = [NSString stringWithFormat:@"com.ReactiveCocoa.RACTargetQueueScheduler(%s)", dispatch_queue_get_label(targetQueue)]; } dispatch_queue_t queue = dispatch_queue_create(name.UTF8String, DISPATCH_QUEUE_SERIAL); if (queue == NULL) return nil; dispatch_set_target_queue(queue, targetQueue); return [super initWithName:name queue:queue]; } |
先新建了一個queue,name是@”com.ReactiveCocoa.RACScheduler.mainThreadScheduler”,型別是Serial Dispatch Queue 型別的,然後呼叫了dispatch_set_target_queue方法。
所以重點就在dispatch_set_target_queue方法裡面了。
dispatch_set_target_queue方法主要有兩個目的:一是設定dispatch_queue_create建立佇列的優先順序,二是建立佇列的執行階層。
- 當使用dispatch_queue_create建立佇列的時候,不管是序列還是並行,它們的優先順序都是DISPATCH_QUEUE_PRIORITY_DEFAULT級別,而這個API就是可以設定佇列的優先順序。
舉個例子:
1 2 3 4 |
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); //注意:被設定優先順序的佇列是第一個引數。 dispatch_set_target_queue(serialQueue, globalQueue); |
通過上面的程式碼,就把將serailQueue設定成DISPATCH_QUEUE_PRIORITY_HIGH。
- 使用這個dispatch_set_target_queue方法可以設定佇列執行階層,例如dispatch_set_target_queue(queue, targetQueue);
這樣設定時,相當於將queue指派給targetQueue,如果targetQueue是序列佇列,則queue是序列執行的;如果targetQueue是並行佇列,那麼queue是並行的。
舉個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
dispatch_queue_t targetQueue = dispatch_queue_create("targetQueue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT); dispatch_set_target_queue(queue1, targetQueue); dispatch_set_target_queue(queue2, targetQueue); dispatch_async(queue1, ^{ NSLog(@"queue1 1"); }); dispatch_async(queue1, ^{ NSLog(@"queue1 2"); }); dispatch_async(queue2, ^{ NSLog(@"queue2 1"); }); dispatch_async(queue2, ^{ NSLog(@"queue2 2"); }); dispatch_async(targetQueue, ^{ NSLog(@"target queue"); }); |
如果targetQueue為Serial Dispatch Queue,那麼輸出結果必定如下:
1 2 3 4 5 |
queue1 1 queue1 2 queue2 1 queue2 2 target queue |
如果targetQueue為Concurrent Dispatch Queue,那麼輸出結果可能如下:
1 2 3 4 5 |
queue1 1 queue2 1 queue1 2 target queue queue2 2 |
回到RACTargetQueueScheduler中來,在這裡傳進來的入參是dispatch_get_main_queue( ),這是一個Serial Dispatch Queue,這裡再呼叫dispatch_set_target_queue方法,相當於把queue的優先順序設定的和main_queue一致。
3. scheduler
以下三個方法實質是同一個方法。
1 2 3 |
+ (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name; + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority; + (RACScheduler *)scheduler; |
1 2 3 4 5 6 7 8 9 10 11 |
+ (instancetype)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name { return [[RACTargetQueueScheduler alloc] initWithName:name targetQueue:dispatch_get_global_queue(priority, 0)]; } + (instancetype)schedulerWithPriority:(RACSchedulerPriority)priority { return [self schedulerWithPriority:priority name:@"com.ReactiveCocoa.RACScheduler.backgroundScheduler"]; } + (instancetype)scheduler { return [self schedulerWithPriority:RACSchedulerPriorityDefault]; } |
通過原始碼我們能知道,scheduler這一系列的三個方法,是建立了一個 Global Dispatch Queue,對應的屬於Concurrent Dispatch Queue。
schedulerWithPriority: name:方法可以指定執行緒的優先順序和名字。
schedulerWithPriority:方法只能執行優先順序,名字為預設的@”com.ReactiveCocoa.RACScheduler.backgroundScheduler”。
scheduler方法建立出來的queue的優先順序是預設的,名字也是預設的@”com.ReactiveCocoa.RACScheduler.backgroundScheduler”。
注意,scheduler和mainThreadScheduler,immediateScheduler這兩個單例不同的是,scheduler每次都會建立一個新的Concurrent Dispatch Queue。
4. currentScheduler
1 2 3 4 5 6 |
+ (instancetype)currentScheduler { RACScheduler *scheduler = NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey]; if (scheduler != nil) return scheduler; if ([self.class isOnMainThread]) return RACScheduler.mainThreadScheduler; return nil; } |
首先,在ReactiveCocoa 中定義了這麼一個key,@”RACSchedulerCurrentSchedulerKey”,這個用來從執行緒字典裡面存取出對應的RACScheduler。
1 |
NSString * const RACSchedulerCurrentSchedulerKey = @"RACSchedulerCurrentSchedulerKey"; |
在currentScheduler這個方法裡面看到的是從執行緒字典裡面取出一個RACScheduler。至於什麼時候存的,下面會解釋到。
如果能從執行緒字典裡面取出一個RACScheduler,就返回取出的RACScheduler。如果字典裡面沒有,再判斷當前的scheduler是否是在主執行緒上。
1 2 3 |
+ (BOOL)isOnMainThread { return [NSOperationQueue.currentQueue isEqual:NSOperationQueue.mainQueue] || [NSThread isMainThread]; } |
判斷方法如上,只要是NSOperationQueue在mainQueue上,或者NSThread是主執行緒,都算是在主執行緒上。
如果是在主執行緒上,就返回mainThreadScheduler。
如果既不在主執行緒上,執行緒字典裡面也找不到對應key值對應的value,那麼就返回nil。
RACScheduler除了有6個類方法,還有4個例項方法:
1 2 3 4 |
- (RACDisposable *)schedule:(void (^)(void))block; - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block; - (RACDisposable *)afterDelay:(NSTimeInterval)delay schedule:(void (^)(void))block; - (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block; |
這4個方法其實從名字上就知道是用來幹嘛的。
schedule:是為RACScheduler新增一個任務,入參是一個閉包。
after: schedule:是為RACScheduler新增一個定時任務,在date時間之後才執行任務。
afterDelay: schedule:是為RACScheduler新增一個延時執行的任務,延時delay時間之後才執行任務。
after: repeatingEvery: withLeeway: schedule:是為RACScheduler新增一個定時任務,在date時間之後才開始執行,然後每隔interval秒執行一次任務。
這四個方法會分別在RACScheduler的各個子類裡面進行重寫。
比如之前提到的immediateScheduler,schedule:方法中會直接立即執行閉包。after: schedule:方法中新增一個定時任務,在date時間之後才執行任務。after: repeatingEvery: withLeeway: schedule:這個方法在RACImmediateScheduler中就直接返回nil。
還有其他子類在下面會分析這4個方法的實現。
另外還有最後3個方法
1 2 3 |
- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock; - (void)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock addingToDisposable:(RACCompoundDisposable *)disposable - (void)performAsCurrentScheduler:(void (^)(void))block; |
前兩個方法是實現RACSequence中signalWithScheduler:方法的,具體分析見這篇文章
performAsCurrentScheduler:方法是在RACQueueScheduler中使用到了,在下面子類分析裡面詳細分析。
二. RACSequence的一些子類
RACSequence總共有以下5個子類。
1. RACTestScheduler
這個類主要是一個測試類,主要用在單元測試中,它是用來驗證非同步呼叫沒有花費大量的時間等待。RACTestScheduler也可以用在多執行緒當中,當時一次只能在排隊的方法佇列中選擇一個方法執行。
1 2 3 4 5 6 7 |
@interface RACTestSchedulerAction : NSObject @property (nonatomic, copy, readonly) NSDate *date; @property (nonatomic, copy, readonly) void (^block)(void); @property (nonatomic, strong, readonly) RACDisposable *disposable; - (id)initWithDate:(NSDate *)date block:(void (^)(void))block; @end |
在單元測試中,ReactiveCocoa為了方便比較每個方法的呼叫,新建了一個RACTestSchedulerAction物件,用來更加方便的比較和描述測試的全過程。RACTestSchedulerAction的定義如上。現在再來解釋一下引數。
date是一個時間,時間主要是用來比較和決定下一次該輪到哪個閉包要開始執行了。
void (^block)(void)閉包是RACScheduler中的一個任務。
disposable是控制一個action是否可以執行的。一旦disposed了,那麼這個action就不會被執行。
initWithDate: block: 方法是初始化一個新的action。
在單元測試過程中,需要呼叫step方法來進行檢視每次呼叫閉包的情況。
1 2 3 4 5 6 7 |
- (void)step { [self step:1]; } - (void)stepAll { [self step:NSUIntegerMax]; } |
step和stepAll方法都是呼叫step:方法。step只是執行一次RACScheduler中的任務,stepAll是執行所有的RACScheduler中的任務。既然都是呼叫step:,那接下來分析一下step:的具體實現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
- (void)step:(NSUInteger)ticks { @synchronized (self) { for (NSUInteger i = 0; i < ticks; i++) { const void *actionPtr = NULL; if (!CFBinaryHeapGetMinimumIfPresent(self.scheduledActions, &actionPtr)) break; RACTestSchedulerAction *action = (__bridge id)actionPtr; CFBinaryHeapRemoveMinimumValue(self.scheduledActions); if (action.disposable.disposed) continue; RACScheduler *previousScheduler = RACScheduler.currentScheduler; NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self; action.block(); if (previousScheduler != nil) { NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler; } else { [NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey]; } } } } |
step:的實現主要就是一個for迴圈。迴圈的次數就是入參ticks決定的。首先const void *actionPtr是一個指向函式的指標。在上述實現中有一個很重要的函式——CFBinaryHeapGetMinimumIfPresent。該函式的原型如下:
1 |
Boolean CFBinaryHeapGetMinimumIfPresent(CFBinaryHeapRef heap, const void **value) |
這個函式的主要作用的是在二分堆heap中查詢一個最小值。
1 2 3 4 5 |
static CFComparisonResult RACCompareScheduledActions(const void *ptr1, const void *ptr2, void *info) { RACTestSchedulerAction *action1 = (__bridge id)ptr1; RACTestSchedulerAction *action2 = (__bridge id)ptr2; return CFDateCompare((__bridge CFDateRef)action1.date, (__bridge CFDateRef)action2.date, NULL); } |
比較規則如上,就是比較兩者的date的值。從二分堆中找出這樣一個最小值,對應的就是scheduler中的任務。如果最小值有幾個相等最小值,就隨機返回一個最小值。返回的函式放在actionPtr中。整個函式的返回值是一個BOOL值,如果二分堆不為空,能找到最小值就返回YES,如果二分堆為空,就找不到最小值了,就返回NO。
stepAll方法裡面傳入了NSUIntegerMax,這個for迴圈也不會死迴圈,因為到堆中所有的任務都執行完成之後,CFBinaryHeapGetMinimumIfPresent返回NO,就會執行break,跳出迴圈。
這裡會把currentScheduler儲存到執行緒字典裡面。接著會執行action.block,執行任務。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != nil); @synchronized (self) { NSDate *uniqueDate = [NSDate dateWithTimeIntervalSinceReferenceDate:self.numberOfDirectlyScheduledBlocks]; self.numberOfDirectlyScheduledBlocks++; RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:uniqueDate block:block]; CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action); return action.disposable; } } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != nil); @synchronized (self) { RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:date block:block]; CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action); return action.disposable; } } |
schedule:方法裡面會累加numberOfDirectlyScheduledBlocks值,這個值也會初始化成時間,以便比較各個方法該排程的時間。numberOfDirectlyScheduledBlocks最終會代表總共有多少個block任務產生了。然後用CFBinaryHeapAddValue加入到堆中。
after:schedule:就是直接新建RACTestSchedulerAction物件,然後再用CFBinaryHeapAddValue把block閉包加入到堆中。
after: repeatingEvery: withLeeway: schedule:同樣也是新建RACTestSchedulerAction物件,然後再用CFBinaryHeapAddValue把block閉包加入到堆中。
2. RACSubscriptionScheduler
RACSubscriptionScheduler是RACScheduler最後一個單例。RACScheduler中唯一的三個單例現在就齊全了:RACImmediateScheduler,RACTargetQueueScheduler ,RACSubscriptionScheduler。
1 2 3 4 5 6 7 8 9 |
+ (instancetype)subscriptionScheduler { static dispatch_once_t onceToken; static RACScheduler *subscriptionScheduler; dispatch_once(&onceToken, ^{ subscriptionScheduler = [[RACSubscriptionScheduler alloc] init]; }); return subscriptionScheduler; } |
RACSubscriptionScheduler 的名字是@”com.ReactiveCocoa.RACScheduler.subscriptionScheduler”
1 2 3 4 5 6 |
- (id)init { self = [super initWithName:@"com.ReactiveCocoa.RACScheduler.subscriptionScheduler"]; if (self == nil) return nil; _backgroundScheduler = [RACScheduler scheduler]; return self; } |
RACSubscriptionScheduler初始化的時候會新建一個Global Dispatch Queue。
1 2 3 4 5 6 |
- (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block]; block(); return nil; } |
如果RACScheduler.currentScheduler為nil就用backgroundScheduler去呼叫block閉包,否則就執行block閉包。
1 2 3 4 |
- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler; return [scheduler after:date schedule:block]; } |
1 2 3 4 |
- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler; return [scheduler after:date repeatingEvery:interval withLeeway:leeway schedule:block]; } |
兩個after方法都有取出RACScheduler.currentScheduler,如果為空就用self.backgroundScheduler去呼叫各自的after的方法。
RACSubscriptionScheduler中的backgroundScheduler的意義就在此,當RACScheduler.currentScheduler不存在的時候就會替換成self.backgroundScheduler。
3. RACImmediateScheduler
這個子類在分析immediateScheduler方法的時候,詳細分析過了,這裡不再贅述。
4. RACQueueScheduler
1 2 3 4 5 6 7 8 9 10 11 12 |
- (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); RACDisposable *disposable = [[RACDisposable alloc] init]; dispatch_async(self.queue, ^{ if (disposable.disposed) return; [self performAsCurrentScheduler:block]; }); return disposable; } |
schedule:會呼叫performAsCurrentScheduler:方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)performAsCurrentScheduler:(void (^)(void))block { NSCParameterAssert(block != NULL); RACScheduler *previousScheduler = RACScheduler.currentScheduler; NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self; @autoreleasepool { block(); } if (previousScheduler != nil) { NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler; } else { [NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey]; } } |
performAsCurrentScheduler:方法會先在呼叫block( )之前,把當前的scheduler存入執行緒字典中。
試想,如果現在在一個Concurrent Dispatch Queue中,在執行block( )之前需要先切換執行緒,切換到當前scheduler中。當執行完block閉包之後,previousScheduler如果不為nil,那麼就還原現場,執行緒字典裡面再存回原來的scheduler,反之previousScheduler為nil,那麼就移除掉執行緒字典裡面的key。
這裡需要值得注意的是:
scheduler本質其實是一個quene,並不是一個執行緒。它只能保證裡面的執行緒都是序列執行的,但是它不能保證每個執行緒不一定都是在同一個執行緒裡面執行。
如上面這段performAsCurrentScheduler:的實現所表現的那樣。所以
在scheduler使用Core Data很容易崩潰,很可能跑到子執行緒上面去了。一旦寫資料的時候到了子執行緒上,很容易就Crash了。一定要記得回到main queue上。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != NULL); RACDisposable *disposable = [[RACDisposable alloc] init]; dispatch_after([self.class wallTimeWithDate:date], self.queue, ^{ if (disposable.disposed) return; [self performAsCurrentScheduler:block]; }); return disposable; } |
在after中呼叫dispatch_after方法,經過date時間之後再呼叫performAsCurrentScheduler:。
wallTimeWithDate:的實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
+ (dispatch_time_t)wallTimeWithDate:(NSDate *)date { NSCParameterAssert(date != nil); double seconds = 0; double frac = modf(date.timeIntervalSince1970, &seconds); struct timespec walltime = { .tv_sec = (time_t)fmin(fmax(seconds, LONG_MIN), LONG_MAX), .tv_nsec = (long)fmin(fmax(frac * NSEC_PER_SEC, LONG_MIN), LONG_MAX) }; return dispatch_walltime(&walltime, 0); } |
dispatch_walltime函式是由POSIX中使用的struct timespec型別的時間得到dispatch_time_t型別的值。dispatch_time函式通常用於計算相對時間,而dispatch_walltime函式用於計算絕對時間。
這段程式碼其實很簡單,就是把date的時間轉換成一個dispatch_time_t型別的。由NSDate類物件獲取能傳遞給dispatch_after函式的dispatch_time_t型別的值。
1 2 3 |
- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(interval > 0.0 && interval = 0.0 && leeway |
after: repeatingEvery: withLeeway: schedule:方法裡面的實現就是用GCD在self.queue上建立了一個Timer,時間間隔是interval,修正時間是leeway。
leeway這個引數是為dispatch source指定一個期望的定時器事件精度,讓系統能夠靈活地管理並喚醒核心。例如系統可以使用leeway值來提前或延遲觸發定時器,使其更好地與其它系統事件結合。建立自己的定時器時,應該儘量指定一個leeway值。不過就算指定leeway值為0,也不能完完全全期望定時器能夠按照精確的納秒來觸發事件。
這個定時器在interval執行入參閉包。在取消任務的時候呼叫dispatch_source_cancel取消定時器timer。
5. RACTargetQueueScheduler
這個子類在分析mainThreadScheduler方法的時候,詳細分析過了,這裡不再贅述。
三. RACScheduler是如何“取消”併發任務的
既然RACScheduler是對GCD的封裝,那麼在GCD的上層可以實現一些GCD所無法完成的“特性”。這裡的“特性”是打引號的,因為底層是GCD,上層的特性只能通過一些特殊手段來實現看似是新的特性。在這一點上,RACScheduler就實現了GCD沒有的特性——“取消”任務。
Operation Queues :
相對 GCD來說,使用 Operation Queues 會增加一點點額外的開銷,但是卻換來了非常強大的靈活性和功能,它可以給 operation 之間新增依賴關係、取消一個正在執行的 operation 、暫停和恢復 operation queue 等;
GCD:
是一種更輕量級的,以 FIFO的順序執行併發任務的方式,使用 GCD時我們可以並不關心任務的排程情況,而讓系統幫我們自動處理。但是 GCD的缺陷也是非常明顯的,想要給任務之間新增依賴關係、取消或者暫停一個正在執行的任務時就會變得非常棘手。
既然GCD不方便取消一個任務,那麼RACScheduler是怎麼做到的呢?
這就體現在RACQueueScheduler上。回頭看看RACQueueScheduler的schedule:實現 和 after: schedule:實現。
最核心的程式碼:
1 2 3 4 |
dispatch_async(self.queue, ^{ if (disposable.disposed) return; [self performAsCurrentScheduler:block]; }); |
在呼叫performAsCurrentScheduler:之前,加了一個判斷,判斷當前是否取消了任務,如果取消了任務,就return,不會呼叫block閉包。這樣就實現了取消任務的“假象”。
四. RACScheduler是如何和RAC其他元件進行完美整合的
在整個ReactiveCocoa中,利用RACScheduler實現了很多操作,和RAC是深度整合的。這裡就來總結總結ReactiveCocoa中總共有哪些地方用到了RACScheduler。
在ReactiveCocoa 中全域性搜尋RACScheduler,遍歷完所有庫,RACScheduler就用在以下10個類中。下面就來看看是如何用在這些地方的。
從下面這些地方使用了Scheduler中,我們就可以瞭解到哪些操作是在子執行緒,哪些是在主執行緒。區分出了這些,對於執行緒不安全的操作,我們就能心有成足的處理好它們,讓它們回到主執行緒中去操作,這樣就可以減少很多莫名的Crash。這些Crash都是因為執行緒問題導致的。
1. 在RACCommand中
1 |
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock |
這個方法十分複雜,裡面用到了RACScheduler.immediateScheduler,deliverOn:RACScheduler.mainThreadScheduler。具體的原始碼分析會在下一篇RACCommand原始碼分析裡面詳細分析。
1 |
- (RACSignal *)execute:(id)input |
在這個方法中,會呼叫subscribeOn:RACScheduler.mainThreadScheduler。
2. 在RACDynamicSignal中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (RACDisposable *)subscribe:(id)subscriber { NSCParameterAssert(subscriber != nil); RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable]; if (self.didSubscribe != NULL) { RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ RACDisposable *innerDisposable = self.didSubscribe(subscriber); [disposable addDisposable:innerDisposable]; }]; [disposable addDisposable:schedulingDisposable]; } return disposable; } |
在RACDynamicSignal的subscribe:訂閱過程中會用到subscriptionScheduler。於是對這個scheduler呼叫schedule:就會執行下面這段程式碼:
1 2 3 4 5 6 7 8 |
- (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block]; block(); return nil; } |
如果currentScheduler不為空,閉包會在currentScheduler中執行,如果currentScheduler為空,閉包就會在backgroundScheduler中執行,這是一個Global Dispatch Queue,優先順序是RACSchedulerPriorityDefault。
同理,在RACEmptySignal,RACErrorSignal,RACReturnSignal,RACSignal的相關的signal的訂閱中也都會呼叫subscriptionScheduler。
3. 在RACBehaviorSubject中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (RACDisposable *)subscribe:(id)subscriber { RACDisposable *subscriptionDisposable = [super subscribe:subscriber]; RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ @synchronized (self) { [subscriber sendNext:self.currentValue]; } }]; return [RACDisposable disposableWithBlock:^{ [subscriptionDisposable dispose]; [schedulingDisposable dispose]; }]; } |
在RACBehaviorSubject的subscribe:訂閱過程中會用到subscriptionScheduler。於是對這個scheduler呼叫schedule:,程式碼在上面分析過了。
同理,如果currentScheduler不為空,閉包會在currentScheduler中執行,如果currentScheduler為空,閉包就會在backgroundScheduler中執行,這是一個Global Dispatch Queue,優先順序是RACSchedulerPriorityDefault。
4. 在RACReplaySubject中
它的訂閱也同上面訊號的訂閱一樣,會呼叫subscriptionScheduler。
由於RACReplaySubject是在子執行緒上,所以建議在使用Core Data這些不安全庫的時候一定要記得加上deliverOn。
5. 在RACSequence中
在RACSequence中,以下兩個方法用到了RACScheduler:
1 2 3 |
- (RACSignal *)signal { return [[self signalWithScheduler:[RACScheduler scheduler]] setNameWithFormat:@"[%@] -signal", self.name]; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (RACSignal *)signalWithScheduler:(RACScheduler *)scheduler { return [[RACSignal createSignal:^(id subscriber) { __block RACSequence *sequence = self; return [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) { if (sequence.head == nil) { [subscriber sendCompleted]; return; } [subscriber sendNext:sequence.head]; sequence = sequence.tail; reschedule(); }]; }] setNameWithFormat:@"[%@] -signalWithScheduler: %@", self.name, scheduler]; } |
上面兩個方法會呼叫RACScheduler中的scheduleRecursiveBlock:方法。關於這個方法的原始碼分析可以看RACSequence的原始碼分析。
6. 在RACSignal+Operations中
這裡總共有9個方法用到了Scheduler。
第一個方法:
1 |
static RACDisposable *subscribeForever (RACSignal *signal, void (^next)(id), void (^error)(NSError *, RACDisposable *), void (^completed)(RACDisposable *)) |
在上面這個方法裡面用到了
1 |
RACScheduler *recursiveScheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler]; |
取出currentScheduler或者一個Global Dispatch Queue,然後呼叫scheduleRecursiveBlock:。
第二個方法:
1 |
- (RACSignal *)throttle:(NSTimeInterval)interval valuesPassingTest:(BOOL (^)(id next))predicate |
在上面這個方法中會呼叫
1 2 |
RACScheduler *scheduler = [RACScheduler scheduler]; RACScheduler *delayScheduler = RACScheduler.currentScheduler ?: scheduler |
在delayScheduler中呼叫afterDelay: schedule:方法,這也是throttle:valuesPassingTest:方法實現的很重要的一步。
第三個方法:
1 |
- (RACSignal *)delay:(NSTimeInterval)interval |
由於這是一個延遲方法,肯定是會呼叫Scheduler的after方法。
1 2 |
RACScheduler *delayScheduler = RACScheduler.currentScheduler ?: scheduler; RACDisposable *schedulerDisposable = [delayScheduler afterDelay:interval schedule:block]; |
RACScheduler.currentScheduler ?: scheduler 這個判斷在上述幾個時間相關的方法都用到了。
所以,這裡給一個建議:
delay由於不一定會回到當前執行緒中,所以delay之後再去訂閱可能就在子執行緒中去執行。所以使用delay的時候最好追加一個deliverOn。
第四個方法:
1 |
- (RACSignal *)bufferWithTime:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler |
在這個方法中理所當然的需要呼叫[scheduler afterDelay:interval schedule:flushValues]這個方法,來達到延遲的目的,從而實現緩衝buffer的效果。
第五個方法:
1 |
+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler |
第六個方法:
1 |
+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway { } |
第五個方法 和 第六個方法都用傳進去的入參scheduler去呼叫after: repeatingEvery: withLeeway: schedule:方法。
第七個方法:
1 |
- (RACSignal *)timeout:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler { } |
在這個方法中會用入參scheduler呼叫afterDelay: schedule:,延遲一段時候後,執行[disposable dispose],從而也實現了超時傳送sendError:。
第八個方法:
1 |
- (RACSignal *)deliverOn:(RACScheduler *)scheduler { } |
第九個方法:
1 |
- (RACSignal *)subscribeOn:(RACScheduler *)scheduler { } |
第八個方法 和 第九個方法都是根據入參scheduler去呼叫schedule:方法。入參是什麼型別的scheduler決定了schedule:執行在哪個queue上。
7. 在RACSignal中
在RACSignal也有積極計算和惰性求值的訊號。
1 2 3 4 5 6 7 8 |
+ (RACSignal *)startEagerlyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id subscriber))block { NSCParameterAssert(scheduler != nil); NSCParameterAssert(block != NULL); RACSignal *signal = [self startLazilyWithScheduler:scheduler block:block]; [[signal publish] connect]; return [signal setNameWithFormat:@"+startEagerlyWithScheduler: %@ block:", scheduler]; } |
startEagerlyWithScheduler中會呼叫startLazilyWithScheduler產生一個訊號signal,然後緊接著轉換成熱訊號。通過startEagerlyWithScheduler產生的訊號就直接是一個熱訊號。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
+ (RACSignal *)startLazilyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id subscriber))block { NSCParameterAssert(scheduler != nil); NSCParameterAssert(block != NULL); RACMulticastConnection *connection = [[RACSignal createSignal:^ id (id subscriber) { block(subscriber); return nil; }] multicast:[RACReplaySubject subject]]; return [[[RACSignal createSignal:^ id (id subscriber) { [connection.signal subscribe:subscriber]; [connection connect]; return nil; }] subscribeOn:scheduler] setNameWithFormat:@"+startLazilyWithScheduler: %@ block:", scheduler]; } |
上述是startLazilyWithScheduler:的原始碼實現,在這個方法中和startEagerlyWithScheduler最大的區別就出來了,connect方法在return的訊號中,所以Lazily就體現在,通過startLazilyWithScheduler建立出來的訊號,只有訂閱它之後才能呼叫到connect,轉變成熱訊號。
在這裡呼叫了subscribeOn:scheduler,這裡用到了scheduler。
8. 在NSData+RACSupport中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (RACSignal *)rac_readContentsOfURL:(NSURL *)URL options:(NSDataReadingOptions)options scheduler:(RACScheduler *)scheduler { NSCParameterAssert(scheduler != nil); RACReplaySubject *subject = [RACReplaySubject subject]; [subject setNameWithFormat:@"+rac_readContentsOfURL: %@ options: %lu scheduler: %@", URL, (unsigned long)options, scheduler]; [scheduler schedule:^{ NSError *error = nil; NSData *data = [[NSData alloc] initWithContentsOfURL:URL options:options error:&error]; if (data == nil) { [subject sendError:error]; } else { [subject sendNext:data]; [subject sendCompleted]; } }]; return subject; } |
在這個方法中,會傳入RACQueueScheduler或者RACTargetQueueScheduler的RACScheduler。那麼呼叫schedule方法就會執行到這裡:
1 2 3 4 5 6 7 8 9 10 11 12 |
- (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); RACDisposable *disposable = [[RACDisposable alloc] init]; dispatch_async(self.queue, ^{ if (disposable.disposed) return; [self performAsCurrentScheduler:block]; }); return disposable; } |
9. 在NSString+RACSupport中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (RACSignal *)rac_readContentsOfURL:(NSURL *)URL usedEncoding:(NSStringEncoding *)encoding scheduler:(RACScheduler *)scheduler { NSCParameterAssert(scheduler != nil); RACReplaySubject *subject = [RACReplaySubject subject]; [subject setNameWithFormat:@"+rac_readContentsOfURL: %@ usedEncoding:scheduler: %@", URL, scheduler]; [scheduler schedule:^{ NSError *error = nil; NSString *string = [NSString stringWithContentsOfURL:URL usedEncoding:encoding error:&error]; if (string == nil) { [subject sendError:error]; } else { [subject sendNext:string]; [subject sendCompleted]; } }]; return subject; } |
同NSData+RACSupport中的rac_readContentsOfURL: options: scheduler:一樣,也會傳入RACQueueScheduler或者RACTargetQueueScheduler的RACScheduler。
10. 在NSUserDefaults+RACSupport中
1 |
RACScheduler *scheduler = [RACScheduler scheduler]; |
在這個方法中也會新建RACTargetQueueScheduler,一個Global Dispatch Queue。優先順序是RACSchedulerPriorityDefault。
最後
關於RACScheduler底層實現分析都已經分析完成。最後請大家多多指教。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式