iOS RunLoop理解

RunLoop概念

一个APP之所以能在程序运行起来不停止,就是RunLoop的原因,RunLoop就像一个死循环,等待处理外部手机操作,网络请求以及内部通讯等命令,其实RunLoop是管理线程的一种机制,这种机制不仅在iOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。

RunLoop作用

一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠。
RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

RunLoop和线程

RunLoop是为了线程而生,没有线程,它就没有存在的必要。RunLoop是线程的基础架构部分。线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是RunLoop。

RunLoop中API

CocoaTouch和CoreFundation都提供了Runloop对象方便配置和管理线程的RunLoop。OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
CocoaTouch层面提供的API比较简单:
每个线程,包括程序的主线程都有与之相应的RunLoop对象。
主线程的RunLoop默认是启动的,通过[NSRunLoop mainRunLoop]获得。子线程的RunLoop如果要启动需要手动调用。在任何一个CocoaTouch程序的线程中,都可以通过NSRunLoop *runloop = [NSRunLoop currentRunLoop]来获取到当前线程的RunLoop。开启子线程RunLoop,可以使用[[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
那么,开启的RunLoop什么时候销毁呢?答案是当该线程销毁时,该线程的RunLoop肯定被销毁或者RunLoop的mode为空的时候销毁,那么怎么判断mode为空呢?答案是该mode中没有observer,source,timer事件就为空。

CoreFundation层面提供的API相对更多一点:
在 CoreFoundation 里面关于 RunLoop 有5个类:
1.CFRunLoopRef
它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

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

3.CFRunLoopSourceRef
处理事件源。Source有两个版本:Source0 和 Source1。分别处理不同事件,Source0处理外部交互事件,Source1处理内部通信等事件。

4.CFRunLoopTimerRef
处理Timer事件。

5.CFRunLoopObserverRef
观察者,监听RunLoop不同状态。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};
-(void)observerRunLoop
{
    //监听kCFRunLoopDefaultMode下的RunLoop状态
    CFRunLoopMode mode =kCFRunLoopDefaultMode;
    
    CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 0, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                SDLog(@"RunLoop启动");
                break;
            case kCFRunLoopBeforeWaiting:
                SDLog(@"RunLoop即将休眠");
                break;
            case kCFRunLoopAfterWaiting:
                SDLog(@"RunLoop被唤醒");
                break;
            case kCFRunLoopBeforeTimers:
                SDLog(@"RunLoop即将处理Timers");
                break;
            case kCFRunLoopBeforeSources:
                SDLog(@"RunLoop即将处理Sources");
                break;
            case kCFRunLoopExit:
                SDLog(@"RunLoop退出");
                break;
                
            default:
                break;
        }
    });
    
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, mode);
}

RunLoop关于mode的应用

RunLoop包含5中mode(模式),每种模式接受不同的事件源。
a. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
b. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
c. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
e. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

每种mode中又包含三种item(soures,observer,timer),如果一个Mode中一个item都没有,则这个RunLoop会直接退出。我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是因为系统为其添加了很多item。

mode:最常见的两种模式,默认模式(空闲)NSDefaultRunLoopMode,UI模式UITrackingRunLoopMode,比如UI相关事件(滑动,点击等),就是主线程RunLoop在UITrackingRunLoopMode模式下进行监视处理的。

1.例如:performSelector方法
//performSelector默认是在当前RunLoop的默认模式下执行方法
[self performSelector:@selector(test) withObject:self];
//可以通过performSelector指定RunLoop模式的方式解决RunLoop问题
    [self performSelector:@selector(test) withObject:self afterDelay:3 inModes:@[NSRunLoopCommonModes]];
2.例如:处理滑动时间和定时器冲突的问题

主线程调用timer,添加到NSDefaultRunLoopMode的RunLoop中,此时滑动scrollview,那么timer将停止打印,若添加到UITrackingRunLoopMode的RunLoop中,滑动scrollview,那么timer可以打印,但是不滑动时不打印。原因就是滑动时主线程RunLoop在UITrackingRunLoopMode模式下运行,timer如果放到该模式下就能检测到,如果放到NSDefaultRunLoopMode模式下就检测不到。如果想在两种模式下都检测到,就都需要添加,当然,iOS为我们提供了复合的Mode-NSRunLoopCommonModes,包含上述两种模式。
解决方法就是添加到NSRunLoopCommonModes。

NSTimer * timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    }];
    
 [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    
 [timer setFireDate:[NSDate distantPast]];

还有种解决办法,就是子线程执行timer,或者使用dispatch_source_set_timer在全局并行队列执行。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            
        }];
        
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
        
        [timer setFireDate:[NSDate distantPast]];
        
       //注意要开启子线程的RunLoop,因为子线程RunLoop默认关闭

        [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
    });
timer=dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        
    });
    dispatch_resume(timer);
3.开启常驻子线程

NSRunLoop提供了添加item的API,也可以通过添加item让子线程RunLoop活下来。

[NSRunLoop currentRunLoop]addTimer:(nonnull NSTimer *) forMode:(nonnull NSRunLoopMode)

[[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];

[NSRunLoop currentRunLoop]addObserver:(nonnull NSObject *) forKeyPath:(nonnull NSString *) options:(NSKeyValueObservingOptions) context:(nullable void *):

例如:让一个子线程处理处理完一个任务之后,再处理另一个任务。

/*单纯使用线程间通讯是做不到的,因为子线程一旦执行完任务就销毁了啊,无法再被唤醒,除非使用该子线程常驻不被销毁。
*/
[self performSelector:@selector(test) onThread:子线程 withObject:nil waitUntilDone:YES];
/*这样就可以考虑在子线程中开启该子线程RunLoop,并让RunLoop做任务让该子线程保持存活,那么做什么任务呢,根据之前的知识,可以是source事件,可以是timer事件,也可以是observer,推荐使用基于端口的source0事件。
*/
    NSRunLoop *runloop=[NSRunLoop currentRunLoop];
    [runloop addPort:[NSPort port]forMode:NSDefaultRunLoopMode];
    [runloop run];

RunLoop的内部逻辑

RunLoop_1.png

RunLoop循环内部会不断创建和销毁自动释放池处理一些垃圾数据(使用过的变量等)
自动释放池第一次创建:当RunLoop启动时
自动释放池最后一次销毁:当RunLoop销毁时
自动释放池其他时间创建和销毁:当RunLoop即将进入休眠的时候,释放之前的自动释放池(回收数据),创建新的自动释放池。

RunLoop在常用SDK中应用场景

AFNetworking(相当于一个线程常驻的方式)
这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

据说使用NSURLConnection的老前辈都需要通过RunLoop调试子线程的网络回调。

PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

AsyncDisplayKit
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

参考文章:https://blog.ibireme.com/2015/05/18/runloop/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容