iOS-RunLoop初识

前言

iOS开发中,RunLoop就是个神秘的领域,很多2~3年的开发者都不能准确的描述它的具体含义,甚至可能从来都没有接触过该方面的技术,或者项目中隐约用到过(定时器),但是不知道是其的作用,问题一解决就不在深究了,不过也有情可原,毕竟大部分开发者还处于基本功能的构建,很少涉及性能的优化。

RunLoop之所以神秘,个人认为,系统能够利用RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能。实现这种技术的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

RunLoop的基本概念

简介
RunLoop从字面上来看:运行循环,或者是跑圈。
#基本作用:
1、保持程序的持续运行(比如主运行循环)
2、处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
3、节省CPU资源,提高性能。
通常来讲,一个线程一次只能执行一个任务,当线程中的任务执行完成后,线程就会自动退出。如果我们需要一个长运行机制,让线程能够随时处理事件并且保证不退出,通常我们的做法,逻辑是这样的:

function  loop {
     initialize();
     BOOL running = YES;
    do{
           // 执行各种任务,处理各种事件
      }while (running);
     return 0;
 }

上述函数中,通过一个循环来执行任务,根据事件的回调和观察者的属性来改更改running的值,有效的使得函数中的任务长久的运行。简单的说,这就是程序的运行模式。

众所周知,iOS应用启动后,不会正常的自动退出。这就是因为iOS系统拥有的RunLoop机制的作用,当应用启动后,主线程的RunLoop默认开启,从而应用进入一个死循环,阻止了程序在运行完毕之后退出。

int main(int argc, char * argv[]) {
@autoreleasepool {
    //这个函数永远不会有返回值
    return UIApplicationMain(argc, argv, nil, 
          NSStringFromClass([AppDelegate class]));
      }  
}

1.在UIApplicationMain函数内部就启动了一个RunLoop
2.UIApplicationMain函数一直没有返回,保持了程序的持续运行
3.这个默认启动的RunLoop是主线程关联的
4.一个线程对应一个RunLoop,主线程的RunLoop默认已经启动
5.子线程的RunLoop得手动启动(调用运行方法)
6.RunLoop只能选择一个模式启动,如果当前模式中没有任Source,Timer,Observer,那么就直接退出RunLoop

RunLoop在循环过程中监听着port事件和timer事件,当前线程有任务时,唤醒当当线程去执行任务,任务执行完成以后,使当前线程进入休眠状态。

RunLoop与线程

RunLoop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,RunLoop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。RunLoop是线程的基础架构部分,Cocoa和CoreFundation框架都提供了RunLoop的相关接口,方便配置和管理线程的RunLoop。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象,主线程的RunLoop对象是默认开启的,子线程的run loop对象需要程序员手动开启。

源码中可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

RunLoop与autorelease pool

从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件,触摸事件。我们都知道: 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。但是如果每次都放进应用程序的main.m中的autoreleasepool中,迟早有被撑满的一刻。这个过程中必须有一个释放的动作。在一次完整的运行循环结束之前,会被销毁。

Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的runloop迭代结束时释放。

在使用手动的内存管理方式的项目中,会经常用到很多自动释放的对象,如果这些对象不能够被即时释放掉,会造成内存占用量急剧增大。Run loop就为我们做了这样的工作,每当一个运行循环结束的时候,它都会释放一次autorelease pool,同时pool中的所有自动释放类型变量都会被释放掉。

RunLoop常用的类

首先要知道iOS里面有两套API可以访问和使用RunLoop:
1、Foundation --->  NSRunLoop
2、Core Foundation ---> CFRunLoopRef

CoreFoundation 里面关于 RunLoop 有5个类:
1、CFRunLoopRef
2、CFRunLoopModeRef
3、CFRunLoopSourceRef
4、CFRunLoopTimerRef
5、CFRunLoopObserverRef

上面两套都可以使用,但是要知道CFRunLoopRef是用c语言写的,是开源的,相比于NSRunLoop更加底层,而NSRunLoop其实是对CFRunLoopRef的一个简单的封装。便于使用而已。这样说来,显然CFRunLoopRef的性能要高一点。

另外,我们不能在一个线程中去操作另外一个线程的run loop对象,那很可能会造成意想不到的后果。不过幸运的是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的run loop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:

  • (CFRunLoopRef)getCFRunLoop;
    获取对应的CFRunLoopRef类,来达到线程安全的目的。

CFRunLoopSourceRef

Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。

事件源(input source)

按照官方文档的分类

Port-Based Sources (基于端口,跟其他线程交互,通过内核发布的消息)
Custom Input Sources (自定义)
Cocoa Perform Selector Sources (performSelector…方法)

按照函数调用栈的分类

Source0:非基于Port的
Source1:基于Port的

• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

定时源(timer source)

定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定时间或者重复的时间间隔。定时源则直接传递消息给处理例程,不会立即退出run loop。

需要注意的是,尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的run loop的特定模式相关。如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到run loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次run loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不启动。

创建定时器源有两种方法,
方法一:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
                                          target:self                                                                           
                                       selector:@selector(backgroundThreadFire:) 
                                       userInfo:nil
                                        repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

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

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

  • NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响
  • GCD的定时器不受RunLoop的Mode影响

开发中,我们需要一个定时操作,来循环的执行任务,如果这个任务是不耗时的,我们可以选择NSTimer和GCD,如果任务是耗时的,我们通常会在子线程创建一个定时器,来执行耗时任务,当选择NSTimer时计时,定时器里的任务不会执行,原因是当前线程对相应的RunLoop并没有开启,创建线程的函数执行完成之后,这个线程就被销毁了(可以用继承CustomThread : NSThread,重写dealloc方法来验证),此时,你可能想用一个全局的对象来保存线程,任然不能解决问题,全局的对象描述指向线程的那个引用地址,这和线程的实体没有关系,并不能代替一个实在的线程。
解决方案:开启当前线程的RunLoop,当不需要任务的时候关闭

 //创建一个线程
 CustomThread *thread = [[CustomThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer  forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    while (!_isfinished) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
    }
    NSLog(@"线程开始了");
}];
//开启线程
[thread start];
- (void)timerAction:(id) objc
 {
      while (_isfinished) {
      [NSThread exit];//不需要时,关闭当前线程
  }
  NSLog(@"定时器开启了");
}

GCD执行,不存在上述问题,因为,系统内部默认给我们开启当前线程的RunLoop,性能更加稳定,还不存在内存泄露。

/*timer需要全局变量*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_currentTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_currentTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_currentTimer, ^{
     NSLog(@"-----开始前线程:%@----", [NSThread currentThread]);
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程中实现需要的功能
      NSLog(@"-----开始后线程:%@----", [NSThread currentThread]);
    });
});
dispatch_resume(_currentTimer);
/*
 // 挂起定时器(dispatch_suspend 之后的 Timer,是不能被释放的!会引起崩溃)
 dispatch_suspend(_timer);
 
 // 关闭定时器
 dispatch_source_cancel(_timer);
 */

选择 GCD 还是 NSTimer? 参考文章

CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

应用
//添加runloop监听者
- (void)addRunloopObserver{

//获取 当前的Runloop ref - 指针
CFRunLoopRef currentRunLoop =  CFRunLoopGetCurrent();
//定义一个RunloopObserver
CFRunLoopObserverRef defaultModeObserver;
//上下文
/*
typedef struct {
    CFIndex version; //版本号 long
    void *  info;//万能指针,这里我们要填写对象(self或者传进来的对象)
    const void *(*retain)(const void *info);        //填写&CFRetain
    void    (*release)(const void *info);           //填写&CGFRelease
    CFStringRef (*copyDescription)(const void *info); //NULL
 } CFRunLoopObserverContext;
 */
CFRunLoopObserverContext context = {
    0,
    (__bridge void *)(self),
    &CFRetain,
    &CFRelease,
    NULL
};
/*
 1 NULL空指针 nil空对象 这里填写NULL
 2 模式
   typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 
   kCFRunLoopEntry = (1UL << 0), //即将进入Runloop
   kCFRunLoopBeforeTimers = (1UL << 1), //即将处理NSTimer 
   kCFRunLoopBeforeSources = (1UL << 2), //即将处理Sources 
   kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 
   kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒 
   kCFRunLoopExit = (1UL << 7), //即将退出runloop 
   kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变};
 3 是否重复 - YES
 4 nil 或者 NSIntegerMax - 999
 5 回调
 6 上下文
 */
//创建观察者
defaultModeObserver = CFRunLoopObserverCreate(NULL,
                                              kCFRunLoopBeforeWaiting, YES,
                                              NSIntegerMax - 999,
                                              &Callback,
                                              &context);
//添加当前runloop的观察着
if(defaultModeObserver){
    CFRunLoopAddObserver(currentRunLoop, defaultModeObserver, kCFRunLoopDefaultMode);
    //释放
    CFRelease(defaultModeObserver);
   }
}
//这里处理耗时操作了
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
   //通过info桥接为需要的对象
}

CFRunLoopModeRef

系统默认注册了5个Mode:(前两个跟最后一个常用)
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个
Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触
摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 
Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:


RunLoop_0.png

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

RunLoop的事件队列

RunLoop_1.png

从这个事件队列中可以看出:

①如果是事件到达,消息会被传递给相应的处理程序来处理, runloop处理完当次事件后,run loop会退出,而不管之前预定的时间到了没有。你可以重新启动run loop来等待下一事件。

②如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会休眠等待相应事件的发生。这就是为什么run loop可以做到让线程有工作的时候忙于工作,而没工作的时候处于休眠状态。

RunLoop 的实际应用举例

AF2.x,创建了一条常驻线程专门处理所有请求的回调事件。

 + (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 添加进去了。

参考文章

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

推荐阅读更多精彩内容