Cocoa RunLoop 系列之Configure Custom InputSource

博客地址

在上一篇博客Cocoa RunLoop 系列之基础知识介绍了RunLoop的InpuSource有两种:一种是基于Mach端口且由内核触发的source1,另外一种就是自定义且需要手动触发的source0。

其中source0包括两种自定义形式:一种是Apple实现的自定义InputSource,提供了一系列接口,直接调用即可;另外一种就是由用户根据开发需要完全自定义实现。本文要介绍的就是后者。

自定义InputSource在实际开发过程的中,可用于在子线程实现周期性且长时间的任务,通过自定义InputSource控制任务的执行。

然而,实际开发中,大部分需要处理的InputSource都属于source1,少数需要自定义InputSource的情况也可以借助Apple的自定义InputSource函数接口来满足需求。因此,实际开发中几乎不需要用户配置自定义InputSource。既然如此,是否还有探索配置自定义InputSource的必要?我个人的答案是肯定的。通过配置自定InputSource可以窥探RunLoop的整个Routine的具体流程,而不是只停留在理论层面,有助于更深刻地理解RunLoop运行机制。

下面进入正文,结合理论和源代码阐述配置自定义InputSource的全过程。

理论概述

下图是Apple开发文档中介绍自定义InputSource运行流程图:

[图片上传失败...(image-fa0cd2-1514272652279)]

结合上图,总结一下几点:

  1. 在工作线程创建一个自定义InputSource并部署到RunLoop中
  2. 主线程中对线程的InputSource和RunLoop进行引用,用于后续操作
  3. 主线程与工作线程共享一个指令集合,以保证指令同步
  4. 通过主线程向InputSource中添加指令和数据
  5. 指令添加结束后,主线程发送一个通知给InputSource,随后唤醒工作线程中的RunLoop
  6. 工作线程的InputSource在接受到通知后,传送指令到RunLoop中等待处理
  7. RunLoop处理完成,进入休眠,等待下一次唤醒

代码实现

以上述理论为基础,结合Apple文档提供的代码片段,实现了一个配置自定义InputSource的Demo,完整实例可以查看GitHub源码

创建并配置InputSource对象

IBRunLoopInputSource类用于管理和配置CFRunLoopSourceRef对象,以及包含一个指令集合。

以下是初始化函数:


    @interface IBRunLoopInputSource ()
    {
        //InputSource对象
        CFRunLoopSourceRef _runLoopSource;
        //当前指令
        NSInteger _currCommand;
    }
    //指令集合
    @property (nonatomic , strong) NSMutableDictionary * commandInfo;
    
    @end
    
    @implementation IBRunLoopInputSource
    
    #pragma mark - Init

    - (id)init
    {
        self = [super self];
        
        if (self) {
            
            //InputSource上下文 ,共有8个回调函数,目前只实现3个
            CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
                &RunLoopSourceScheduleRoutine,
                &RunLoopSourceCancelRoutine,
                &RunLoopSourcePerformRoutine};
            
            //初始化自定义InputSource
            _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
            
        }
        
        return self;
    }

上述代码中可看的一共有8个与InputSource相关的回调函数,此处只配置了3个,分别是RunLoopSourceScheduleRoutine、RunLoopSourceCancelRoutine和RunLoopSourcePerformRoutine。这3个回调函数的实现会在后面进行介绍。

对InputSource的基本操作:


    //添加自定义InputSource到当前RunLoop
    - (void)addToCurrentRunLoop
    {
        CFRunLoopRef runLoop = CFRunLoopGetCurrent();
        //添加到当前RunLoop的kCFRunLoopDefaultMode模式下
        CFRunLoopAddSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
    }
    
    //从指定RunLoop移除自定义InputSource
    - (void)invalidateFromRunLoop:(CFRunLoopRef )runLoop
    {
        CFRunLoopRemoveSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
    }

对指令集合的基本操作:


    //添加指令到InputSource
    - (void)addCommand:(NSInteger)command withData:(id)data
    {
        if (data)
        {
            [self.commandInfo setObject:data forKey:@(command)];
        }
        
    }
    
    //触发InputSource指令
    - (void)fireCommand:(NSInteger)command onRunLoop:(CFRunLoopRef)runloop
    {
        _currCommand = command;
        
        //通知InputSource准备触发指令
        CFRunLoopSourceSignal(_runLoopSource);
        //唤醒InputSource所在的RunLoop,该RunLoop必须有的InputSource所在的RunLoop
        CFRunLoopWakeUp(runloop);
    }

从上面的代码可看的,正如之前理论概述总讲的顺序:发出指令之后,先通知InputSource,再唤醒其所在的RunLoop。

指令通过RunLoop循环,触发相关的回调函数,最终派发给IBRunLoopInputSource对象,然后再处理。


    //执行InputSource指令
    - (void)performSourceCommands
    {
        //根据指令获得对应的数据
        id data = [self.commandInfo objectForKey:@(_currCommand)];
        
        if (!data) {
            data = [NSString stringWithFormat:@"Empty data for command : %ld" , _currCommand ];
        }
        
        //通过代理进行指令数据处理
        if (self.delegate && [self.delegate respondsToSelector:@selector(inputSourceForTest:)]) {
            [self.delegate inputSourceForTest:data];
        }
       
    }

在这里,也许有同学感到困惑:为什么绕了一大圈,最终指令执行的代码还是由IBRunLoopInputSource对象来处理,不如直接把指令处理的函数接口公开,直接调用好了?我之前也有类似的困惑,后面仔细一想才想通。可以从两个角度来解答这个困惑:

  1. 自定义InputSource的一个主要目的在于在子线程中进行周期性的任务
  2. 假设在主线程中直接调用,那么执行的代码也是在主线程,背离了初衷。而通过子线程的RunLoop派发之后,指令对应的处理执行是在子线程
  3. RunLoop的智能休眠配合自定义InputSource能将子线程长时间执行的情况下的资源开销降到最低

上述3点恰恰的自定义InputSource的精华所在。

创建并配置InputSourceContext对象

IBRunLoopContext类是一个容器类,用于管理InputSource与RunLoop之间的关系。Demo中的代码实现的最简单的一对一的关系,也可以实现一对多的关系,即一个InputSource关联多个RunLoop。

初始化如下:


    - (id)initWithSource:(IBRunLoopInputSource *)runLoopSource andLoop:(CFRunLoopRef )runLoop
    {
        self = [super init];
        if (self)
        {
            //强引用InputSource和InputSource所在的RunLoop
            _runLoopInputSource = runLoopSource;
            
            _runLoop = runLoop;
        }
        return self;
    }

当InputSource加入RunLoop中之后,会触发相关的回调函数。在前文中提到,在创建InputSource的时候Demo中配置了3个与InputSource相关的回调函数,具体实现如下:


    //inputsource部署回调
    void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
    {
        IBRunLoopInputSource* inputSource = (__bridge IBRunLoopInputSource*)info;
        //创建一个context,包含当前输入源和RunLoop
        IBRunLoopContext * theContext = [[IBRunLoopContext alloc] initWithSource:inputSource andLoop:rl];
        //将context传入主线程建立强引用,用于后续操作
        [(AppDelegate *)[NSApp delegate] performSelectorOnMainThread:@selector(registerSource:)
                              withObject:theContext waitUntilDone:NO];
        //InputSource弱引用context,因为context已经强引用InputSource,避免循环引用,用于后续移除操作
        inputSource.context = theContext;
    }
    
    //inputsource执行任务回调
    void RunLoopSourcePerformRoutine (void *info)
    {
        IBRunLoopInputSource*  inputSource = (__bridge IBRunLoopInputSource*)info;
        //执行InputSource相关的处理
        [inputSource performSourceCommands];
    }
    
    //inputsource移除回调
    void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
    {
        IBRunLoopInputSource* inputSource = (__bridge IBRunLoopInputSource*)info;
        //移除主线程中InputSource对应的Context引用
        if (inputSource.context)
        {
            [(AppDelegate *)[NSApp delegate] performSelectorOnMainThread:@selector(removeSource:)
                                                              withObject:inputSource.context waitUntilDone:YES];
        }
       
    }

上述代码分别是InputSource部署、执行和移除相关的回调函数:

  1. 部署:在InputSource部署到RunLoop之后,触发回调函数RunLoopSourceScheduleRoutine,将inputSource对象和RunLoop打包成一个context,通过Apple实现的自定义InputSource函数,发送给主线程,用于发送指令
  2. 执行:执行对应的指令
  3. 移除:在主线程中的context引用

创建并配置工作线程

IBRunLoopInputSourceThread类用于配置RunLoop和InputSource。

线程入口函数实现如下:


    - (void)main
    {
        @autoreleasepool {
          
            //创建InputSource
            self.inputSource = [[IBRunLoopInputSource alloc] init];
            [self.inputSource setDelegate:self];
            //添加InputSource到当前线程RunLoop
            [self.inputSource addToCurrentRunLoop];
            //配置RunLoop监听器
            [self configureRunLoopObserver];
            
            while (!self.cancelled) {
                
                //作为对照,执行线程其他非InputSource任务
                [self doOtherTask];
                //切入指定模式RunLoop,且只执行一次
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
                
            }
            
        }
    }

在子线程中的入口函数中,创建InputSource并加入RunLoop,随后启动RunLoop。这里一定要在while循环中切换RunLoop,否则RunLoop只会执行一次便退出。原因在于[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]函数只会执行一次RunLoop,如果InputSource未添加或者已处理完或者超时会立即退出RunLoop。

完善主线程配置

主线程的配置在AppDelegate类中实现,包括创建工作线程、管理InputSource引用以及添加指令和发送通知。

管理InputSource引用:


    //注册子线程中InputSource对应的context,用于后续通信
    - (void)registerSource:(IBRunLoopContext*)sourceInfo
    {
        [self.sourcesToPing addObject:sourceInfo];
    }
    
    //移除子线程中InputSource对应的context
    - (void)removeSource:(IBRunLoopContext*)sourceInfo
    {
        [self.sourcesToPing enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            
            if ([obj isEqual:sourceInfo])
            {
               [self.sourcesToPing removeObject:obj];
                *stop = YES;
            }
            
        }];
      
    }

添加指令和发送通知


    - (void)addCommand:(NSInteger)command withData:(id)data
   {
       NSAssert([self.sourcesToPing count] !=  0, @"Empty Input Source...");
       
       if (self.sourcesToPing.count > 0) {
           
           //此处默认取第一个用于测试,可优化
           IBRunLoopContext *runLoopContext = [self.sourcesToPing objectAtIndex:0];
           IBRunLoopInputSource *inputSource = runLoopContext.runLoopInputSource;
           //向数据源添加指令
           [inputSource addCommand:command withData:data];
           //添加后并非要立刻触发,此处仅用于测试
           [inputSource fireCommand:command onRunLoop:runLoopContext.runLoop];
       }
       
   }

总结

在写上一篇博客的时候,对与配置自定义InputSource还尚不了解。利用碎片时间和工作间隙仔细阅读了Apple开发文档的相关资料,并且在网上查阅了同行的一些博客之后,决定自己动手写了一个Demo。写Demo的过程的遇到一些新的困惑,随着Demo的完成,大部分困惑也随之而解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容