當系統(tǒng)的輸入源不足以滿足我們的需求的時候, 我們可以自定義輸入源. 看了蘋果的官方文檔, 也沒有知道為什么, 所以妄自猜測下, 可能是當系統(tǒng)的輸入源不足以滿足我們的需求的時候, 我們需要自定義輸入源.
定義輸入源
網(wǎng)上大部分配置sources的demo, 核心代碼都出自這里, 下面簡單的對自定義source的類圖進行分析.

核心類是CCRunLoopInputSource也就是自定義的輸入源. CCRunLoopCustomInputSourceThread是自定義輸入源生存的環(huán)境.
創(chuàng)建自定義輸入源需要定義以下內(nèi)容
- 1 輸入源要處理的信息.
- 2 輸入源被添加到Runloop時的調(diào)度例程.
- 3 輸入源被告知有事件要處理的調(diào)度例程.
- 4 輸入源被取消時的調(diào)度例程.
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
// CCRunLoopInputSource.m
- (instancetype)init
{
self = [super init];
if (self) {
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
&runLoopSourceScheduleRoutine,
&runLoopSourceCancelRoutine,
&runLoopSourcePerformRoutine};
_runLoopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
_commands = [NSMutableArray array];
}
return self;
}
在CFRunLoopSourceCreate創(chuàng)建輸入源的時候, 需要傳入CFRunLoopSourceContext結(jié)構(gòu)體指針, 這里第二個參數(shù)的info傳的是self, 當系統(tǒng)回調(diào)時候會把這個信息當成上下文回調(diào), 此外還定義了schedule, perform, cancel三個函數(shù)指針, 分別對應(yīng)事件源被加到Runloop, 輸入源被告知有事件要處理, 和輸入源失效的函數(shù)回調(diào).
當調(diào)用以下方法
- (void)addToCurrentRunLoop
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
}
會導致runLoopSourceScheduleRoutine函數(shù)回調(diào),
void runLoopSourceScheduleRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
{
CCRunLoopInputSource *runLoopInputSource = (__bridge CCRunLoopInputSource *)info;
CCAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
CCRunLoopContext *runLoopContext = [[CCRunLoopContext alloc] initWithSource:runLoopInputSource runLoop:runLoopRef];
[appDelegate performSelectorOnMainThread:@selector(registerSource:) withObject:runLoopContext waitUntilDone:NO];
}
這里只是把回調(diào)的事件源封裝成CCRunLoopContext再拋出去, 這里是拋給CCAppDelegate, 實際上拋給哪個類處理都是可以的.
當執(zhí)行
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runLoop
{
NSLog(@"Current Thread: %@", [NSThread currentThread]);
CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(runLoop);
}
實際上是把當前事件源標記為有事件要處理, 然后調(diào)用CFRunLoopWakeUp喚起線程, 類似我們的
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
將當前畫布標記為dirty, 然后觸發(fā)重繪.
線程被wakeup后, 會執(zhí)行上一篇博文里面步驟9, 處理source1和timer, 這里當然就是source1了,
void runLoopSourcePerformRoutine (void *info)
{
CCRunLoopInputSource *runLoopInputSource = (__bridge CCRunLoopInputSource *)info;
[runLoopInputSource inputSourceFired];
}
實際是執(zhí)行了runLoopSourcePerformRoutine回調(diào), 這里我們看到只是將回調(diào)傳遞來的事件源info取出來, 并執(zhí)行inputSourceFired
- (void)inputSourceFired
{
NSLog(@"Enter inputSourceFired");
// Test
if (_testPrintString) {
if ([self.delegate respondsToSelector:@selector(activeInputSourceForTestPrintStringEvent:)]) {
[self.delegate activeInputSourceForTestPrintStringEvent:_testPrintString];
}
}
NSLog(@"Exit inputSourceFired");
}
這里調(diào)用了代理方法activeInputSourceForTestPrintStringEvent, 將, 最終CCRunLoopInputSource的代理CCRunLoopCustomInputSourceThread將處理這個事件.
- (void)activeInputSourceForTestPrintStringEvent:(NSString *)string
{
NSLog(@"activeInputSourceForTestPrintStringEvent : %@", string);
}
當然這里的代理不一定要是CCRunLoopCustomInputSourceThread, 也可以是其它的類.
當調(diào)用
- (void)invalidate
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopRemoveSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
}
使一個事件源失效的時候, 會觸發(fā)runLoopSourceCancelRoutine回調(diào)
void runLoopSourceCancelRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
{
CCRunLoopInputSource *runLoopInputSource = (__bridge CCRunLoopInputSource *)info;
CCAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
CCRunLoopContext *runLoopContext = [[CCRunLoopContext alloc] initWithSource:runLoopInputSource runLoop:runLoopRef];
[appDelegate performSelectorOnMainThread:@selector(removeSource:) withObject:runLoopContext waitUntilDone:YES];
}
下面看下AppDelegate的代碼
@implementation CCAppDelegate (RunLoop)
- (void)registerSource:(CCRunLoopContext *)sourceContext
{
if (!self.sources) {
self.sources = [NSMutableArray array];
}
[self.sources addObject:sourceContext];
}
- (void)removeSource:(CCRunLoopContext *)sourceContext
{
[self.sources enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CCRunLoopContext *context = obj;
if ([context isEqual:sourceContext]) {
[self.sources removeObject:context];
*stop = YES;
}
}];
}
- (void)testInputSourceEvent
{
CCRunLoopContext *runLoopContext = [self.sources objectAtIndex:0];
CCRunLoopInputSource *inputSource = runLoopContext.runLoopInputSource;
[inputSource addTestPrintCommandWithString:[[NSDate date] description]];
[inputSource fireAllCommandsOnRunLoop:runLoopContext.runLoop];
}
@end
這里維護了一個輸入源的數(shù)組, 用來區(qū)分不同的輸入源. 對于不同的輸入源我們可以在testInputSourceEvent選擇不同的輸入源進行觸發(fā).
[inputSource addTestPrintCommandWithString:[[NSDate date] description]];
上面的方法, 我理解是進行數(shù)據(jù)的傳遞, 作者也設(shè)計了更加通用的接口
- (void)addCommand:(NSInteger)command data:(NSData *)data;
不過demo里面沒有使用到.
這里實際上是建立了一個通道和線程狀態(tài)切換的機制

臆想:
這個通道是線程之間傳遞數(shù)據(jù), 喚醒休眠線程的一套機制, 我們完全可以自定義一個輸入源, 用來處理服務(wù)器發(fā)來的數(shù)據(jù).
步驟如下:
- 1 初始化線程的時候創(chuàng)建輸入source, 并添加.
- 2 當有數(shù)據(jù)過來的時候調(diào)用
CFRunLoopSourceSignal和CFRunLoopWakeUp, 并將當前數(shù)據(jù)的回調(diào)模塊信息保存到CFRunLoopSourceSignal. - 3 在子線程里進行數(shù)據(jù)解析等一些耗時操作.
- 4 當數(shù)據(jù)解析完, 將數(shù)據(jù)封裝成
CCRunLoopContext, 找到回調(diào)模塊, 在主線程進行回調(diào)(performSelectorOnMainThread).
相對比dispatch_async這樣做的好處是, 在觸發(fā)事件前, 可以保存一些數(shù)據(jù)到自定義source, 這樣在回調(diào)的時候可以很方便的找到指定的方法進行回調(diào), 有點類似運行時的效果.