iOS开发 -- RunLoop的基本概念与例子分析

作者: 啊左 | 来源:发表于2016-10-26 23:27 被阅读2117次

    看了一下,上一篇貌似5个月前的😅。
    最近公司忙着开发一个cordova的项目,自己也是边工作边找一些资料学习,都没怎么关注博客上的内容...呃,主要还是懒癌发作吧😌。争取多写写博客,记录记录点滴,也希望不管技能、生活还是职业生涯上都能不断成长,共勉~
    这篇是关于RunLoop的笔记的整理和一点见解。

    【本次开发环境: Xcode:7.2 iOS Simulator:iphone6 By:啊左 本文Demo下载链接:RunLoop-Demo


    基本概念

    一、RunLoop简介

    RunLoop,跑圈。在iOS开发中,也就是运行循环。
    在应用需要的时候自己跑起来运行,在用户没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。

    二. RunLoop的概念与作用

    概念:一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。但是有时候我们需要线程能够一直“待命”随时处理事件而不退出,这就需要一个机制来完成这样的任务。
    这样一种机制的代码逻辑如下:

    function loop() { 
          initialize(); 
         do { 
               var message = get_next_message(); process_message(message);
     } while (message != quit);
    }
    

    这种模型通常被称作 Event Loop
    Event Loop 在很多系统和框架里都有实现。而实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
    例如一个应用放那里,不进行操作就像静止休息一样,点击按钮,就有响应,就像“随时待命”一样,这就是RunLoop的功劳。
    所以 RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行 RunLoop 的逻辑。
    线程开始这个函数之后,便一直会处于此函数 "接受消息->等待->处理" 的循环中:
    (有事:做出反应; 木事:休眠省电; 再次有事:重新唤醒、处理事件。)
    直到这个循环结束(比如传入 quit 的消息),最后函数返回。
    作用:
    1.保持程序持续运行:例如程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行;
    2.处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等 );
    3.节省CPU资源,优化程序性能:程序运行起来时,当什么操作都没有做的时候,RunLoop就通知系统,现在没有事情做,然后进行休息待命状态,这时系统就会将其资源释放出来去做其他的事情。当有事情做,也就是一有响应的时候RunLoop就会立马起来去做事情;

    RunLoop,最重要的作用,也就是用来管理线程的。可以说,没有线程,也就没有RunLoop的存在必要。
    当线程的RunLoop一开启,RunLoop便开始对线程进行管理工作:在线程执行完任务后,线程便会进入休眠状态,并且不会退出,随时等待新的任务。

    三、RunLoop与线程的关系

    1. 每条线程都有唯一的一个与之对应的RunLoop对象;
    2. RunLoop在第一次获取时创建,在线程结束时销毁;只能在一个线程的内部获取其 RunLoop(主线程除外)。
    3. 主线程的RunLoop系统默认启动,子线程的RunLoop需要主动开启;

    其实在我们每次建立项目的时候,就已经使用上了RunLoop。
    在程序的启动入口 main 函数中有这样一段熟悉的代码:

    int main(int argc, char * argv[]) 
    {     
        @autoreleasepool 
       { 
         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
       }
    }
    

    实际上 UIApplicationMain 函数内部就启动了一个与主线程相关联的 RunLoop。
    当我们点击运行,系统运行 UIApplicationMain 函数,系统进入了:主线程 main 的运行循环。RunLoop 使得主线程一直处在运行循环中。
    我们可以做一下验证,在“Main.storyboard”中随意放置几个按钮控件,main.m 文件代码修改如下::

    int main(int argc, char * argv[]) { 
         @autoreleasepool { 
            NSLog(@"开始"); 
            return 0; 
    }
    }
    

    点击运行,输出“开始”后,模拟器界面也是一片空白。“stop”按钮也点不下去了:

    stop按钮不能点击
    因为当输出“开始”后,“return 0”,之后没有进入主线程运行循环,程序一启动就结束了,控件与其他程序有关的都没有执行,所以界面空白。
    说明了在 UIApplicationMain 函数中,开启了一个和主线程相关的 RunLoop,导致 UIApplicationMain 不会返回,一直在运行中,也就保证了程序的持续运行。
    这也是为什么应用能够在我们无任何操作时休息,在我们进行操作的时候又能够立刻进行响应活动,恰恰因为应用处于 RunLoop 的“等待命令”的状态。

    四、RunLoop 对象与相关类。

    对象:

    从 RunLoop 的概念,我们可以知道 RunLoop 实际上就是一个管理着线程对象。那么,如何获取 RunLoop 对象呢?
    Foundation框架中:

    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop];   // 获得主线程的RunLoop对象
    

    Core Foundation框架中:

    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain();   // 获得主线程的RunLoop对象`
    
    文档中的相关类:
    CFRunLoopRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopModeRef
    CFRunLoopObserverRef`
    

    他们的关系如下图:


    1. 一个RunLoop包含若干个Mode,而每个Mode又包含若干个Source/Timer/Observer。
    2. RunLoop每次只能指定一种Mode。而且如果需要切换 Mode,只能退出当前 Loop。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
    3. 如果一个 mode 中一个 “Source/Timer/Observer” 都没有,则 RunLoop 会直接退出,不进入循环。
    CFRunLoopSourceRef 输入源

    是事件产生的地方,函数调用栈上Source有两个版本:Source0 和 Source1。

    • Source0:非基于端口port,例如触摸,滚动,selector选择器等用户触发的事件;(只包含了一个回调函数,它并不能主动触发事件)
    • Source1:基于端口port,一些系统事件; (包含了一个 mach_port 和一个回调函数,被用于通过内核和其他线程相互发送消息。能主动唤醒 RunLoop 的线程)
    CFRunLoopTimerRef 定时源

    基于时间的触发器,与NSTimer可混用。
    包含了一个时间长度和一个回调函数。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

    CFRunLoopModeRef mode类型

    事实上CFRunLoopModeRef 类并没有对外暴露,而如果在Xcode中查看CFRunLoopRef,可以看到CFRunLoopModeRef 类,通过 CFRunLoopRef 的接口进行了封装。
    CFRunLoopModeRef有5种形式:(当然,还有一些开发中基本用不到的更多的苹果内部的 Mode:Mode介绍

    kCFRunLoopDefaultMode 默认模式,通常主线程在这个模式下运行;
    UITrackingRunLoopMode 界面跟踪Mode,用于追踪Scrollview触摸滑动时的状态;
    kCFRunLoopCommonModes 占位符,带有Common标记的字符串,比较特殊的一个mode;
    UIInitializationRunLoopMode 刚启动App时进入的第一个Mode,启动后不在使用;
    GSEventReceiveRunLoop 内部Mode,接收系事件。 
    

    从关系图,我们可以知道 RunLoop 一次只能指定一种 Mode,且能够让不同组的 Source/Timer/Observer 互不影响,具体的实现后面会用一个项目例子来参考。

    CFRunLoopObserverRef 观察者

    RunLoop的观察者,能够监听RunLoop的状态改变。
    每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化,可以观察到不同时刻的状态有以下几个:

    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 
    kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 
    kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 
    kCFRunLoopExit = (1UL << 7), // 即将退出Loop
    };
    

    两个例子

    测试一、二的UI设计界面如下:

    测试一:RunLoop的运用。

    在“ViewController.m”中创建一个子线程,在线程方法中一直开启RunLoop。并在“Main.storyboard”中添加一个名为“showSource”的按钮控件,创建RunLoop事件源,使得RunLoop进入循环:

    1 @interface ViewController () 
    2  
    3 @property (strong,nonatomic)NSThread *thread; //记得使用Strong属性 
    4 - (IBAction)showSource:(id)sender;            //点击按钮,添加RunLoop事件源用。 
    5  
    6 @end 
    7  
    8 @implementation ViewController 
    9 
    10 - (void)viewDidLoad {
    11  [super viewDidLoad];
    12 //创建自定义的子线程
    13 self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadMethod) object:nil];
    14 [self.thread start]; //启动子线程
    15 }
    16 -(void)threadMethod
    17 {
    18 NSLog(@"打开子线程方法");
    19 while (1) {
    20 
    21 //条件一:run,进入循环,如果没有source/timer就直接退出,不进入循环,后面加上source才能进入工作。
    22 /*【原因:如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会休眠等待相应事件的发生;
    23  这就是为什么run loop可以做到让线程有工作的时候忙于工作,而没工作的时候处于休眠状态。】
    24 */
    25  [[NSRunLoop currentRunLoop]run];
    26 
    27 //上面一行代码等于加了参数为1的while,所以当有source进入循环,下面这条代码的就不会运行。
    28   NSLog(@"这里是threadMethod:%@", [NSThread currentThread]);
    29 //如果要测试“二、addTime”按钮的话,建议注释掉上面这句代码。
    30  }
    31 }
    32 
    33 #pragma mark -- 测试一:子线程Selector源的启动
    34 - (IBAction)showSource:(id)sender {
    35 
    36 //注意:在这个方法里面输出的是main主线程,因为是主线程运行的UI控件行为。
    37   NSLog(@"这里是主线程:%@",[NSThread currentThread]);
    38 /*
    39  在没有run之前,一直处于休眠状态。所以如果要运行selector方法,还需要threadMethod中条件一不断循环的Run!
    40  在我们指定的线程中调用方法,此处相当于增加了一个带source的mode,有内容,实现了RunLoop循环运行成立的条件二。
    41 */
    42 //试着在这句之前添加[[NSRunLoop currentRunLoop]run];是不能启动子线程的RunLoop,因为此处是在main主线程上。
    43  [self performSelector:@selector(threadSelector) onThread:self.thread withObject:nil waitUntilDone:NO];
    44 }
    45 -(void)threadSelector//【此处运行在子线程】
    46 {
    47   NSLog(@"打开子线程Selector源");
    48   NSLog(@"此处是threadSelector源:%@",[NSThread currentThread]);
    49 }
    

    输出结果:

    2016-10-24 10:48:24.971 RunLoop演示[18111:752173] 打开子线程方法
    2016-10-24 10:48:24.973 RunLoop演示[18111:752173] 这里是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)}
    2016-10-24 10:48:26.256 RunLoop演示[18111:752173] 这里是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)}
    ........
    2016-10-24 10:48:26.260 RunLoop演示[18111:752173] 这里是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)}
    2016-10-24 10:48:26.261 RunLoop演示[18111:751978] 这里是主线程:<NSThread: 0x7fc830402b30>{number = 1, name =** main**}
    2016-10-24 10:48:26.261 RunLoop演示[18111:752173] 这里是threadMethod:<NSThread: 0x7fc830411a70>{number = 2, name = (null)}
    2016-10-24 10:48:26.263 RunLoop演示[18111:752173] 打开子线程Selector源
    2016-10-24 10:48:26.264 RunLoop演示[18111:752173] 此处是threadSelector源:<NSThread: 0x7fc830411a70>{number = 2, name = (null)}
    

    分析代码:

    第3行为什么子线程thread需要用到strong属性?
    如果使用weak,子线程调用不了,子线程thread一创建就立刻销毁了。如果我们使用自己自定义的线程,并且重写线程的“-(void)dealloc”方法,我们会看到其实子线程thread一创建就调用dealloc立刻销毁了。
    19-28行为什么要用到while?
    重点:Run loop的管理并不完全是由系统自动控制的,而是要由我们手动显式开启。所以我们在设计子线程代码的时候,必须符合以下条件才能进入循环:

    1. RunLoop处于开启状态; (子线程由我们手动开启)
    2. 正确响应输入事件;

    所以第一步我们需要使用while/for语句来驱动RunLoop,以便能够进行循环。
    第37行
    通过输出线程的对象信息,我们可以发现,此时处于UI控件按钮的事件其实属于主线程main,
    (在这里有个疑问,如何把Run驱动RunLoop的代码放在此处的话,还能不能performSelector创建事件源呢?
    答案是不能的,因为此时是在主线程里。也就是:Run的不是子线程:self.thread。因此也不会执行threadSelector方法)
    第43行:
    我们在while中使RunLoop一直处在开启的状态,所以当创建一个Selector源时,满足条件2:RunLoop进入循环中,执行子线程的threadSelector方法,在这个RunLoop子线程处于运行循环管理中,如“while(1)”死循环一般,便不会执行后面那句输出代码,也即是停止输出 “这里是threadMethod:.........”。
    (是不是类似文章开头关于main函数的测试,当进入循环后,便不会执行后面输出“结束”那段代码了。区别是主线程是默认自动开启的,而子线程的RunLoop则需要我们手动开启。)

    测试二:mode模式与定时源的同步性

    在“Main.storyboard”中进行timer事件测试。
    a.添加一个用于显示内容的名为“textView”的文本控件,b.再添加一个名为“addTime”的按钮控件。

    @interface ViewController ()
    //测试一
    @property (strong,nonatomic)NSThread *thread;
    - (IBAction)showSource:(id)sender;
    
    //测试二
    @property (weak, nonatomic) IBOutlet UITextView *textView;
    - (IBAction)addTime:(UIButton *)sender;
    @end
    

    然后在“ViewController.m”中threadSelector方法后面添加以下代码;

    #pragma mark -- 二、Time测试
    - (IBAction)addTime:(UIButton *)sender { 
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self 
                selector:@selector(showTimer) userInfo:nil repeats:YES];
    //添加timer到RunLoop 
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    -(void)showTimer 
    //【在主线程】{ 
    NSLog(@"调用time的线程:%@",
    [NSThread currentThread]); 
    [self showText:@"-------time-------"];
    }
    #pragma mark --在文本控件textView后面增加str字符串
    -(void)showText:(NSString *)str 
    //注意:因为UI控件需要在主线程里面,如果是在子线程threadMethod方法执行此段代码则运行报错。
    { 
      NSString *text = self.textView.text; 
      self.textView.text = [text stringByAppendingString:str];
    }
    
    关于mode模式:

    操作:当点击addTime按钮后,textView控件上不断显示“-------time-------”,但是当我们拖拽textView进度条上下移动时,会发现"-(void)showTime:"不会执行,textView控件上的内容不再增加“-------time-------”,就像“卡住了,死机了”一样。当我们停止对textView进行拖拽后,控件上的内容又不断添加更新了。
    解决方案:
    修改mode类型:把默认模式NSDefaultRunLoopMode改为占位符NSRunLoopCommonModes
    发现如果修改成这样,那么即使我们对textView进行拖拽,内容会一直增加“-------time-------”,再也不会由于拖拽而被牵制住了。
    原因:
    每次RunLoop只能支持一种mode。当我们点击addtime按钮后,定时源(timer)加入到RunLoop中,而当滑动textView时,RunLoop自动切换成UITrackingRunLoopMode模式,定时器就停止了响应。
    NSRunLoopCommonModes等效于NSDefaultRunLoopModeNSEventTrackingRunLoopMode两种模式的结合
    所以当我们在带有 “Common ”标记的NSRunLoopCommonModes模式下添加定时源(timer)后。即使我们对textView进行滚动操作,也不会影响到内容的显示了。
    另外提一下,还有另一种添加time的方法:

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
                               selector:@selector(showTimer) userInfo:nil repeats:YES];
    
    /*使用scheduledTimerWithTimeInterval方法,会自动添加到RunLoop,
     所以可以不写以下代码,只是会默认为NSDefaultRunLoopMode模式*/
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
    关于同步:

    当我们观察控制台的输出,可以发现,其实调用 "-(void)showTimer" 输出的是在主线程mian中。
    这是因为输入源使用传递异步事件,且通常消息来自于其他线程或程序。
    定时源是在以同步方式传递信息的。

    其他补充

    1.RunLoop输入源的结构图如下:


    RunLoop接收输入事件来自两种不同的来源:输入源(input source)定时源(timer source)
    输入源:传递异步事件,通常消息来自于其他线程或程序。
    输入源有3种类型:
    • Selector源:如例子按钮事件中的performSelector,当在子线程中执行Selector时,目标线程必须RunLoop处于开启状态,不然Selector就一直处于休眠状态;
    • 基于端口的输入源:就是之前提到的Source1。通过内置的端口相关的对象和函数,创建配置基于端口的输入源。 例如可以使用NSPort的方法把该端口添加到 RunLoop;
    • 自定义输入源:创建custom输入源,必须使用Core Foundation里面的CFRunLoopSourceRef类型相关的函数来创建,并自定义自己的行为和消息传递机制;

    测试一中,当我们点击按钮后,执行UI按钮控件的事件,此时“performSelector”一个Selector输入源,所以,系统执行Selector方法。
    2.RunLoop的内部流程的逻辑如下:

    备注:左边黄色的地方,“source0 (port) ”改为"source1 (port)"
    所以在测试一中,处于while一直进行着的语句:
    [[NSRunLoop currentRunLoop]run];
    

    每次的Run都代表着:进行一次消息轮询,如果没有任务需要处理的消息源,则直接返回;


    本文主要阐述基本概念与应用,如果有兴趣的童鞋可以参考:
    1.RunLoop的官方文档
    2.ibireme的文章,关于RunLoop背后的底层原理的详解:
    http://blog.ibireme.com/2015/05/18/runloop/
    3、以及这篇关于输入源定时源的详解介绍:
    http://blog.csdn.net/ztp800201/article/details/9240913


    (转载请标明原文出处,谢谢支持 ~ - ~)
     by:啊左~

    相关文章

      网友评论

      • Maj_sunshine:新手 不知道runloop在实际开发过程中的运用 总感觉很虚幻 求教怎么使用:scream:
        啊左:@学污直径 上面有输入源和定时源两个例子你可以看下。Runloop主要是用来管理线程,更重要的是了解多线程这方面,Runloop你可以慢慢在制作demo中加入自己的理解。
      • Delpan:至今不明白为什么个个写RunLoop都喜欢用那个流程图,第七步都写了Port还怎么可能是Source0.....一个错,接下来的个个都错。
        啊左:@Delpan 第七步确实不关source0的事情;
        图片是来自网上,一直在关注蓝色部分的流程,感觉很多网友都有解释了所以没有剖析这部分。估计是source1写成0了吧;
        在这里只是做为一个流程范图,希望不会误导吧。主要想表达的是这部分:每次的Run都代表着:进行一次消息轮询。
      • 53c469a10b6e:return 下面写输出 这。。。
        啊左:@夺光恶 哈,所以直接就报警告了,纯碎为了便于循环的理解。
      • 张云龙:用NSTimer时研究过,其他时候还没有深入,感谢分享,先收藏。

      本文标题:iOS开发 -- RunLoop的基本概念与例子分析

      本文链接:https://www.haomeiwen.com/subject/nwrjuttx.html