NSRunLoop

1.什么是RunLoop

  • 运行循环
  • 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)
  • RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop
  • 线程退出,则RunLoop也退出;强制退出RunLoop,RunLoop也会退出
  • RunLoop作用

    • 保持程序的持续运行
    • 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息 ......
  • 模拟RunLoop内部实现

    • 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)

获得RunLoop对象

  • RunLoop对象
    • NSRunLoop
    • CFRunLoopRef

iOS中有两套API可以创建获取RunLoop对象。分别是Foundation框架的NSRunLoop和C语言的CFRunLoopRef

NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)

模拟RunLoop内部实现

void message(int num)
{
    printf("执行第%i个任务", num);
}
int main(int argc, const char * argv[]) {
    do {
        printf("有事做吗? 没事我就休息了");
        int number;
        scanf("%i", &number);
        message(number);
    } while (1);
    return 0;
}

 

RunLoop与线程的关系

一个线程对应一个RunLoop,主线程的RunLoop默认程序启动就已经创建好了。

子线程默认没有RunLoop,不过子线程可以有RunLoop,子线程的RunLoop得手动创建并且手动启动(调用run方法)

RunLoop在第一次获取时创建,在线程结束时销毁

可以理解为,子线程的RunLoop是懒加载的(主线程除外)。只有用到的时候才会创建( 调用currentRunLoop方法)。

如果是在子线程中调用currentRunLoop,那么系统会先查看当前子线程是否有与之对应的NSRunLoop,如果没有就创建一个RunLoop对象

注意:如果想给子线程添加一个与之对应的RunLoop,不能通过alloc、init方法,只能通过currentRunLoop,如果用alloc、init创建出来的RunLoop不能添加到子线程。

如何获取RunLoop对象

1.通过Foundation框架获取

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

2.通过Core Foundation框架获取

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

RunLoop底层实现

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        // 创建字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 创建主线程
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 保存主线程
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // 从字典中获取子线程对应的loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        // 如果不存在子线程对应的loop就创建一个
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 把新创建的loop保存在字典中,线程作为key,loop作为value
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }

以上是从CF-1151.16的CFRunLoop.c文件中拷贝的RunLoop的源码:

当我们通过[NSRunLoop currentRunLoop]调用NSRunLoop的currentRunLoop方法的时候,底层就会调用NSRunLoopRef的get方法。

  1.程序启动,底层会先创建一个字典。

  2.然后马上会创建一个主线程的RunLoop,并把主线程作为key,把主线程的RunLoop作为value添加到字典中。

注意:这也就是为什么一个线程对应一个RunLoop的原因因为RunLoop是通过key-value的形式和线程以一一对应的方式保存在字典中的。

  3.如果从子线程通过[NSRunLoop currentRunLoop]调用NSRunLoop的currentRunLoop方法的时候,系统会以子线程作为key,去字典中取对应的RunLoop对象。

  4.如果取出来的RunLoop对象为空,则系统会创建一个RunLoop对象并以子线程作为key把该RunLoop对象存储到字典中去。

RunLoop相关类

Core Foundation中关于RunLoop的5个类:
  CFRunLoopRef :RunLoop对象
  CFRunLoopModeRef :RunLoop的模式,可以把RunLoop理解为空调,对应着许多模式,但是一个RunLoop同时只能执行一种模式
  CFRunLoopSourceRef : 事件来源,用来处理RunLoop的事件
  CFRunLoopTimerRef :定时器,处理和定时器相关的事情
  CFRunLoopObserverRef :通过observer监听事件

RunLoop的结构

一个RunLoop有多个模式:每个模式都有各自的source、timer和observer。

注意:一个RunLoop有多个模式,但是在同一时刻只能执行一种模式。

CFRunLoopModeRef:

CFRunLoopModeRef代表RunLoop的运行模式
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响

系统默认注册了5个Mode:

  NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行。程序启动,如果用户什么都没做,默认就在这个模式
  UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。程序启动,如果用户滑动了scrollView,就会从默认模式切换到这个模式
  UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
  GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到                                       
  NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

PS:前四种模式是真正的模式,最后一种模式不是真正的模式。主要学习前两种模式和最后一种模式。

runLoop默认是个死循环,源码如下:

// 用DefaultMode启动
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

CFRunLoopSourceRef:

CFRunLoopSourceRef是事件源(输入源)

按照官方文档,Source的分类
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources

按照函数调用栈,Source的分类
Source0:非基于Port的, 处理app内部事件,APP自己负责触发,如UIEvent、CFSocket都属于Source0
Source1:基于Port的,通过内核和其他线程相互发送消息,由runLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort

CFRunLoopTimerRef:

CFRunLoopTimerRef是基于时间的触发器
CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响
GCD的定时器不受RunLoop的Mode影响

创建出来NSTimer对象,我们需要把NSTimer对象添加到runLoop中

// 创建一个NSTimer之后, 必须将NSTimer添加到RunLoop中, 才能执行
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];
// 添加到runLoop中(下面这就话就是把timer添加到当前线程的默认模式下)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

runLoop同一时间只能执行一个模式,所以如果把timer添加到默认模式,那么timer只在默认模式下生效。例如,切换到追踪模式,默认模式下的timer是无效的。

那么怎么让timer在默认模式和追踪模式下都有效呢?

/*
     common modes = 
     {
     0 : <CFString 0x105b56e50 [0x104e83180]>{contents = "UITrackingRunLoopMode"}
     2 : <CFString 0x104e5f080 [0x104e83180]>{contents = "kCFRunLoopDefaultMode"}
     }
 */

//
这是一个占位用的Mode,不是一种真正的Mode // 其实Common是一个标识, 它是将NSDefaultRunLoopMode和UITrackingRunLoopMode标记为了Common // 所以, 只要将timer添加到Common占位模式下,timer就可以在NSDefaultRunLoopMode和UITrackingRunLoopMode模式下都能运行
// 相当于timer添加到了这两个模式中,在这两个模式中都有效
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

 

GCD的timer不受NSRunLoop定时器的影响

    // 1.创建tiemr
    // queue: 代表定时器将来回调的方法在哪个线程中执行
//    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    self.timer = timer;
    // 2.设置timer
    /*
     第一个参数: 需要设置哪个timer
     第二个参数: 指定定时器开始的时间
     第三个参数: 指定间隔时间
     第四个参数: 定时器的精准度, 如果传0代表要求非常精准(系统会让定时器执行的时间变得更加准确) 如果传入一个大于0的值, 就代表我们允许的误差
     // 例如传入60, 就代表允许误差有60秒
     */
    
    // 定时器开始时间
//    dispatch_time_t startTime = DISPATCH_TIME_NOW;
    // 调用这个函数,就可以指定两秒之后开始/而不是立即开始
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
    
    // 定时器间隔的时间
    uint64_t timerInterval = 2.0 * NSEC_PER_SEC;
    dispatch_source_set_timer(timer, startTime, timerInterval, 0 * NSEC_PER_SEC);
    
    // 3.设置timer的回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"我被调用了  %@", [NSThread currentThread]);
    });
    
    // 4.开始执行定时器
    dispatch_resume(timer);
    
}

CFRunLoopObserverRef:

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:

自定义Observer来监听指定线程的状态的改变:

    // 0.创建一个监听对象
    /*
     第一个参数: 告诉系统如何给Observer对象分配存储空间
     第二个参数: 需要监听的类型
     第三个参数: 是否需要重复监听
     第四个参数: 优先级
     第五个参数: 监听到对应的状态之后的回调
     */
    CFRunLoopObserverRef observer =  CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//        NSLog(@"%lu", activity);
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"进入RunLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入睡眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"刚刚从睡眠中醒来");
                break;
            case kCFRunLoopExit:
                NSLog(@"退出RunLoop");
                break;
                
            default:
                break;
        }
    });
    
    // 1.给主线程的RunLoop添加监听
    /*
     第一个参数:需要监听的RunLoop对象
     第二个参数:给指定的RunLoop对象添加的监听对象
     第三个参数:在那种模式下监听
     */
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 如果通过scheduled方法创建NSTimer, 系统会默认添加到当前线程的默认模式下
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];

 总结:

一条线程对应一条RunLoop,程序一启动,主线程的RunLoop就已经创建并且和主线程绑定好。通过查看RunLoop源代码,系统内部是通过字典的形式把线程和RunLoop进行了绑定。

子线程的RunLoop默认是没有的,如果想使用子线程的RunLoop,只需要在子线程调用NSRunLoopcurrentRunLoop方法即可。

我们可以把RunLoop理解为懒加载的,只有在用到的时候才会创建。ru如果子线程中调用了currentRunLoop方法,那么系统会先根据子线程去字典中取对应的RunLoop,如果没有,则系统会创建一个RunLoop并且和该子线程进行绑定并且保存到字典中。

每个RunLoop中又有很多的mode,每个mode中又可以有很多的source、timer和observer。需要注意的是,RunLoop在同一时刻只能执行一种模式,也就是同一时刻,只有一个模式中的source、timer和observer有效,其他模式的source、timer和observer无效。苹果这样做的目的是防止不同模式中的source、timer和observer相互影响,不好控制。

可以通过timer的形式来监听RunLoop的执行流程:

进入RunLoop,首先会处理一些系统的事件(也就是首先执行timer、source0、source1)当处理完后,RunLoop就会睡觉。当用户触发一些事件后,RunLoop就会从睡眠中醒来,处理timer、source0和source1.处理完事件后又继续睡觉。

RunLoop是有生命周期的,RunLoop挂掉有两种情况:

  1.生命周期到了,默认RunLoop的生命周期是很大的,不过我们可以自己设置runLoop的生命周期

  2.线程挂了,RunLoop也会挂掉

 

RunLoop的应用

runLoop主要有5个应用场景:NSTimer、ImageView显示图片、performSelecter、常驻线程、自动释放池

1.NSTimer

上面已经举过一个NSTimer的例子,这里不再多说。

2.ImageView显示

  默认程序启动会进入runLoop的default模式。performSelecter: withObject:afterDelay:inMode:方法默认就是在default模式下有效。而在track追踪模式下无效,所以可以通过设置模式来控制imageView图片的显示。

 

// 只有在追踪模式下才会给imageView设置图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avatar"] afterDelay:2.0 inModes:@[UITrackingRunLoopMode]];

  开发中一般在默认情况下设置图片而在追踪模式下是不设置图片的,这样一来可以提高我们应用程序的流畅性,为什么呢?如果在track模式下,不仅处理屏幕的拖拽事件,还要给imageView设置图片,很容易出现程序卡顿的现象。

3.PerformSelector

4.常驻线程

常驻线程应用场景:

  举个例子,某个应用需要频繁的下载或者上传大容量的音频或者视频,默认主线程就是一个常驻线程,但是这种耗时操作肯定要转移到子线程中取完成。比如说微信\陌陌,用户有时候需要一直发送语音,如果每发送一条语音就开启一个自子线程,那么频繁的开启、销毁线极大的消耗手机性能,所以常驻线程就应运而生。

如何创建常驻线程?

尝试一:再次调用[self.thread  start];答案当然是否定的。原因如下:

  注意点:默认情况下,只要一个线程的任务执行完毕,那么这个线程就不能使用了。所以不能通过start方法来重新启动一个已经执行完任务的线程。否则会报以下错误: -[WSThread start]: attempt to start the thread again'

尝试二:给这个子线程一个执行不完的任务while(1);答案依然是否定的,原因如下:

  把while(1)添加到子线程执行,而子线程的任务中有一个while死循环,那么其他任务永远也执行不到。

  所以,通过死循环虽然保证了子线程永远不死,但是不能让子线程处理任务,因为子线程一直在处理while死循环的任务。

尝试三:联想主线程为什么不死,因为主线程默认一启动就会绑定一个runLoop,所以尝试给子线程绑定一个runLoop

[NSRunLoop currentRunLoop];
[runLoop run];

但是仅仅创建一个runLoop然后run依然无效。原因如下:

注意:
(1). currentRunLoop仅仅代表创建了一个NSRunLoop对象, 并没有运行RunLoop
(2). 一个NSRunLoop中, 如果没有source或者timer, 那么NSRunLoop就会退出死循环(面试很可能问到)。因为如果runLoop没有source和timer,那么这个runLoop就没有source和timer事件处理,这个runLoop也就变得没有意义,所以runLoop会自动退出。(runLoop是否退出和observer没有关系,只和source和timer有关系)

所以,给runLoop添加一个source或者timer

最终的解决方案:

NSRunLoop *runLoop =[NSRunLoop currentRunLoop];
// 以下代码的目的是为了保证runloop不死
/*
// 给runLoop添加一个timer
//   NSTimer *timer = [NSTimer timerWithTimeInterval:99999 target:self selector:@selector(demo) userInfo:nil repeats:NO];
//  [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
*/
// 或者给runLoop添加一个source,一般都是添加source,不添加timer,写三方框架的大牛都这么写 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];

至此,一个常驻子线程就已经创建好了,并且可以接受并处理事件。并且只要是在这个常驻子线程中执行的任务,都是在同一个线程中。

如下是创建常驻子线程的代码:

#import "ViewController.h"
#import "WSThread.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@property (nonatomic, strong) WSThread *thread; /**< 子线程 */

@end

@implementation ViewController


- (void)viewDidLoad
{
    [super viewDidLoad];
    
    
    self.thread = [[WSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
    [self.thread start];
}

- (void)demo
{
    // 在子线程执行
    NSLog(@"%s", __func__);
    
    // 注意点: 默认情况下只要一个线程的任务执行完毕, 那么这个线程就不能使用了
    // 在self.thread线程中执行test方法
//    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
//    while(1);
    
    // 给子线程添加一个RunLoop
    // 注意:
    // 1. currentRunLoop仅仅代表创建了一个NSRunLoop对象, 并没有运行RunLoop
    // 2. 一个NSRunLoop中, 如果没有source或者timer, 那么NSRunLoop就会退出死循环
    
    NSRunLoop *runLoop =[NSRunLoop currentRunLoop];
    // 以下代码的目的是为了保证runloop不死
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
    
    NSLog(@"-----------");
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 主线程
    NSLog(@"%s", __func__);
//    [self.thread start];
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

@end

打印结果:

2015-10-31 17:46:29.780 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:29.780 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:29.959 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:29.959 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:30.121 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:30.122 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:30.266 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:30.267 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:30.431 08-RunLoop应用场景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:30.432 08-RunLoop应用场景[3855:303300] -[ViewController test] <WSThread: 0x7fe44a53a290>{number = 2, name = (null)}


5.自动释放池

 程序“即将进入runLoop”会创建自动释放池,“即将退出runLoop”会销毁自动释放池。即将进入休眠状态会销毁之前的自动释放池,再创建一个新的自动释放池。

所以,释放池中的对象不是立即销毁的,而是在即将进入休眠和退出runloop的时候销毁的。

    /*
     _wrapRunLoopWithAutoreleasePoolHandler
     + activities = 0x1 = 1 = 即将进入RunLoop
     + 创建一个自动释放池
    
    _wrapRunLoopWithAutoreleasePoolHandler  
     + activities = 0xa0 = 160 = 128 + 32
     +  32 即将进入休眠  1.销毁一个自动释放池  2.再创建一个新的自动释放池
     +  128 即将退出RunLoop  销毁一个自动释放池
     */
    NSLog(@"%@", [NSRunLoop currentRunLoop]);
    NSLog(@"%d", 1 << 0); // 1
    NSLog(@"%d", 1 << 1); // 2
    NSLog(@"%d", 1 << 2); // 4
    NSLog(@"%d", 1 << 5); // 32
    NSLog(@"%d", 1 << 6); // 64
    NSLog(@"%d", 1 << 7); // 128 

学习RunLoop的资料

苹果官方文档
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

CFRunLoopRef是开源的
http://opensource.apple.com/source/CF/CF-1151.16/

posted @ 2015-08-26 03:17  oneSong  阅读(703)  评论(0编辑  收藏