标签: objective-c RunLoop
前言
本文主要从RunLoop是什么,如何使用RunLoop,它和线程之间的关系以及在编程中可能用到的地方来介绍RunLoop。现在CoreFoundation开源了,你可以在这里下载源码。
正文
RunLoop是什么?在windows下用MFC做过开发的人对于基于消息循环的事件处理机制应该不会陌生,RunLoop和其差不多。从名字上看,它是一个loop而且是一个带条件的,同时又是一个”死循环”,但是它又很特殊,在没有事干的时候,这个循环处于闲等待的状态。写一个死循环容易:
while(1)
{
}
如果RunLoop是这样做的话,那么也就没什么值研究的了。所以说它是一种带条件的循环,那么又是什么条件呢?那就是消息或者事件,RunLoop是一个基于消息的循环。当没有消息的时候,它就会进入休眠,内核会将其挂起也就是:我睡觉了,没事别叫我。当有消息来的时候就加入到这个循环里时,内核就会将其唤醒,然后对消息进行处理,也就是:睡你XX,起来嗨。形象一点就是:
程序员 = 死了没();
while(程序员)
{
有事干了 = 我睡觉了没事别叫我();
if(该搬砖了){
搬砖();
}else if(该吃饭了){
吃饭();
}else if(该陪妹子了){
@throw(你没妹子)
}
}
RunLoop是和线程配合使用的,一般来说一个线程执行用来执行一个任务,执行完之后就会退出。当我们启动应用程序的时候,默认启动了一个主线程,所有的UI界面都是运行在主线程里的。但是启动后应用程序并不会马上就退出,而是一直运行在那里,等待用户的输入。正常情况下,应该是启动完成后,就会退出。正是RunLoop,才使得应用程序一直运行着。线程刚创建的时候并没有RunLoop(主线程除外),如果不去主动获取。RunLoop的创建发生在第一次获取时,销毁发生在线程结束时。用户只能在一个线程内部获取其RunLoop(主线程除外)。
如何像主线程一样,让我们创建的线程成为常驻线程,当有任务的时候,就交线这个线程去执行。后面的例子中会提到。
RunLoop工作
先来看看,在我们启动一个应用程序的时候,观察一下主线程的RunLoop的状态。首先说一下,RunLoop的工作的mode:
-
NSDefaultRunLoopMode:RunLoop的默认Mode,空闲状态,也就是什么也不干的时候。
-
UITrackingRunLoopMode:有滑动等其它需要追踪的事件发生时,RunLoop处于此Mode,比如在滚动scrollView时。
-
NSInitializationRunLoopMode:这是一个Private RunLoopMode,App启动时处于此Mode,启动完成进入App主界面后App处于NSDefaultRunLoopMode。
-
NSRunLoopCommonModes:此Mode默认包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。所以,当RunLoop运行在NSDefaultRunLoopMode或UITrackingRunLoopMode时,监听NSRunLoopCommonModes的Observer都会被回调。另外,还可以向NSRunLoopCommonModes里添加其它自定义的Mode。
每个mode下面又有timer,source和observer。timer是基于时间的触发器,我们在创建timer完的时候一定要把timer放进当前线程的RunLoop中,如果不是主线程还要将这个RunLoop启动,不然这个timer是不会触发的。source是事件产生的地方,系统提供了两个版本,也可以自定义source:
-
source0:处理App内部事件(特指non-port-based事件,这里的事件是一个广义的事件,包括但不限于UI事件),App负责管理自己的事件触发,如:UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。
-
source1:由RunLoop和内核管理,由Mach port驱动(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特别要注意一下Mach port的概念,它是一个轻量级的进程间通讯的方式,可以理解为它是一个通讯通道,假如同时有几个进程都挂在这个通道上,那么其它进程向这个通道发送消息后,这些挂在这个通道上的进程都可以收到相应的消息。这个Port的概念非常重要,因为它是RunLoop休眠和被唤醒的关键,它是RunLoop与系统内核进行消息通讯的窗口。
observe是用来监听RunLoop的状态的,RunLoop有以下几种状态:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //InputSource/Timer已经加入到RunLoop了
kCFRunLoopBeforeTimers = (1UL << 1), //Timer即将要被执行了
kCFRunLoopBeforeSources = (1UL << 2), //InputSource即将要被执行了
kCFRunLoopBeforeWaiting = (1UL << 5), //RunLoop即将休眠了
kCFRunLoopAfterWaiting = (1UL << 6), //RunLoop即将被唤醒
kCFRunLoopExit = (1UL << 7), //RunLoop停止运转了
kCFRunLoopAllActivities = 0x0FFFFFFFU
}
可以看到前四个描述了一个RunLoop的循环周期。
RunLoop和NSAutoreleasePool的关系
这里得说一下NSAutoreleasePool,先来看看官方文档中怎么说的:
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
意思是说,应用程序在主线程每个事件循环周期的开始中创建一个自动释放池,在这个循环周期结束的时候,把自动释放池排干,即把注册到自动释放池里的对象全部释放掉。之前一直不太明白,自动释放池是怎么释放对象的,现在可以来试验一下了:
//ViewController.m
@interface ViewController ()
//注意label的内存管理语义,用weak也可以,不过weak在赋完值后,输出不了正确结果后面会解释
@property (nonatomic, assign) UILabel *label;
@end
- (void)viewDidLoad
{
[super viewDidLoad];
//创建子线程执行任务
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread setName:@"com.thread"];
[self.thread start];
}
- (void)addRunLoopObserver
{
//添加RunLoop监听,需要使用CFRunLoop,因为NSRunloop没有这个功能
//1.获取runloop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
//2.1创建上下文
CFRunLoopObserverContext context = {
0, //这个context的版本
(__bridge void *)(self), //传入的参数,这里我传入控制器
CFRetain, //告诉它retain是调用哪个函数
CFRelease, //告诉它release是调用哪个函数
nil,
};
//2.2创建runloop观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting,
YES, 0,
callBack,
&context);
//3.给runloop添加观察者
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
}
void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
//1.拿到传过来的参数,再进行转换,因为这是C函数,不能直接调用self,所以在添加观察的时候把self传过来。
ViewController *vc = (__bridge ViewController *)(info);
//2.看看我们刚才设置的label,在这个循环同期内是否有值,可以看到是有值的。
NSLog(@"%@",vc.label.text);
}
-(void)run
{
NSLog(@"跑起来");
@autoreleasepool {
//这里生成的UILabel就注册到一个自动释放池里了,并且生成后并没有马上释放,也就是说self.label还是有值的哦,可以正常使用,但是编译器给出警告
self.label = [[UILabel alloc] init];
[self.label setText:@"这是一个标签"];
}
NSRunLoop * rl = [NSRunLoop currentRunLoop];
//向线程添加一个port,这样可以维持线程,使其成为常驻线程
[rl addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
//添加RunLoopObserver
[self addRunLoopObserver];
//启动runloop
[rl run];
//如果线程成为了常驻线程,不会执行到这行代码
NSLog(@"end");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 让子线程再次执行任务
[self performSelector:@selector(againRun) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)againRun
{
NSLog(@"再次跑起来");
//下面这行是停止当前线程的RunLoop,这个方法是在我们创建的子线程中不是主线程,加上这一行,再跑一次看看会有什么结果
// CFRunLoopStop(CFRunLoopGetCurrent());
}
这把段代码跑一次,输出的结果为:
2017-05-09 11:37:51.134 copy[92859:12314616] 跑起来
2017-05-09 11:37:51.159 copy[92859:12314616] 这是一个标签
当点击屏幕的时候,会在我们创建的线程内执行一次方法againRun
,执行完后,RunLoop就进入了休眠,这时是第二个循环周期,label已经被释放了,所以程序就会因为访问已释放的内存而崩掉。还有一点label的内存管理语义这里设定的为assgin,设置weak的时候也可以但是输也不了正确的结果,通过断点调试的时候确实可以看到当label的内存管理语义为weak的时候,它是有值的,这条语句执行完后self.label = [[UILabel alloc] init];
,label并没有马上释放掉,因为自动释放池延迟了它的释放,正常情况下,赋值完就会立该释放,但这里得不到正确的结果,猜想肯定编译器做了手脚。大家可以将label的内存管理语义改为weak,再跑一次看看。
RunLoop对timer的影响
先来看一个例子:
//ViewController.m
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.scrollView setContentSize:CGSizeMake(0, 1000)];
self.scrollView.delegate = self;
NSTimer *timer1 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doTimer1) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(@"%@",(NSString *)CFBridgingRelease(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@",(NSString *)CFBridgingRelease(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())));
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@",(NSString *)CFBridgingRelease(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())));
}
- (IBAction)button:(id)sender
{
NSLog(@"%@",(NSString *)CFBridgingRelease(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())));
}
- (void)doTimer1
{
NSLog(@"timer go");
}
@end
当程序跑起来什么也不干就会每隔一秒,控制台输入一个timer go,但是如果我们不停的滑动scrollView,控制台就停止输出了。因为这个timer运行在NSDefaultRunLoopMode下,只有当RunLoop在这个mode下才会执行timer,当程序运行的时候,我们没做任何事,Runloop就处在这个mode下,等待用户的输入。但是当我们滑动scrollView时,RunLoop由NSDefaultRunLoopMode转到了UITrackingRunLoopMode下,就不会执行timer了。在点击按钮,滑动scrollView,点击屏幕和移动屏幕中可以看到mode的变化。
总结
最后以一个RunLoop的应用场景结束本文。在有UITableView的控制器里,滑动UITableView加载图片时,主线程会把图片设置到cell上,如果这时你同时又滑动tableView,会因为主线程同时设置图片和滑动而造成到卡顿的现象。知道了RunLoop后,在滑动的时候,让子线程去处理网络请求,当停止滑动的时候,这时RunLoop进入到了NSDefaultRunLoopMode下,这时让主线程去设置图片。这样实现的话,代码只有一句:
UIImage *downloadImage = .....
[self performSelector:@selector(setImage:) withObject:downloadImage afterDelay:0 inModes:NSDefaultRunLoopMode];
–EOF–