iOS学习笔记 - NSRunLoop

####什么是RunLoop
RunLoop字面上是运行循环的意思,本质是iOS系统的消息处理机制。

####为什么使用runloop

当需要和该线程进行交互的时候。主线程默认有一个runloop。当自己启动一个线程,如果只是用于处理单一的事件,该线程在执行完之后就退出了。所以当我们需要让该线程即监听某项事务事,就得让线程一直不退出,runloop就是这么一个循环,没有事件的时候,处于休眠状态,有事件到了,处理事件。

####runloop分析

在RunLoop中,有三个概念

  • 输入源
    • performSelector源
    • 基于端口(Mach port)的源
    • 自定义的源
  • 定时器
  • 观察者

在一个线程中我们需要做的事情并不单一,如需要处理定时钟事件,需要处理用户的触控事件,需要接受网络远端发过来的数据,将这些需要做的事情统统注册到事件源中,每一次循环的开始便去检查这些事件源是否有需要处理的数据,有的话则去处理。

拿具体的应用举个例子,NSURLConnection网络数据请求,默认是异步的方式,其实现原理就是创建之后将其作为事件源加入到当前的 RunLoop,而等待网络响应以及网络数据接受的过程则在一个新创建的独立的线程中完成,当这个线程处理到某个阶段的时候比如得到对方的响应或者接受完了网络数据之后便通知之前的线程去执行其相关的delegate方法。所以在Cocoa中经常看到scheduleInRunLoop:forMode: 这样的方法,这个便是将其加入到事件源中,当检测到某个事件发生的时候,相关的delegate方法便被调用。对于CoreFoundation这一层而言,通常的模式是创建输入源,然后将输入源通过CFRunLoopAddSource函数加入到RunLoop中,相关事件发生后,相关的回调函数会被调用。

每一个线程都有其对应的RunLoop,但是默认非主线程的RunLoop是没有运行的,需要为RunLoop添加至少一个事件源,然后去run它。一般情况下我们是没有必要去启用线程的RunLoop的,如果线程的runloop没有自己的input source,它不会运行。

####何时使用runloop

以下几个情况是需要用线程的runloop

  • 使用port或是自定义的input source来和其他线程进行通信
  • 在线程(非主线程)中使用timer
  • performSelector方法(如performSelectorOnThread)
  • 使用线程执行周期性工作(如网络请求)

在线程中只需要调用[NSRunLoop currentRunLoop]就可以得到当前线程的runloop,假设我们想要等待某个异步方法的回调。比如connection。如果我们的线程中没有启动run loop,是不会有效果的(因为线程已经运行完毕,正常退出了)。我们可以用一个条件来运行run loop

1
2
3
4
BOOL done = NO;
do{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}while(!done);

这样就可以一直进行等待,直到在别的位置将done置为YES,表示任务完成。

runloop的一个典型应用就是可以阻塞线程,让其暂停运行,感觉按了home键之后程序进入挂起状态就是利用的这个原理吧,我猜的,没细研究。
下面为cocoachina帖子里的一个回复,比喻很恰当:

1
2
3
4
while(done)
{
[NSRunLoop currentRunLoop] runMode:currentMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}

上面这段话看似程序进入了死循环,其实并不是这样。这段程序的意思是:

如果当前线程有当前设置的runMode下的事件发生,runloop就会启动,处理对应的事件。如果没有事件发生,runloop就会每过10秒钟启动一次当前线程的runloop.

如果runloop每次启动成功 [ NSRunLoop currentRunLoop] runMode:currentMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; 返回值为YES,这个启动成功包括了时间触发和10秒钟到了之后触发两种情况。如果启动失败返回false.
说到这个地方可能还不明白,为什么要搞个循环,为什么要用runloop,我刚开始的时候也是搞得不太明白。
这个地方解释一下:
当我们在ios设备上面触摸屏幕之后,对应的tounch事件就会调用,这是为什么呢,其实这个地方就有runloop的功劳。
其实runloop做为一种时间处理机制,类似一个车间的主任(不知道这种比喻是否恰当),这个主任他负责处理这个车间流水线上面发生特定类型事件的处理(这里的特定事件就是runMode)这个事件可以包括安全事件,机械时间等等。该主任处理时间的传统方式可以是每隔一分钟巡逻生产线一次(对应的是cpu空转轮询消息队列的方式),这种方式比较耗费工人体力(cpu资源,电量),当发现有问题发生,他就找对应的工人去处理,这里的工人对应于时间的处理函数;还有一种方式就是主任平时都在睡觉打麻将,当生产线发生问题的时候,如果是属于他的职责,系统就直接给他发送一条短信通知他,他收到之后再通知对应的工人去处理,上面那个十秒钟(这个是可以更改的)就是如果十秒内没有消息通知过来,主任才会去车间巡逻一次,但是这十秒由于主任是没有收到事件处理消息的,所以他通常是到了车间就走了(对应runLoop启动就结束,没有事件处理)。因此采用runloop的好处就显而易见了。

####runloop举例

下面看一个例子,这里边有需要注意的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
– (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@”start new thread …”);
[NSThread detachNewThreadSelector:@selector(runOnNewThread) toTarget:self withObject:nil];
while (!end) {
NSLog(@”runloop…”);
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@”runloop end.”);
}
NSLog(@”ok.”);
}
-(void)runOnNewThread{
NSLog(@”run for new thread …”);
sleep(1);
end=YES;
NSLog(@”end.”);
}

但是这样做,运行时会发现,while循环后执行的语句会在很长时间后才被执行。

那是不是可以这样:

1
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];

缩短runloop的休眠时间,看起来解决了上面出现的问题。

不过这样也有问题,runloop对象被经常性的唤醒,这违背了runloop的设计初衷。runloop的作用就是要减少cpu做无谓的空转,cpu可在空闲的时候休眠,以节约电量

那么怎么做呢?正确的写法是:

1
2
3
4
5
6
7
8
9
-(void)runOnNewThread{
NSLog(@”run for new thread …”);
sleep(1);
[self performSelectorOnMainThread:@selector(setEnd) withObject:nil waitUntilDone:NO];//此为重点
NSLog(@”end.”);
}
-(void)setEnd{
end=YES;
}

见注释部分,要将直接设置变量,改为向主线程发送消息,执行方法。问题得到解决。

这里要说一下,造成while循环后语句延缓执行的原因是,runloop未被唤醒。因为,改变变量的值,runloop对象根本不知道。延缓的时长总是不定的,这是因为,有其他事件在某个时点唤醒了主线程,这才结束了while循环。那么,向主线程发送消息,就是在向Main Thread的runloop对象添加input source,这将唤醒runloop,因此问题就解决了。