iOS RunLoop详解

一.RunLoop介绍

  • 1.概念

RunLoop是一个运行循环,正是因为RunLoop,IOS才可以保持程序的持续运行,处理App中的各种事件,并且可以节省CPU资源,提高性能(因为RunLoop可以做到工作休息两不误)。因为一般来讲,一个线程在处理完一个任务以后就会退出。

线程与RunLoop

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要通过系统提供的方法进行获取
  • 获取RunLoop以后,如果没有事件源和Timer事件或者没有设置RunLoop运行模式,RunLoop会在获取以后立即销毁;如果超时,RunLoop也会被销毁。
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
UIApplicationMain内部就开启了一个RunLoop,这个函数是没有返回值的,因为RunLoop是一个运行循环。如果此处改为:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return 0;
    }
}
App在启动以后,就会结束运行。
  • 2.如何访问RunLoop对象

  • Foundation
    NSRunLoop
    [NSRunLoop currentRunLoop];//获取当前线程RunLoop,如果当前线程RunLoop未创建,则创建。
    [NSRunLoop mainRunLoop];//获取主线程RunLoop
  • Core Foundation
    CFRunLoopRef
    CFRunLoopGetCurrent();//获取当前线程RunLoop,如果当前线程RunLoop未创建,则创建。
    CFRunLoopGetMain();//获取主线程RunLoop

NSRunLoop是CFRunLoopRef的OC封装

  • 3.RunLoop相关类介绍

  • CFRunLoopRef【RunLoop对象】
  • CFRunLoopModeRef【RunLoop的运行模式】
  • CFRunLoopSourceRef【RunLoop要处理的事件源】
  • CFRunLoopTimerRef【Timer事件】
  • CFRunLoopObserverRef【RunLoop观察者】
RunLoop.jpg

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

  • CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式(下面列举5种)

NSDefaultRunLoopMode(Cocoa)/kCFRunLoopDefaultMode(Core Foundation):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
NSRunLoopCommonModes(Cocoa)/kCFRunLoopCommonModes(Core Foundation): 这是一个占位用的Mode,不是一种真正的Mode
  • 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
  • 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
    这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
  • kCFRunLoopCommonModes是一种模式组合,IOS系统中默认包含了kCFRunLoopDefaultMode和UITrackingRunLoopMode,系统会分别注册这两种模式,还可以通过CFRunLoopAddCommonMode()将自定义Mode放到kCFRunLoopCommonModes中。
  • CFRunLoopSourceRef
  • 按照官方文档的分类
    Port-Based Sources (基于端口,跟其他线程交互,通过内核发布的消息)
    Custom Input Sources (自定义)
    Cocoa Perform Selector Sources (performSelector…方法)
  • 按照函数调用栈的分类
    Source0:非基于Port的
    Source1:基于Port的

Source0: event事件,只含有回调,需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop。
Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop 的线程。

  • CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器
基本上说的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响
GCD的定时器不受RunLoop的Mode影响

  • CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

可以监听的RunLoop状态
/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU//以上所有状态
};

监听RunLoop状态示例:

    //新建子线程
- (void)viewDidLoad {
    [super viewDidLoad];
    //新建子线程
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadTask) object:nil];
    [self.thread start];
}

- (void)subThreadTask {
    //创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
            {
                NSLog(@"即将进入RunLoop");
            }
                break;
            case kCFRunLoopBeforeTimers:
            {
                NSLog(@"即将处理Timer");
            }
                break;
            case kCFRunLoopBeforeSources:
            {
                NSLog(@"即将处理Source");
            }
                break;
            case kCFRunLoopBeforeWaiting:
            {
                NSLog(@"即将进入休眠");
            }
                break;
            case kCFRunLoopAfterWaiting:
            {
                NSLog(@"即将从休眠中唤醒");
            }
                break;
            case kCFRunLoopExit:
            {
                NSLog(@"即将退出RunLoop");
            }
                break;
            default:
                break;
        }
    });
    //添加观察者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    //释放资源
    CFRelease(observer);
    
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    self.timer = [NSTimer timerWithTimeInterval:2.0f target:self selector:@selector(timerActon) userInfo:nil repeats:YES];
    [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4.0f]];
}

- (void)timerActon {
    NSLog(@"定时器工作了");
}

运行程序控制台输出:
2018-05-24 09:32:04.495445+0800 Test[1059:57057] 即将进入RunLoop
2018-05-24 09:32:04.499081+0800 Test[1059:57057] 即将处理Timer
2018-05-24 09:32:04.502540+0800 Test[1059:57057] 即将处理Source
2018-05-24 09:32:04.503239+0800 Test[1059:57057] 即将进入休眠
2018-05-24 09:32:06.497401+0800 Test[1059:57057] 即将从休眠中唤醒
2018-05-24 09:32:06.497973+0800 Test[1059:57057] 定时器工作了
2018-05-24 09:32:06.498469+0800 Test[1059:57057] 即将处理Timer
2018-05-24 09:32:06.498736+0800 Test[1059:57057] 即将处理Source
2018-05-24 09:32:06.499872+0800 Test[1059:57057] 即将进入休眠
2018-05-24 09:32:08.500044+0800 Test[1059:57057] 即将从休眠中唤醒
2018-05-24 09:32:08.500392+0800 Test[1059:57057] 定时器工作了
2018-05-24 09:32:08.500790+0800 Test[1059:57057] 即将退出RunLoop
注:因为定时器是每两秒钟调用一次,子线程的runLoop在4秒钟以后会销毁,所以定时器会输出两次。4秒钟以后,runLoop会销毁,在销毁以前观察者会收到"即将推出RunLoop"的状态通知。

二.RunLoop流程

  • 1.官方文档
    RunLoop官方流程图

    来自Apple官方文档翻译
  • 2.网友对官方文档的整理
    RunLoop流程

三.RunLoop应用

  • 1.NSTimer
    创建一个tableView和一个定时器,定时器用于显示当前时间。当tableView静止的时候,定时器正常工作。当拖拽tableView的时候,定时器停止了工作。
    定时器的创建代码:
    //创建定时器 并指定RunLoop运行模式为NSDefaultRunLoopMode
    self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
NSDefaultRunLoopMode下的定时器.gif

如果需要在列表滑动时定时器继续工作,则需要指定RunLoop运行模式为NSRunLoopCommonModes

    //创建定时器 并指定RunLoop运行模式为NSRunLoopCommonModes
    self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes下的定时器.gif
  • 2.PerformSelector
    PerformSelector 可以指定在何种RunLoopMode下进行工作
    //testAction仅在当前RunLoop处于UITrackingRunLoopMode下工作,即ScrollView滚动的时候
    [self performSelector:@selector(testAction) withObject:nil afterDelay:2.0f inModes:@[UITrackingRunLoopMode]];
  • 3.常驻线程
    有时候我们需要让一个线程保活,即一直处于可以执行任务的状态。这时候我们就需要用到RunLoop。
    一般情况,一个线程在任务执行完毕以后,是不可以去执行其他工作的:
- (IBAction)openSubThreadAndexecutingMethodA:(id)sender {
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(methodA) object:nil];
    [self.thread start];
}

- (IBAction)executingMethodB:(id)sender {
    [self performSelector:@selector(methodAB) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)methodA {
    NSLog(@"方法A被执行");
}

- (void)methodAB {
    NSLog(@"方法B被执行");
}
点击按钮执行上述代码中的方法.png

当我们点击"开启子线程并执行MethodA"进行子线程创建,并且让MethodA在子线程中执行。当MethodA执行完毕以后,我们如果点击
“让子线程去执行MethodB”按钮,让MethodB在之前创建的子线程中去执行的话,程序就会崩溃。因为当MethodA被执行完毕以后,子线程已经处于finished状态,系统会将其释放。
解决上述问题,让子线程进行常驻,随时可以执行任务:

将上述methodA方法改为:
- (void)methodA {
    //在当前子线程中开启一个RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
    NSLog(@"方法A被执行");
}

此时我们点击"开启子线程并执行MethodA"创建子线程,并且执行methodA。methodA方法是不会输出"方法A被执行"的。因为我们创建了一个持续运行的RunLoop。只有RunLoop结束的时候,此语句才会被输出。我们点击“让子线程去执行MethodB”按钮去执行methodB,程序正常运行。并且我们如果获取此子线程的状态,它处于正在运行的状态,这就达到了保活的目的。在开启RunLoop的时候,我们需要添加事件源或者Timer,否则RunLoop在获取以后,会立马运行结束。

  • 4.自动释放池

第一次创建的时机:即将进入runloop的时候【kCFRunLoopEntry】
释放的时机:runloop进入休眠状态【kCFRunLoopBeforeWaiting】,或者退出runLoop时【kCFRunLoopExit】。

  • 注: 当runloop即将休眠的时候会把之前的自动释放池释放,然后重新创建一个新的释放池。

四.RunLoop参考资料

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

推荐阅读更多精彩内容