RunLoop总结

RunLoop总览

RunLoop指的是NSRunloop(基于foundation框架)或者CFRunloopRef(基于core foundation框架)CFRunloopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRefOC封装,并未提供额外的其他功能。可以将runloop理解为一个处理消息循环的对象,有消息时就处理,没有消息的时候就休眠。两者是可以相互转化的,等价的。NSRunLoop是基于CFRunLoopRef的一层OC包装。RunLoop的作用就是保持程序的持续运行,处理app的各种事件,提高CPU的使用率,有事情就做,没事情做就休眠。


RunLoop和线程的关系

runloop的创建是不让开发人员手动创建的,runloop和线程是一一对应的,具有唯一性,并且区分是否为主线程。

一个线程对应一个runloop,是键值对的关系存在的。主线程RunLoop在线程启动的时候,系统自动创建,子线程的RunLoop需要手动调用currentRunLoop方法让系统创建。RunLoop都是在第一次获取的时候创建,在线程结束时销毁。runloop并不是在alloc init方法中创建的,而是通过调用currentRunLoop方法来创建的,可以看出非主线程只有在自己线程内才可以获得runloopRunLoop的创建是懒加载的。

RunLoop的生命周期

RunLoop的生命周期创建->运行(开启,内部循环)->退出


RunLoop的相关方法

+ (NSRunLoop *)currentRunLoop

    如果调用的线程中没有runloop,那么将会创建一个并返回

  + (NSRunLoop *)mainRunLoop

    返回主线程的runloop

  - (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate

    运行loop一次或者直到limitDate。如果没有input sources加入到这个loop,那么马上返回;否则一直运行到limitDate,或者接口到一个input source然后返回。

  - (void)addPort:(NSPort *)aPort forMode:(NSString *)mode

  - (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode

    porttimer都可以添加到多个mode

  - (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(id)anArgument

    取消所有mode中的perform selectargument必须跟指定调用时候的一样

  - (void)cancelPerformSelectorsWithTarget:(id)target

  - (NSString *)currentMode

    如果run loop没有运行,那么返回nil

  - (CFRunLoopRef)getCFRunLoop

  - (NSDate *)limitDateForMode:(NSString *)mode

    下一次运行的时间,如果没有指定的mode上没有input source,返回nil

  - (void)performSelector:(SEL)aSelector target:(id)target argument:(id)anArgument order:(NSUInteger)order modes:(NSArray *)modes

order值越低优先级越高

  - (void)removePort:(NSPort *)aPort forMode:(NSString *)mode

  - (void)run

    在default mode下无限运行loop,但是如果没有任何input source,会立即返回。手动移除所有已知的inout source并不能保证run loop停止运行,因为系统可能会添加一些input source

  - (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate

    运行input source一次,为指定modeinput阻塞直到date的时间。如过没有input source,立即返回并返回NO

  - (void)runUntilDate:(NSDate *)limitDate

  如果没有input source,立即返回。否则在limitDate到来之前,不停的循环。


RunLoop的获得

获取runloop只有两个方法,currentRunLoopmainRunLoop


NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];

CFRunLoopRef runloop2 =   CFRunLoopGetMain();



开启runloop的方法

NSRunLoop提供的方法:

- (void)run; //默认模式

- (void)runUntilDate:(NSDate *)limitDate;

- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;


CFRunLoop提供的函数:

/// 默认模式

void CFRunLoopRun(void);

/// 在指定模式,指定时间,运行

CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);


当执行了上面的运行方法后,如果runloop所在的模式没有对应的事件源,即上面图中提到的input sourcestimer sources,会直接退出当前runloop(注意:是当前)。另外注意的是,input sources里面的Selector Sources,它有一些特殊情况,上面也提到了。这些情况下runloop还是会直接退出。


runloop退出的方式

1.最大的运行时间到期,推荐使用这个方式

2.modeItem(事件源)为空,不推荐使用,因为一些系统的Item并不知道。

3.调用CFRunLoopStop,退出runloop并将程序的控制权交给调用者(如果runloop有嵌套,则只退出最内层的runloop),一些情况下,CFRunLoopStop并不能真正的退出runloop

比如使用

- (void)run; //默认模式

- (void)runUntilDate:(NSDate *)limitDate;

当执行NSRunLooprun方法,一旦成功(默认模式下有事件源),那么run会不停的调用runMode:beforeDate:来运行runloop,那么即便CFRunLoopStop退出了一个runloop,很快会有另一个runloop执行。即:如果你想退出一个runloop,那么你就不该调用run方法来开启runloop

runUntilDate:run一样不停的执行runMode:beforeDate:方法,CFRunLoopStop也是退不出来的,不同的是runUntilDate:自己有个期限,超过这个期限会自动退出

很明显,你会想到利用事件源为空来退出,这种方法上面已经说了,不推荐。。。




runloop运行后,如果runloop所在的模式没有对应的事件源,就会直接退出当前runlooprunloop的退出和observer没有关系,observer只是起到了监听的作用。

runloop的内部循环图



每个runloop可以运行在不同的模式下,在一个时刻只能运行在一种模式下,一个run loop mode是一个集合,其中包含了其监听的若干事件源,定时器以及事件发生时需要通知的run loop observers。运行在一种mode下的run loop只会处理其run loop mode中包含的源事件,定时器事件,以及

通知run loop mode中包含的observers




RunLoop五个相关的类


CFRunloopRef

CFRunloopModeRefRunloop的运行模式】

CFRunloopSourceRefRunloop要处理的事件源】

CFRunloopTimerRefTimer事件】

CFRunloopObserverRefRunloop的观察者(监听者)】



CFRunloopRef

(1) CFRunloopModeRef代表着Runloop的运行模式
(2)一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
(3)每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
(4)如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
(5)这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响 

(6)系统默认注册了5mode 

模式一种五种。

Default模式

定义:NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation)

描述:默认模式中几乎包含了所有输入源(NSConnection除外),一般情况下应使用此模式,通常主线程是在这个Mode下运行。

Connection模式

定义:NSConnectionReplyMode(Cocoa)

描述:处理NSConnection对象相关事件,系统内部使用,用户基本不会使用。

Modal模式

定义:NSModalPanelRunLoopMode(Cocoa)

描述:处理modal panels事件。

Event tracking模式

定义:UITrackingRunLoopMode(iOS) NSEventTrackingRunLoopMode(cocoa)

描述:在拖动loop或其他user interface tracking loops时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住UITableView拖动时就会处于此模式,界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode影响

Common模式

定义:NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation)

描述:这是一个伪模式,其为一组run loop mode的集合,将输入源加入此模式意味着在Common Modes中包含的所有模式下都可以处理。在Cocoa应用程序中,默认情况下Common Modes包含default modes,modal modes,event Tracking modes.可使用CFRunLoopAddCommonMode方法想Common Modes中添加自定义modes

获取当前线程的run loop mode

NSString* runLoopMode = [[NSRunLoop currentRunLoop] currentMode];


RunLoop可以通过[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]来指定在一段时间内的运行模式。如果不指定的话,RunLoop默认会运行在Default下(不断重复调用runMode:NSDefaultRunLoopMode beforDate:

runloop模式的切换


对于非主线程,我们可以退出当前模式,然后再进入另一个模式,也可以直接进入另一个模式,即嵌套

对于主线程,我们当然也可以像上面一样操作,但是主线程有其特殊性,有很多系统的事件。系统会做一些切换,我们更关心的是系统是如何切换的?系统切换模式时,并没有使用嵌套


RunLoop的应用


NSTimerNSURLConnection默认运行在default mode下,这样当用户在拖动UITableView处于UITrackingRunLoopMode模式时,NSTimer不能fire,NSURLConnection的数据也无法处理。

NSTimer的例子:

在一个UITableViewController中启动一个0.2s的循环定时器,在定时器到期时更新一个计数器,并显示在label上。

-(void)viewDidLoad

{

    label =[[[UILabel alloc]initWithFrame:CGRectMake(10, 100, 100, 50)]autorelease];

    [self.view addSubview:label];

    count = 0;

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1

                                                      target: self

                                                    selector: @selector(incrementCounter:)

                                                    userInfo: nil

                                                     repeats: YES];

}


- (void)incrementCounter:(NSTimer *)theTimer

{

    count++;

    label.text = [NSString stringWithFormat:@"%d",count];

}

在正常情况下,可看到每隔0.2slabel上显示的数字+1,但当你拖动或按住tableView时,label上的数字不再更新,当你手指离开时,label上的数字继续更新。当你拖动UItableView时,当前线程run loop处于UIEventTrackingRunLoopMode模式,在这种模式下,不处理定时器事件,即定时器无法fire,label上的数字也就无法更新。

解决方法,一种方法是在另外的线程中处理定时器事件,可把Timer加入到NSOperation中在另一个线程中调度;还有一种方法时修改Timer运行的run loop模式,将其加入到UITrackingRunLoopMode模式或NSRunLoopCommonModes模式中。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];


- (void)viewDidLoad{

    [super viewDidLoad];

    NSLog(@"主线程 %@",[NSThread currentThread]);

    //创建并执行新的线程

    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];

    [thread start];

}

- (void)newThread{

   @autoreleasepool{

    //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode

    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];

    //开始执行新线程的Run Loop,如果不启动run looptimer的事件是不会响应的

    [[NSRunLoop currentRunLoop] run];

    }

}

- (void)timer_callback{

    NSLog(@"Timer %@",[NSThread currentThread]);

}



NSURLConnection也是如此,见SDWebImage中的描述,以及SDWebImageDownloader.m代码中的实现。修改NSURLConnection的运行模式可使用scheduleInRunLoop:forMode:方法。

NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];

 NSURLConnection *connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]autorelease];

[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

[connection start];

猜你喜欢

转载自blog.csdn.net/liuyinghui523/article/details/79272421