iOS SDK详解之Runloop

原创Blog,转载请注明出处
http://blog.csdn.net/hello_hwc?viewmode=list
我的stackoverflow

前言:Runloop多线程开发的中的一个概念,当然也可以向Runloop中提交一些任务,监听一些事件。属于多线程编程的一部分,合理的使用Runloop对App的性能提高有很大帮助。

关于Runloop 的官方资料

笔者也是主要从三个链接中理解和概括的,如果英文好的同学,同时觉得自己对Runloop的理解不够,还是建议自习读读官方的文档,那是最权威的东西。

  • Runloop Programming Guide
  • NSRunLoop Class
  • CFRunLoopRef

什么是Runloop

Runloop是为了让线程高效处理任务而创建的机制。一个Runloop就是一个事件处理循环(Event processing loop),通过Runloop可以执行一些计划任务,接受一些事件。Runloop的特点是能够在有任务的时候让线程被唤醒,而没有任务的时候线程休眠

Runloop与线程的关系

在iOS开发中,每个线程(NSThread)在创建的时候都会自动绑定一个Runloop。主线程的Runloop在App启动的时候自动运行。自己创建的线程,要写代码来启动Runloop,提交任务,接受事件。

RunLoop的运行机制

Runloop和它的名字很像,他就是线程运行的一个循环,通过这个循环可以处理事件回调。Runloop从两个源头接收事件

从input sources接收异步事件,来源通常是从其其它线程或者其他应用

从Timer sources接收同步事件,这种事件是在指定时间触发的

除了提供回调之外,Runloop还能触发一些Notification来告诉观察者当前Runloop处于的状态。

iOS SDK详解之Runloop_第1张图片

我们再来看看iOS中,与Runloop相关的数据类型

NSRunLoop ,面相对象的方式处理Runloop,非线程安全

CFRunLoop ,C相关的关于Runloop的函数,线程安全

CFRunLoopSource,一种抽象的表示input source的对象,这种对象可以被加入到Runloop中,并且产生异步回调。例如网络数据到达,或者用户操作。CFRunLoopSource一共有两种类型,Version 0 Source 和Version 1 Source。(之所以这样命名,是因为其内部实现的有个version字段对应的是0/1)

  • Version 0 Source,这种source要应用自己管理,当这种source触发的时候,需要程序手动调用CFRunLoopSourceSignal 来触发。CFSocket就是这种Source。
  • Version1 Source,这种Source由内核和Runloop自动管理。当这种Source能够触发的时候,使用Mach ports 来通知Source。也就是说,当Mach ports 接收到可以触发的消息时候,内核自动触发这种Source

简单理解,由应用发起的任务就是Version 0,其余是Source 1

CFRunLoopTimer,是一种特别得Source,在将来的某一时间点触发。注意,这个timer的触发事件并不是精确的。当Timer所处的Mode并没有在运行,或者一个长时间的回调在执行。Timer会延迟到下一个Runloop再判断是否要触发。CFRunLoopTimer与NSTimer是toll-free bridged,意味着传入NSTimer *的地方就可以传入CFRunLoopTimerRef

CFRunLoopObserver,通过CFRunLoopObserver可以监听Runloop执行过程中的不同状态。例如开始一个Loop,结束一个Loop等。

Runloop Mode

Runloop Mode可以理解为运行模式,每个运行模式中包含了input sources和time sources以及observers,Runloop可以在多个运行模式中切换。只有处于Runloop当前模式中的timer,sources以及observers才是有效的。

通过模式切换,可以过滤掉不想要的事件,有选择的处理任务,使得各种mode相互不影响。比如主线程的Runloop就有两个模式kCFRunLoopDefaultMode和UITrackingRunLoopMode。其中,默认是运行在kCFRunLoopDefaultMode。在监听用户持续触摸的时候会切换到UITrackingRunLoopMode。所以,在Scrollview滚动的时候,添加的timer会失效,来保证滚动的流畅性。
iOS SDK详解之Runloop_第2张图片

Mode分为两种,Core Foundation预置的有几种,当然也可以自己通过CFRunloop相关API创建Mode,不同Mode按照name来区分。

预定义的几种Mode

  1. kCFRunLoopDefaultMode 默认的模式,大多数情况下都适用默认模式
  2. UITrackingRunLoopMode,接收到用户的持续触摸时候切换到的模式。
  3. kCFRunLoopCommonModes ,common mode,当像common mode中添加input source时候,也会像在标注为common mode的其他添加。(注意,不包括observer以及timer)

input Source

实际开发中常用的提交Source的是这一组列代码。这一组代码提交的Source是序列化的提交到一个线程上的,解决了很多在一个线程上执行多任务的问题。在执行完后,source会自动释放,使用这一组代码,可以向任何线程传递消息

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

Timer Sources

Timer Source和NSTimer的使用方式类似,但是要注意一点,timer并不是准确的时间。Timer也是和Mode绑定的,如果当前的Timer所在的mode并没有被Runloop切换到,那么Timer是不会触发的。

Observer

通过Observer能够监听Runloop的自身状态,给开发者一些机会,比如在Runloop开始的时候准备一些资源,在Runloop结束的时候保存一些资源。

能够监听到的状态如下

  1. 进入Runloop
  2. Runloop将要处理Timer
  3. Runloop将要处理input source
  4. Runloop将要休眠
  5. Runloop已经唤醒,但是还没有处理事件
  6. 离开Runloop

Observer和Timer一样,也可以配置只响应一次,或者重复响应。

Runloop的具体处理流程

文档原文

  1. Notify observers that the run loop has been entered.

  2. Notify observers that any ready timers are about to fire.

  3. Notify observers that any input sources that are not port based are about to fire.

  4. Fire any non-port-based input sources that are ready to fire.

  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.

  6. Notify observers that the thread is about to sleep.

  7. Put the thread to sleep until one of the following events occurs:

    • An event arrives for a port-based input source.
    • A timer fires.
    • The timeout value set for the run loop expires.
    • The run loop is explicitly woken up.
  8. Notify observers that the thread just woke up.

  9. Process the pending event.

    • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
    • If an input source fired, deliver the event.
    • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
  10. Notify observers that the run loop has exited.

什么时候使用Runloop?

前面提到了,Runloop是线程在创建的时候自动创建的(主线程Runloop自动启动),iOS SDK中,不允许显示创建Runloop,所以使用Runloop通畅有以下两个场景

场景 一

监听主线程的Runloop状态变化,进而进行一些处理。例如监听切换到UITrackingRunLoopMode的时候停止主线程上某些耗时的响应。

场景 二

创建后台线程。注意,多线程有很多技术,例如之前讲过的GCD,NSOperationQueue,当只是想把任务提交到后台,并且获取结果的时候,不要用Runloop,GCD或者NSOperationQueue更加合适。
Runloop适合以下场景。
- 使用port或者custom input 和其他线程通信的时候
- 在线程上使用Timer
- performSelector:一组函数
- 让线程不被kill,等待间歇性的任务
其中,后三条是实际应用中用到比较多的,尤其是最后一条,这么做是能够不频繁创建和释放线程,因为线程多了,很可能造成死锁或者低优先级的任务一直处于等待的情况,而且线程的创建和释放本身也是一种损耗。

在应用前,再来回顾下,对于Runloop来说,不需要也不能显式创建,每个线程在创建的时候自带了Runloop,每个线程只有一个Runloop,开发者不能创建或者销毁Runloop。对于Runloop来说,它可以包含多个Mode,一次切换到一个mode。每个Mode中包括input source,timer source,和Observer,只有处在Runloop当前执行的Mode中的source,Observer才是有效的。

有一点要注意

使用后台线程的Runloop时候,一定要给Runloop一个Input Source,否则每次运行后,都会立刻退出。

举个例子,监听主线程的Runloop状态

既然是监听状态,必然用到的是Observer,第一个例子,我们简单监听main runloop的全部状态,代码如下

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    CFRunLoopRef  mainRunloop = CFRunLoopGetMain();
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, true,0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"%@",[[self activityToStringDic] objectForKey:@(activity)]);
    });
    CFRunLoopAddObserver(mainRunloop,observer,kCFRunLoopDefaultMode);
    return YES;
}
-(NSDictionary *)activityToStringDic{
    return @{
             @(kCFRunLoopEntry):@"进入",
             @(kCFRunLoopBeforeTimers):@"在Timer处理之前",
             @(kCFRunLoopBeforeSources):@"在Source处理之前",
             @(kCFRunLoopBeforeWaiting):@"进入休眠状态之前",
             @(kCFRunLoopAfterWaiting):@"唤醒之前",
             @(kCFRunLoopExit):@"退出",
             };
}

注意,这个工程是新建的空工程,运行代码我们会看到如下log

2015-11-19 13:44:41.465 LeoRunLoopDemo[1840:165314] 进入
2015-11-19 13:44:41.465 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.465 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.466 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.466 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.467 LeoRunLoopDemo[1840:165314] 进入休眠状态之前
2015-11-19 13:44:41.467 LeoRunLoopDemo[1840:165314] 唤醒之前
2015-11-19 13:44:41.468 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.468 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.468 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.468 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.468 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.468 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.469 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.469 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.469 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.474 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.474 LeoRunLoopDemo[1840:165314] 进入休眠状态之前
2015-11-19 13:44:41.833 LeoRunLoopDemo[1840:165314] 唤醒之前
2015-11-19 13:44:41.836 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.837 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.837 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.838 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.839 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.840 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.841 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.842 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.842 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:41.843 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:41.843 LeoRunLoopDemo[1840:165314] 进入休眠状态之前
2015-11-19 13:44:42.883 LeoRunLoopDemo[1840:165314] 唤醒之前
2015-11-19 13:44:42.884 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:44:42.884 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:44:42.884 LeoRunLoopDemo[1840:165314] 进入休眠状态之前

可以看到,在app启动的时候,main runloop自动启动,并且要处理一些soucre,然后main runloop进入了休眠。
然后,我们再第一个页面上点击一下
能看到如下log

2015-11-19 13:47:05.162 LeoRunLoopDemo[1840:165314] 唤醒之前
2015-11-19 13:47:05.162 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:47:05.163 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:47:05.164 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:47:05.164 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:47:05.165 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:47:05.165 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:47:05.165 LeoRunLoopDemo[1840:165314] 进入休眠状态之前
2015-11-19 13:47:05.226 LeoRunLoopDemo[1840:165314] 唤醒之前
2015-11-19 13:47:05.226 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:47:05.226 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:47:05.227 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:47:05.227 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:47:05.227 LeoRunLoopDemo[1840:165314] 进入休眠状态之前
2015-11-19 13:47:05.918 LeoRunLoopDemo[1840:165314] 唤醒之前
2015-11-19 13:47:05.918 LeoRunLoopDemo[1840:165314]Timer处理之前
2015-11-19 13:47:05.919 LeoRunLoopDemo[1840:165314]Source处理之前
2015-11-19 13:47:05.919 LeoRunLoopDemo[1840:165314] 进入休眠状态之前

可以看到,当点击事件产生的时候,iOS系统是通过mian runloop来分发事件的。那么,我们就可以利用main runloop在空闲的时候,提交一些任务到main runloop.
可以看看百度知团队的写的这篇文章

你可能感兴趣的:(ios,sdk,runloop)