RunLoop

一. 简介

  • RunLoop:通常指的是NSRunLoopCFRunLoopRef
  • CFRunLoopRef:是CoreFoundation框架下RunLoop的对象类,是纯C的代码函数。
  • NSRunLoop:是Foundation框架下RunLoop的对象类,是CFRunloopRef的OC封装。

二. 什么是RunLoop?

  • RunLoop:运行环,处理在程序的运行过程中出现的各种事件,保证了程序的持续运行。
  • RunLoop线程息息相关,每条线程都有一个与之对应的RunLoop对象。
  • 主线程的RunLoop对象系统自动创建好的,子线程的RunLoop对象则需要手动获取。
  • RunLoop所对应的线程没有事件需要处理的时候,RunLoop会使线程进入睡眠模式,从而节省CPU资源,提高程序的性能。

三. RunLoop与线程的关系

RunLoop,就是一个对象,每条线程都有一个对应的RunLoop对象。一般来说,一个线程只能执行一个任务,执行完之后,线程就会退出。这个RunLoop对象可以在线程没有任何事件需要处理的时候,使线程进入睡眠模式,节省CPU资源,提高程序的性能。在线程上有消息需要处理的时候,立刻唤醒线程,执行操作任务。

四. RunLoop的原理

RunLoop它就是线程中的一个循环,创建启动之后,它会在循环中不断的检测有没有等待接受的事件。如果有,RunLoop会通知线程进行处理,如果没有,会让线程进入睡眠模式。其底层实现类似于一个do{}while()的循环函数,该函数一直处于等待-处理的循环之中,直到超时或者手动让这个循环结束。主线程的RunLoop只要程序不退出、崩溃会一直循环。

五. 相关类

  • CFRunLoopRef:RunLoop 的对象
  • CFRunLoopModeRef:RunLoop 的运行模式
  • CFRunLoopSourceRef:RunLoop 的输入源 / 事件源
  • CFRunLoopTimerRef:RunLoop 的定时源
  • CFRunLoopObserverRef:RunLoop 的观察者,监听 RunLoop 的状态改变

注:每一个RunLoop 的对象包含若干个RunLoop 的运行模式,而每一个RunLoop 的运行模式又包含若干个RunLoop 的输入源 / 事件源RunLoop 的定时源以及RunLoop 的观察者

1. CFRunLoopRef

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

另外,如果使用NSRunLoop

  • 获取当前线程的RunLoop对象:
    [NSRunLoop currentRunLoop];
  • 获得主线程的RunLoop对象:
    CFRunLoopGetMain();

注:苹果不允许直接创建RunLoop对象,只能通过以上的方法获取RunLoop,其实整个程序的每一个RunLoop对象都放在一个全局的Dictionary里面。线程刚创建时并没有RunLoop对象,如果你不主动获取,那它一直都不会有。RunLoop对象的创建是发生在第一次获取的时候,并将它保存在Dictionary里面。RunLoop对象的销毁是发生在线程结束时。

  • 非主线程的RunLoop对象,只能在线程的内部获取。

2. CFRunLoopModeRef

  • kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行。
  • UITrackingRunLoopMode:跟踪用户交互事件模式。
  • kCFRunLoopCommonModes:伪模式,不是一种真实的模式,而是一种模式组合,即kCFRunLoopDefaultMode模式和UITrackingRunLoopMode模式的结合。(下面有例子说明)

注:每次调用RunLoop的主函数时,只能指定其中一种Mode(模式),这个Mode被称CurrentMode(当前模式)。如果需要切换Mode,会先退出Loop(其实就是退出当前的Mode),再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode下的 Source、Timer、Observer,让他们互不影响。

3. CFRunLoopTimerRef

定时源,理解为基于时间的触发器,其实就是NSTimerNSTimer的触发是基于RunLoop运行的。

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic,strong)UIScrollView *scrollView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
    [self.view addSubview:_scrollView];
    [_scrollView setContentSize:CGSizeMake(self.view.frame.size.width, self.view.frame.size.height * 2)];
    // 定义一个定时器,约定两秒之后调用self的run方法
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)run{
    NSLog(@"---");
}
@end
  • 如果是NSDefaultRunLoopMode模式,则在scrollView不滑动的情况,每隔两秒,就会打印一次---
  • 如果是UITrackingRunLoopMode模式,则在scrollView滑动的情况,每隔两秒,就会打印一次---
  • 如果是kCFRunLoopCommonModes模式,则在scrollView不管滑不滑动,每隔两秒,都会打印一次---

总结:RunLoop默认是NSDefaultRunLoopMode模式,当scrollView滑动的时候,RunLoop就结束NSDefaultRunLoopMode模式,切换成UITrackingRunLoopMode模式。当scrollView滑动结束的时候,RunLoop又结束UITrackingRunLoopMode模式,重新切换成NSDefaultRunLoopMode模式。NSTimer添加在RunLoop的哪一种模式下,- (void)run就在哪一种模式下执行。

4. CFRunLoopSourceRef

事件源,事件产生的地方。有两个版本:Source0Source1

  • Source0,只包含了一个回调,不能主动触发事件。需要先调用 CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒 RunLoop对象,让其处理这个事件。(外部手动添加的事件。)
  • Source1,包含了一个mach_port和一个回调,被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程。(系统事件)

5. CFRunLoopObserverRef

观察者,用来监听RunLoop的状态改变。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};
- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"监听到RunLoop发生改变---%zd",activity);
    });
    // 添加观察者到当前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 释放observer,最后添加完需要释放掉
    CFRelease(observer);
}

六. RunLoop的运行逻辑

RunLoop创建并启动。

  • 通知观察者,即将进入RunLoop
  • 通知观察者,将要处理NSTimer
  • 通知观察者,将要处理事件Source
  • 通知观察者,正在处理事件Source
  • 通知观察者,线程进入休眠状态。(没有Source需要处理)
  • 通知观察者,RunLoop结束。

RunLoop销毁。(主线程的RunLoop不会销毁)

七. RunLoop的应用

1. NSTimer的使用

参考上面CFRunLoopTimerRef类的例子。

2. ImageView延迟加载显示

需求:列表上面每一个cell都网络图片要显示,正常操作的话,可能会出现卡顿现象。可以通过优化,当列表在滑动的时候,不加载图片,等列表停止滑动之后,再加载图片。

[_itemImg performSelector:@selector(sd_setImageWithURL:) withObject:[NSURL URLWithString:model.pictUrl] afterDelay:1.0 inModes:@[NSDefaultRunLoopMode]];

解释:当调用NSObjectperformSelecter:afterDelay:后,实际上其内部会创建一个NSTimer并添加到当前线程的RunLoop中。

3. 常驻线程

需求:如果经常需要一些耗时操作在子线程上面执行,那么可以让一条子线程永远常驻内存。(省去不断创建线程的麻烦)

#import "ViewController.h"

@interface ViewController ()
// 常驻内存的线程
@property (nonatomic,strong)NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}
- (void)run{
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}
@end

开启runLoop有三种方法:

// 第一种
- (void)run;  
// 第二种
- (void)runUntilDate:(NSDate *)limitDate;
// 第三种
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

注:这三种方式无论通过哪一种方式启动runLoop,如果没有一个数据源或者timer附加于runLoop上,runloop就会立刻退出。

  • 第一种方式,runLoop会一直运行下去,在此期间会处理来自数据源Source的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
  • 第二种方式,可以设置超时时间,在超时时间到达之前,runLoop会一直运行,在此期间runLoop会处理来自数据源Source的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
  • 第三种方式,runLoop会运行一次,超时时间到达或者第一个数据源Source被处理,则runLoop就会退出。

退出runLoop

  • 第一种启动方式想退出runLoop,不应该使用第一种启动方式来启动runLoop。虽然runLoop在没有数据源Source或者附加的timerrunLoop就会退出。但是系统内部有可能会在当前线程的runLoop中添加一些输入源,导致无法退出。
  • 第二种启动方式可以通过设置超时时间来退出runLoop
  • 第三种启动方式,runLoop会运行一次,当超时时间到达或者第一个数据源Source被处理,runLoop就会退出。

4. RunLoop在AFN中的使用

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